1. はじめに
前回の記事社会人野球のEloレーティングを算出してみた(2025年版)では、JABA大会や都市対抗野球などの結果データをもとに、社会人野球チームの単年のEloレーティングを算出することに挑戦してみました。
今回の記事ではその続編として、以下の3点を中心に拡張しました。
1️⃣ 複数年(2021〜2025)の試合結果を対象に、レーティングを累積的に算出
2️⃣ 年度をまたいだチーム名変更・表記ゆれを共通辞書で統一処理
3️⃣ レーティング履歴CSVから、バーチャートレース動画を自動生成
これにより、数年間にわたるチームのレーティング(強さ)の推移を視覚的に表現してみました。
こちらが作成したバーチャートレースです
2. 対象とした大会と期間
- 対象期間:2021年〜2025年
- 対象大会
- JABA 日本選手権対象大会(各地区大会)
- 都市対抗野球大会(予選および本戦)
- 社会人野球日本選手権(予選および本戦)
ただし、2025年11月上旬時点では日本選手権本戦が進行中のため、
本戦分は後日データ更新時に反映予定です。
なお、結果は一球速報の社会人野球のページを見て一つ一つcsvに手で記載してきました。
3. 入力データ(CSV形式)
1試合を1行として記録します。最低限必要なのは以下の5列です。
列名 内容 例
date 試合日 (YYYY-MM-DD) 2024-07-18
team_a チームA Honda
team_b チームB JR東日本
score_a チームAの得点 5
score_b チームBの得点 1
任意で以下の列も追加できるようにしました。
- tournament → 大会名(都市対抗、日本選手権など)
- stage → 本戦/予選/練習試合など(重み付けに利用)
また、重み付けとしてJABA 日本選手権対象大会や都市対抗予選は1.0、都市対抗本戦は1.5、練習試合は0.5(今回は入力データに含んでいません)としました。
サンプルCSVは以下の通りです。
2025/3/8,Honda鈴鹿,鷺宮製作所,4,5,第79回JABA東京スポニチ大会,regional
2025/3/8,NTT西日本,JR東日本,3,1,第79回JABA東京スポニチ大会,regional
2025/3/8,Honda熊本,JFE東日本,5,6,第79回JABA東京スポニチ大会,regional
2025/8/29,東芝,JR東日本,8,9,第96回都市対抗野球大会(1回戦),main
2025/8/30,TDK,鷺宮製作所,4,5,第96回都市対抗野球大会(1回戦),main
2025/8/30,JR東日本東北,トヨタ自動車,5,3,第96回都市対抗野球大会(1回戦),main
4. チーム名統一の考え方
社会人野球では、以下のような企業再編・チーム名変更などが頻繁に発生します。
- 「A社硬式野球部」 → 「Aホールディングス」
- 「B工業」 → 「Bテック」
このような変更を正しく扱うため、
旧名 → 統一名 の対応マップ(辞書)をコード内に定義しました。
これにより、同一チームのレーティング履歴が年度をまたいで一貫するようにしました。
5. レーティング計算の前提
複数年をまたいで Elo レーティングを算出する場合、初年度のレートの初期値をどう扱うかについて考えましたが、この記事では
- 2021年を起点年とし、すべてのチームを初期レート 1500 でスタート
- 2022年以降は前年の最終レートを引き継ぎ、累積的に更新
という形にしています。また、2021年中に登場しなかった新規チームは、登場時点で自動的にレーティング1500で参入するようにしました。
6. Eloレーティング算出プログラム全文(複数年・チーム名統一対応)
# -*- coding: utf-8 -*-
import math
import pandas as pd
# ===== 入力CSVをここで指定 =====
INPUT_FILES = [
"shakaijin_yakyu_2021.csv",
"shakaijin_yakyu_2022.csv",
"shakaijin_yakyu_2023.csv",
"shakaijin_yakyu_2024.csv",
"shakaijin_yakyu_2025.csv",
]
OUTPUT_PREFIX = "shakaijin_elo" # 出力ファイル接頭辞
# ===== 基本設定 =====
INITIAL_RATING = 1500.0
BASE_K = 20.0
ELO_SCALE = 400.0
# 試合ステージごとの重み
STAGE_K_WEIGHT = {
"main": 1.5, "regional": 1.0, "friendly": 0.5,
"本戦": 1.5, "予選": 1.0, "練習試合": 0.5,
}
# ===== チーム名統一マップ =====
TEAM_NAME_MAP = {
"かずさマジック": "日本製鉄かずさマジック",
"日本製鉄広畑": "日本製鉄瀬戸内",
"三菱自動車倉敷": "三菱自動車倉敷オーシャンズ",
}
# ===== 関数群 =====
def margin_of_victory_multiplier(score_diff: int, rating_diff: float) -> float:
"""点差補正(FiveThirtyEight風)"""
return math.log(abs(score_diff) + 1.0) * 2.2 / ((abs(rating_diff) * 0.001) + 2.2)
def expected_score(ra: float, rb: float) -> float:
"""A側の期待勝率"""
return 1.0 / (1.0 + 10.0 ** ((rb - ra) / ELO_SCALE))
def normalize_name(name: str) -> str:
"""チーム名の整形+統一"""
name = str(name).strip()
return TEAM_NAME_MAP.get(name, name)
def load_games(files):
"""複数CSVを読み込み、結合・日付昇順ソート"""
frames = []
rename_map = {
"Date": "date",
"Team A": "team_a",
"Team B": "team_b",
"Socre A": "score_a",
"Score A": "score_a",
"Score B": "score_b",
"tournament": "tournament",
"Tournament": "tournament",
"stage": "stage",
"Stage": "stage",
}
for f in files:
df = pd.read_csv(f)
df = df.rename(columns={k: v for k, v in rename_map.items() if k in df.columns})
frames.append(df)
games = pd.concat(frames, ignore_index=True)
# 必須列チェック
required = {"date", "team_a", "team_b", "score_a", "score_b"}
missing = required - set(games.columns)
if missing:
raise ValueError(f"必須列が不足しています: {missing}")
# 整形
games["date"] = pd.to_datetime(games["date"], errors="coerce")
games["team_a"] = games["team_a"].astype(str).str.strip().apply(normalize_name)
games["team_b"] = games["team_b"].astype(str).str.strip().apply(normalize_name)
games["score_a"] = pd.to_numeric(games["score_a"], errors="coerce").astype("Int64")
games["score_b"] = pd.to_numeric(games["score_b"], errors="coerce").astype("Int64")
# 無効行除外
games = games.dropna(subset=["date", "team_a", "team_b", "score_a", "score_b"])
games = games.sort_values("date").reset_index(drop=True)
return games
def snapshot_year_df(year: int, rating_dict: dict) -> pd.DataFrame:
"""指定年のスナップショット DataFrame を作成"""
df = pd.DataFrame([{"team": t, "rating": r} for t, r in rating_dict.items()])
if df.empty:
return df
df = df.sort_values("rating", ascending=False, ignore_index=True)
df.insert(0, "year", year)
return df
def main():
df = load_games(INPUT_FILES)
rating = {} # 累積レート
history_rows = [] # 試合ごとのレート履歴
per_year_tables = [] # 年度ごとのスナップショット
if len(df) == 0:
print("入力データが空です。")
return
for idx, row in df.iterrows():
this_year = int(row["date"].year)
a = row["team_a"]
b = row["team_b"]
sa, sb = int(row["score_a"]), int(row["score_b"])
stage = row.get("stage", None)
# 初期値
if a not in rating:
rating[a] = INITIAL_RATING
if b not in rating:
rating[b] = INITIAL_RATING
ra, rb = rating[a], rating[b]
ea = expected_score(ra, rb)
# 実績スコア
if sa > sb:
score_a = 1.0
elif sa < sb:
score_a = 0.0
else:
score_a = 0.5
# K値調整
k = BASE_K
if isinstance(stage, str):
k *= STAGE_K_WEIGHT.get(stage, 1.0)
# 点差補正
if sa != sb:
diff = abs(sa - sb)
rdiff_for_mov = (ra - rb) if sa > sb else (rb - ra)
mov = margin_of_victory_multiplier(diff, rdiff_for_mov)
else:
mov = 1.0
delta_a = k * mov * (score_a - ea)
rating[a] = ra + delta_a
rating[b] = rb - delta_a
# 履歴保存(日単位フォーマットに変更)
date_str = row["date"].strftime("%Y-%m-%d")
history_rows.append({
"date": date_str,
"match_index": idx + 1,
"team": a,
"opponent": b,
"score_for": sa,
"score_against": sb,
"rating": rating[a],
"stage": stage,
})
history_rows.append({
"date": date_str,
"match_index": idx + 1,
"team": b,
"opponent": a,
"score_for": sb,
"score_against": sa,
"rating": rating[b],
"stage": stage,
})
# 年度変わり目でスナップショット出力
next_is_new_year = (
idx == len(df) - 1
or int(df.iloc[idx + 1]["date"].year) != this_year
)
if next_is_new_year:
snap_df = snapshot_year_df(this_year, rating)
if not snap_df.empty:
per_year_tables.append(snap_df)
# 出力
final_year = int(df.iloc[-1]["date"].year)
final_table = snapshot_year_df(final_year, rating).drop(columns=["year"])
history = pd.DataFrame(history_rows).sort_values(["team", "match_index"]).reset_index(drop=True)
# 出力形式変更:年月日つき履歴CSV
final_table.to_csv(f"{OUTPUT_PREFIX}_final_ratings.csv", index=False)
history.to_csv(f"{OUTPUT_PREFIX}_rating_history.csv", index=False, encoding="utf-8-sig")
if per_year_tables:
all_years_df = pd.concat(per_year_tables, ignore_index=True)
all_years_df.to_csv(f"{OUTPUT_PREFIX}_yearly_snapshots.csv", index=False)
for y, g in all_years_df.groupby("year"):
g.drop(columns=["year"]).to_csv(f"{OUTPUT_PREFIX}_final_ratings_{y}.csv", index=False)
print(f"[DONE] 最終レーティング: {OUTPUT_PREFIX}_final_ratings.csv")
print(f"[DONE] 年度別スナップショット: {OUTPUT_PREFIX}_yearly_snapshots.csv")
print(f"[DONE] 日単位履歴: {OUTPUT_PREFIX}_rating_history.csv")
print("\n--- 最新Top 10 ---")
print(final_table.head(10).to_string(index=False))
if __name__ == "__main__":
main()
実行後の出力
- shakaijin_elo_rating_history.csv → 日別Eloレーティング履歴
7. 結果(レーティングの推移 TOP20)
| 順位 | 2021年 | 2022年 | 2023年 | 2024年 | 2025年 |
|---|---|---|---|---|---|
| 1 | 大阪ガス (1685.01) | ENEOS (1778.44) | トヨタ自動車 (1849.51) | トヨタ自動車 (1889.24) | トヨタ自動車 (1903.67) |
| 2 | 東京ガス (1650.39) | トヨタ自動車 (1770.36) | ENEOS (1775.90) | 三菱重工East (1812.31) | ヤマハ (1824.11) |
| 3 | ヤマハ (1637.69) | 東京ガス (1745.47) | 日本通運 (1767.03) | Honda (1776.48) | 王子 (1791.45) |
| 4 | セガサミー (1618.71) | NTT東日本 (1711.17) | 東京ガス (1743.16) | 日本通運 (1773.53) | ENEOS (1787.98) |
| 5 | NTT東日本 (1618.33) | 三菱重工East (1680.25) | Honda熊本 (1728.36) | ENEOS (1755.95) | 三菱重工East (1776.96) |
| 6 | 日立製作所 (1617.67) | 日本通運 (1675.49) | ヤマハ (1726.76) | ヤマハ (1749.04) | Honda (1768.81) |
| 7 | 東邦ガス (1609.77) | Honda熊本 (1674.01) | 三菱重工East (1716.50) | NTT東日本 (1730.28) | 日本通運 (1765.34) |
| 8 | 三菱重工East (1609.36) | 東芝 (1656.47) | Honda (1683.57) | 東京ガス (1729.91) | NTT西日本 (1733.34) |
| 9 | 日本通運 (1589.36) | 日立製作所 (1651.20) | 東芝 (1682.28) | 東芝 (1700.24) | SUBARU (1724.76) |
| 10 | トヨタ自動車 (1587.83) | 大阪ガス (1633.30) | JR東日本 (1680.68) | 西部ガス (1698.08) | JFE西日本 (1723.87) |
| 11 | ENEOS (1587.23) | セガサミー (1631.82) | NTT東日本 (1663.61) | 三菱重工West (1696.90) | 東芝 (1722.12) |
| 12 | Honda熊本 (1585.20) | JR東日本 (1621.18) | 三菱重工West (1661.64) | JFE西日本 (1686.68) | 三菱自動車岡崎 (1713.80) |
| 13 | JR東日本東北 (1575.83) | 三菱重工West (1616.61) | 王子 (1643.13) | Honda熊本 (1683.84) | 東京ガス (1710.31) |
| 14 | JFE東日本 (1571.10) | ヤマハ (1613.95) | 三菱自動車岡崎 (1641.12) | JR東日本東北 (1672.36) | Honda熊本 (1707.39) |
| 15 | 王子 (1570.94) | TDK (1607.08) | 西部ガス (1634.39) | JR東日本 (1665.23) | NTT東日本 (1695.20) |
| 16 | 伯和ビクトリーズ (1565.15) | Honda (1602.22) | パナソニック (1631.06) | 日本製鉄鹿島 (1657.48) | JR東日本東北 (1694.38) |
| 17 | JR九州 (1564.38) | 日本新薬 (1598.35) | 日立製作所 (1629.81) | 王子 (1655.58) | 三菱重工West (1690.66) |
| 18 | 日本製鉄かずさマジック (1563.98) | 日本製鉄鹿島 (1595.17) | SUBARU (1626.41) | 日本製鉄かずさマジック (1647.96) | 西部ガス (1689.87) |
| 19 | 日本製鉄瀬戸内 (1562.32) | 鷺宮製作所 (1594.14) | JFE西日本 (1625.56) | 明治安田生命 (1645.46) | JR東日本 (1678.52) |
| 20 | JR東日本 (1561.03) | 日本生命 (1590.76) | 大阪ガス (1616.97) | 日立製作所 (1645.35) | 日本生命 (1674.97) |
8. バーチャートレースで可視化
複数年分のEloレーティング推移を、日ごとのアニメーションとして表現しました。
使用ライブラリは以下です。
pip install bar_chart_race matplotlib pandas
バーチャートレース化するプログラムは以下になります。
# -*- coding: utf-8 -*-
import pandas as pd
import bar_chart_race as bcr
import matplotlib.pyplot as plt
# ===== 設定 =====
INPUT_CSV = "shakaijin_elo_rating_history.csv"
OUTPUT_VIDEO = "elo_barchart_race_daily.mp4"
TOP_N = 10
FPS = 15
TITLE = "社会人野球 Eloレーティング推移(日別)"
# ===== 日本語フォント設定 =====
plt.rcParams['font.family'] = ['Meiryo'] # Windows用
plt.rcParams['axes.unicode_minus'] = False
# ===== データ読み込み =====
df = pd.read_csv(INPUT_CSV, encoding="utf-8")
required = {"date", "team", "rating"}
if not required.issubset(df.columns):
raise ValueError(f"CSVに必要な列がありません: {required - set(df.columns)}")
df["date"] = pd.to_datetime(df["date"], errors="coerce")
df = df.dropna(subset=["date", "team", "rating"])
# ===== 日ごとの平均レーティング =====
daily = df.groupby(["date", "team"])["rating"].mean().unstack(fill_value=None)
# ===== 初期レーティング補完 =====
first_day = daily.index.min()
daily.loc[first_day] = daily.loc[first_day].fillna(1500)
daily = daily.sort_index()
daily = daily.ffill()
# レーティング変動のないチームを除外(任意)
daily = daily.loc[:, daily.std() > 0]
# ===== X軸の範囲を決める =====
x_min = 1400
x_max = daily.max().max() + 40
# ===== xlimを固定したFigureを作成 =====
fig, ax = plt.subplots(figsize=(7, 4.5))
ax.set_xlim(x_min, x_max)
ax.tick_params(axis='x', labelsize=8)
ax.tick_params(axis='y', labelsize=8)
# ===== バーチャートレース生成(x軸固定)=====
bcr.bar_chart_race(
df=daily,
filename=OUTPUT_VIDEO,
n_bars=TOP_N,
sort='desc',
title=TITLE,
fixed_order=False,
fixed_max=True, # 軸範囲を固定
interpolate_period=False,
steps_per_period=40,
period_length=3000 / FPS,
cmap='dark12',
bar_size=0.95,
period_fmt='%Y-%m-%d',
period_label={'x': 0.92, 'y': 0.1, 'ha': 'right', 'va': 'center'},
dpi=300,
fig=fig
)
print(f"[DONE] バーチャートレース動画を出力しました: {OUTPUT_VIDEO}")
実行後の出力
- elo_barchart_race_daily.mp4 → 日付ごとのレーティング推移アニメーション(1章で示した動画です)
9. まとめ
今回は以下の件を対応し、社会人野球チームのランキングをバーチャートレースで示しました。
1️⃣ 複数年(2021〜2025)の試合結果を対象に、レーティングを累積的に算出
2️⃣ 年度をまたいだチーム名変更・表記ゆれを共通辞書で統一処理
3️⃣ レーティング履歴CSVから、バーチャートレース動画を自動生成
今後は、2025年の社会人野球日本選手権の結果を反映したり、 日本選手権対象大会以外のJABA大会も含むなどしていきたいと考えています。