1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

社会人野球のEloレーティングを算出してみた(2025年版)

Posted at

1. 背景

私はスポーツ観戦が好きなのですが、その中でも特に社会人野球が好きで、都市対抗野球にはここ10年ほど毎年足を運んでいます(今年の都市対抗は、6試合も観戦しました。)
ただ、社会人野球はリーグ戦のように年間を通して戦うわけではないため、「どのチームが本当に強いのか」が分かりにくいと感じていました。もちろん強豪と呼ばれるチームは存在しますし、一発勝負ならではの魅力もあるのですが、もう少し客観的に強さを測れないだろうかと思いました。

そこで着目したのが、チェスなどで使われている Eloレーティングです。勝敗データを基に計算すれば、社会人野球のチームの強さを数値として表せるのではないかと考え、実際に試してみたというのがこの記事の内容です。

注:Eloレーティングの説明はQiitaの他の記事で優れた解説がなされていますので、ここでは割愛させていただきます。

2. 対象とした大会

今回対象としたのは以下の大会です。

  • 2025年度 JABA 日本選手権対象大会
  • 2025年度 都市対抗野球大会の予選および本戦

これらの試合結果をCSVにまとめ、Eloレーティングを算出しました。

結果は一球速報の社会人野球のページを見て一つ一つ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,79JABA東京スポニチ大会,regional
2025/3/8,NTT西日本,JR東日本,3,1,79JABA東京スポニチ大会,regional
2025/3/8,Honda熊本,JFE東日本,5,6,79JABA東京スポニチ大会,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. 作成したpythonプログラム

以下が実際に使用したプログラムです。複数年のCSVを読み込み、累積的にレーティングを更新できるようにしています。さらに、各年末時点のスナップショットをCSVとして保存する仕組みも入れています。

ただ今回は2025年度のJABA 日本選手権対象大会と都市対抗野球大会の予選および本戦のみを対象としました。

また今回はすべてのチームを同じスタートラインから評価するため、初期レーティングは1500点で統一しました。その後、各試合の結果に基づいてレートが上下していきます。

# -*- coding: utf-8 -*-
"""
社会人野球 Elo レーティング計算(複数CSV対応・累積・年度別スナップショット出力)
- 入力: INPUT_FILES にリスト指定(1つでも可)
- 年度ごとの「その年最後の試合後のレーティング」を CSV で出力
- シーズンリセットなし(累積)
"""

import math
import pandas as pd

# ===== 入力CSVをここで指定 =====
INPUT_FILES = [
    # "shakaijin_yakyu_2020.csv",
    # "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 列の重み(必要に応じて調整)
STAGE_K_WEIGHT = {
    "main": 1.5, "regional": 1.0, "friendly": 0.5,
    "本戦": 1.5, "予選": 1.0, "練習試合": 0.5,  # 日本語運用もOK
}

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:
    return str(name).strip()

def load_games(files):
    """複数CSVを結合し、日付昇順に整える"""
    frames = []
    rename_map = {
        "Date": "date",
        "Team A": "team_a",
        "Team B": "team_b",
        "Socre A": "score_a",  # typo対策
        "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()
    games["team_b"] = games["team_b"].astype(str).str.strip()
    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
    current_year = int(df.iloc[0]["date"].year)

    for idx, row in df.iterrows():
        a, b = normalize_name(row["team_a"]), normalize_name(row["team_b"])
        sa, sb = int(row["score_a"]), int(row["score_b"])
        stage = row["stage"] if "stage" in df.columns else 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

        # 履歴
        history_rows.append({
            "date": row["date"].date(),
            "match_index": idx + 1,
            "team": a,
            "opponent": b,
            "score_for": sa,
            "score_against": sb,
            "rating": rating[a],
            "stage": stage,
        })
        history_rows.append({
            "date": row["date"].date(),
            "match_index": idx + 1,
            "team": b,
            "opponent": a,
            "score_for": sb,
            "score_against": sa,
            "rating": rating[b],
            "stage": stage,
        })

        # 年度境界を検知(次の行が別年 or 最終行)→ スナップショット保存
        this_year = int(row["date"].year)
        next_is_new_year = False
        if idx == len(df) - 1:
            next_is_new_year = True
        else:
            next_year = int(df.iloc[idx + 1]["date"].year)
            next_is_new_year = (next_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_table = snapshot_year_df(int(df.iloc[-1]["date"].year), rating)
    final_table = final_table.drop(columns=["year"]).rename(columns={"rating": "rating"})  # 見やすく

    history = pd.DataFrame(history_rows).sort_values(["team", "match_index"]).reset_index(drop=True)

    # 出力:全体
    out_final = f"{OUTPUT_PREFIX}_final_ratings.csv"
    out_history = f"{OUTPUT_PREFIX}_rating_history.csv"
    final_table.to_csv(out_final, index=False)
    history.to_csv(out_history, index=False)
    print(f"[DONE] 最終レーティング: {out_final}")
    print(f"[DONE] レーティング履歴: {out_history}")

    # 出力:年度別(個別ファイル & まとめ)
    if per_year_tables:
        all_years_df = pd.concat(per_year_tables, ignore_index=True)
        all_years_path = f"{OUTPUT_PREFIX}_yearly_snapshots.csv"
        all_years_df.to_csv(all_years_path, index=False)
        print(f"[DONE] 年度別スナップショット(まとめ): {all_years_path}")

        # 年ごとに分割保存
        for y, g in all_years_df.groupby("year"):
            path_y = f"{OUTPUT_PREFIX}_final_ratings_{y}.csv"
            g.drop(columns=["year"]).to_csv(path_y, index=False)
            print(f"{y} 年末レーティング: {path_y}")

    # サマリの出力
    print("\n--- 最新Top 10 ---")
    print(final_table.head(10).to_string(index=False))

if __name__ == "__main__":
    main()

5. 結果(2025年TOP20)

計算結果は以下になります。やはり対象大会の2大会で優勝しており、都市対抗でもベスト4に入っているヤマハが1位でした。

順位 チーム レーティング
1 ヤマハ 1687.0
2 王子 1676.7
3 トヨタ自動車 1650.4
4 SUBARU 1625.8
5 NTT西日本 1624.3
6 三菱自動車岡崎 1608.7
7 ENEOS 1606.0
8 Honda熊本 1595.5
9 日本通運 1595.3
10 鷺宮製作所 1594.5
11 JFE西日本 1585.0
12 東芝 1577.4
13 日本生命 1566.2
14 Honda 1565.8
15 西濃運輸 1556.2
16 三菱重工East 1554.5
17 JR東日本東北 1553.9
18 JR東日本 1551.5
19 Honda鈴鹿 1549.2
20 JFE東日本 1549.0

6. まとめと今後の課題

今回は2025年度のJABA 日本選手権対象大会と都市対抗予選・本戦の結果のみでレーティングを算出しましたが、やはり強豪と言われているチームが上位に入ってきているなと感じました。ただ、他にもまだ有名なチームもありますし、年度によってだいぶ変わると思います。
次は過去5〜10年分の結果を集計して、年度ごとのレーティング推移を出したと考えています。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?