【2026年3月 修正】 バックテストの評価条件を見直しました。Marcel法と機械学習を同じ選手セット(PA≥100 / IP≥30)で比較し直した結果、打者OPSはほぼ同等、投手ERAはMarcel優位という結論になります。以下の本文は修正済みです。
はじめに
「来年、この選手はどのくらいの成績を残せるか?」
この問いに答えるため、NPB(日本プロ野球)の選手成績予測システムを作りました。
→ GitHub: https://github.com/yasumorishima/npb-prediction
予測手法は2種類用意しました。
- Marcel法: 1980年代に考案されたシンプルな統計手法
- LightGBM / XGBoost: 現代的な機械学習
結果として、投手ERA予測ではMarcel法がMLを上回り、打者OPS予測では両者がほぼ同等でした。
はじめての方へ:この記事で登場する用語
| 用語 | 意味 |
|---|---|
| Marcel法 | 過去3年の成績を加重平均して翌年を予測するシンプルな手法。重みは直近ほど高い(5:4:3) |
| LightGBM / XGBoost | 勾配ブースティング木モデル。現代的な機械学習アルゴリズムで特徴量から予測する |
| OPS | 出塁率+長打率。打者の打撃力を表す総合指標 |
| ERA(防御率) | 9イニング当たりの自責点。投手の基本的な成績指標 |
| MAE | 平均絶対誤差。予測と実績のずれの平均。小さいほど精度が高い |
| バックテスト | 過去のデータを使って予測モデルの精度を検証すること |
データ取得
データソースは2つです。
- プロ野球データFreak(baseball-data.com) — 打者・投手の年度別成績(2015-2025)
- NPB公式サイト(npb.jp) — 詳細打撃成績(2塁打・3塁打・犠飛、wOBA算出用)
pandas.read_html() でHTMLテーブルを取得しています。
| ファイル | 内容 |
|---|---|
| 打者成績 | 3,780行(2015-2025) |
| 投手成績 | 3,773行(2015-2025) |
| 順位表 | 132行(12球団×11年) |
| 生年月日 | 2,479人 |
| 詳細打撃成績 | 4,538行(npb.jp) |
| 支配下登録選手名鑑 | 7,866行(2018-2025、Marcel予測フィルタ用) |
データの提供元:プロ野球データFreak、NPB公式サイト
Marcel法とは
Tom Tangoが考案した成績予測手法です。実装はシンプルです。
ステップ1: 過去3年の加重平均
直近の成績ほど重要という考えのもと、5/4/3 の比率で加重平均します。
weight_map = {0: 5, 1: 4, 2: 3} # 0=直近、1=2年前、2=3年前
ステップ2: リーグ平均への回帰
サンプルが少ない選手は、リーグ平均に引き戻します。
regression = 1200 / (pa + 1200)
predicted = (1 - regression) * weighted + regression * league_avg
ステップ3: 年齢調整
ピーク年齢(27歳)からの距離に応じて成績を増減させます。
age_factor = 1.0 + (27 - age) * 0.003 # 27歳でピーク
ステップ4: 選手名鑑フィルタ(退団・MLB移籍選手の除外)
Marcel法はロースターに登録された選手を予測対象とします。しかし前年まで活躍していた選手がオフにMLB移籍・退団・引退した場合、そのままでは予測に「幽霊選手」として残ってしまいます。
これを防ぐため、baseball-data.comの年別支配下登録選手一覧(fetch_rosters.pyで取得)を使い、その年のNPBに在籍していない選手を予測から除外しています。
roster_names = set(df_roster[df_roster["year"] == target_year]["player"])
mh = mh[mh["player"].isin(roster_names)] # 名鑑外の打者を除外
mp = mp[mp["player"].isin(roster_names)] # 名鑑外の投手を除外
この処理により、例えば2021年の予測では秋山翔吾・筒香嘉智・菊池雄星(いずれもMLB移籍)が正しく除外され、各チームの予測精度が改善されます。
wOBA / wRC+ を自前算出
打者の総合得点貢献を測る wOBA と、リーグ平均を100とした wRC+ を、NPBの公式データから自前で算出しました。
MLB(Baseball Savant等)と違い、NPBには公式のwOBA公開値がないため、各イベントに重みをつけて計算しています。
# wOBA計算(NPBリーグ環境に合わせた係数)
woba = (
0.69 * BB + 0.72 * HBP +
0.89 * H1B + 1.27 * H2B +
1.62 * H3B + 2.10 * HR
) / (AB + BB + HBP + SF)
2024年 wRC+ Top3(自前算出)
| 選手 | チーム | wOBA | wRC+ |
|---|---|---|---|
| 近藤健介 | ソフトバンク | .479 | 249 |
| オースティン | DeNA | .478 | 248 |
| サンタナ | 楽天 | .441 | 220 |
機械学習(LightGBM / XGBoost)
特徴量には年齢・過去成績に加え、wOBA/wRC+も追加しました。
features = [
'age', 'OPS_prev1', 'OPS_prev2', 'OPS_prev3',
'woba', 'wrc_plus',
'PA_prev1', 'PA_prev2'
]
精度比較(2025年バックテスト)
2015〜2024年のデータで学習し、2025年の実績を予測するバックテストを実施しました。Marcel法と機械学習の比較は、同じ選手セット(打者PA≥100・投手IP≥30)で統一しています。
打者 OPS MAE(2025年、n=172選手)
| 手法 | OPS MAE |
|---|---|
| Marcel法 | .063 |
| XGBoost | .063 |
| LightGBM | .066 |
→ ほぼ同等(差は0.001以下)。
投手 ERA MAE(2025年、n=145選手)
| 手法 | ERA MAE |
|---|---|
| Marcel法 | 0.78 |
| XGBoost | 0.93 |
| LightGBM | 0.92 |
→ Marcel法が上回りました(差は約0.14)。
投手ERA予測ではMarcel法が明確に上回りました。打者OPS予測では両者がほぼ同等でした。シンプルな手法がMLと同等以上の精度を出せることを、NPBデータで確認できました。
なぜこうなるのか、考えられる理由:
- 選手の真の実力は1〜2年程度しか変化しない
- MLは過学習しやすく、サンプル数が限られると精度が落ちる
- シンプルな加重平均が、実際の成績変化の分布にフィットしている
選手ストーリー: 予測が当たるケースと外れるケース
バックテストの数値だけでは伝わりにくいので、具体的な選手で見てみます。
オースティン(DeNA)— 同じ選手で大外れと的中の両方
2022-2023年のケガによる離脱からの復活劇を追うと、Marcel法の強みと弱みが両方見えます。
2024年予測(2021-2023データで予測):
| OPS | PA | |
|---|---|---|
| Marcel予測 | .818 | 145 |
| 2024実績 | .983 | 445 |
| 誤差 | .165 | — |
ケガで2022年38打席・2023年54打席しか立てなかったため、Marcel法はリーグ平均方向に大きく回帰。結果としてOPS .983の大復活を予測できませんでした。
2025年予測(2022-2024データで予測):
| OPS | PA | |
|---|---|---|
| Marcel予測 | .842 | 213 |
| 2025実績 | .834 | 246 |
| 誤差 | .008 | — |
ところが翌年の予測では誤差わずか .008。2024年の好成績がデータに入ったことで、リーグ平均への回帰が「高すぎる成績を適度に引き下げる」方向に作用し、結果的に非常に的確な予測となりました。
同じ選手・同じ手法でも、データの状況次第で精度が大きく変わるという好例です。
筒香嘉智(DeNA)— 復活を予測できなかったケース
2019年までのNPB時代はOPS .900前後の強打者でしたが、MLB挑戦を経て2024年にNPBへ復帰。2024年は168打席でOPS .683と苦しみました。
| OPS | PA | |
|---|---|---|
| Marcel予測 | .656 | 168 |
| 2025実績 | .876 | 257 |
| 誤差 | .220 | — |
Marcel法は2024年の不振に引っ張られ、OPS .656と低めに予測。しかし2025年の筒香選手は20本塁打を放ちOPS .876と復活を遂げました。
Marcel法の限界が見えるケースです。直近の成績に重みを置く設計上、前年までのトレンドを大きく上回るような成績変化には対応しにくいと考えられます。
ピタゴラス勝率
チームの強さを「得点と失点の比率」で予測する手法です。
勝率 ≈ 得点^k / (得点^k + 失点^k)
指数 k はMLBでは 1.83 が標準ですが、NPBでの最適値を探したところ k=1.72 でした。
| 指数 | MAE | 対象 |
|---|---|---|
| NPB最適(k=1.72) | 3.20勝 | 全12球団(2015-2025) |
| MLB標準(k=1.83) | 3.32勝 | 同上 |
FastAPIで推論APIを構築
pip install -r requirements.txt
uvicorn api:app --reload
# → http://localhost:8000/docs でSwagger UI起動
エンドポイント一覧
| パス | 内容 |
|---|---|
GET /predict/hitter/{name} |
打者の翌年予測(Marcel + ML) |
GET /predict/pitcher/{name} |
投手の翌年予測(Marcel + ML) |
GET /predict/team/{name} |
チームのピタゴラス勝率 |
GET /sabermetrics/{name} |
wOBA / wRC+ / wRAA |
GET /rankings/hitters |
打者ランキング |
GET /rankings/pitchers |
投手ランキング |
GET /pythagorean |
全チームのピタゴラス勝率 |
レスポンス例(牧秀悟 翌年予測)
{
"player": "牧 秀悟",
"team": "DeNA",
"marcel": { "OPS": 0.834, "AVG": 0.295, "HR": 22.9, "RBI": 81.4 },
"ml": { "pred_OPS": 0.874 }
}
Docker対応もしており、docker compose up --build で一発起動できます。
Raspberry Pi + Tailscale Funnelで外部公開
ローカルで動かすだけでなく、Raspberry Pi 5にDockerごとデプロイして常時稼働させています。さらにTailscale Funnelを使ってインターネットに公開しました。
# RPiでFunnelを有効化(1コマンドで完了)
sudo tailscale funnel --bg 8000
これだけで固定のHTTPS URLが発行され、外部からSwagger UIを操作できます。VPNサービスのTailscaleが証明書管理・ポートフォワード・NAT越えをすべて肩代わりしてくれるため、ルーター設定なしで公開できるのが手軽でした。
チーム編成シミュレーション
v0.3.0で /simulate/team/{team} エンドポイントを追加しました。選手を入れ替えて勝数の変化をシミュレーションできます。
GET /simulate/team/DeNA?year=2025&add=山川&remove=宮﨑
wRAAベースで得点を加減算し、ピタゴラス勝率を再計算する仕組みです。
Streamlitダッシュボード
APIの全機能をブラウザで操作できるStreamlitダッシュボードも用意しました。
pip install -r requirements.txt
streamlit run streamlit_app.py
7ページ構成で、打者/投手予測・ランキング・ピタゴラス順位表・チーム勝率をインタラクティブに操作できます。チャートはPlotlyで、NPB12球団のチームカラーに対応しています。日本語/英語の2言語切り替えに対応しています。
打者ランキング: wOBA / wRC+ ソート追加
OPS/AVG/HR/RBIに加え、wOBA(打席あたりの得点貢献度)とwRC+(リーグ平均=100の打撃力)でもソートできるようになりました。
投手ランキング: FIP / K% / BB% 等の高度な指標追加
ERA/WHIPに加えて、FIP(味方の守備に左右されない投手の真の実力)や、K%(三振率)、BB%(四球率)、K-BB%(三振率−四球率)、K/9(9イニングあたり奪三振)、BB/9、HR/9 でもソートできます。
FIP = (13×被本塁打 + 3×(四球+死球) - 2×奪三振) / 投球回 + 定数C
FIPが防御率より低い投手は「味方の守備に恵まれず、実力以上に打たれている」可能性があり、逆にFIPが防御率より高い投手は「守備に助けられている」可能性があると考えられます。
予測ページ: 計算式の解説
打者予測ではwOBA/wRC+/wRAAの数値カードとwRC+推移グラフを表示します。投手予測ではFIP/K%/BB%/K-BB%/K9/BB9/HR9の数値カードを表示し、K/9・BB/9・HR/9にはリーグ平均との差分も表示します。各指標には計算式と基準値の解説expander付きです。
レーダーチャートも更新し、打者はHR/AVG/OBP/SLG/wOBA/wRC+の6軸、投手はERA/WHIP/奪三振/K9/BB9/HR9/FIPの7軸で描画します(OPS・勝利・投球回は除外し、セイバー指標を重視した構成)。
トップページ: wRC+ / FIP 順のTOP3 + 先発/リリーフ別
トップページの打者TOP3をwRC+順(OPSから変更)、投手TOP3をFIP順(ERAから変更)に変更しました。いずれも守備の影響を受けにくい指標で、より純粋な打撃力・投球力を反映します。
また投手を先発(投球回100以上)とリリーフ(投球回20〜99)に分けてそれぞれFIP順でTOP3を表示するようにしました。投球回フィルターによる分類のため、怪我等で投球回が少ない先発投手が混入する場合があります。
さらに、カード内の棒グラフとレーダーチャートの指標を統一し、打者カードにwOBA/wRC+、投手カードにFIP/K9/BB9/HR9を追加しました。
現在の課題と今後の改善案
新外国人・新人選手の扱い
Marcel法は過去3年のNPBデータを必要とするため、新外国人・新人・長期離脱からの復帰選手は計算対象外になります。現在の実装では、これらの選手を**「リーグ平均の貢献(wRAA=0)」**として暗黙的に扱っています。
ダッシュボードでは計算外選手をチームカードのバッジとexpanderで可視化しました。さらに予測幅(信頼区間)を実装し、不確実性をグラフ上に表示しています。
✅ 実装済み: 予測幅(信頼区間)
計算外選手はwRAA=0(リーグ平均と同等の貢献)として計算されますが、実際の初年度成績には大きなばらつきがあります。
考え方:
- 歴史的にNPB外国人選手の初年度wRAAは -15点〜+25点 のばらつきがある
- 野球統計の経験則: 10点の得失点差 ≈ 1勝(ピタゴラス勝率から導出)
- → 計算外選手1人あたりの不確実性 ≈ ±1.5勝 と設定
予測幅 = 計算外人数 × 1.5勝
例: 計算外3名のチーム → 予測70勝なら「67〜74勝」と表示
グラフのオレンジのエラーバーが予測幅です。計算外選手が多いチームほど幅が広く、「データのない選手の活躍次第で順位が大きく変わりうる」という示唆を数値で表現しました。
✅ 実装済み: NPB1〜2年目選手のデータ年数バッジ
「計算対象外」とは別の問題として、NPBデータが1〜2年しかない選手も予測の不確実性が高くなります。Marcel法のリーグ平均への回帰は、データが少ないほど強く働くからです。
# データ1年(60IP)の投手の場合: weighted_ip = 60×5 = 300
# proj_ERA = (選手ERA×300 + リーグ平均ERA×600) / 900
# ≈ 選手1/3 + リーグ平均2/3
- データ1年のみの選手 → 予測値の約2/3がリーグ平均に引き寄せられる
- データ2年のみの選手 → 予測値の約半分がリーグ平均に引き寄せられる
「前年Aチームに在籍、今季Bチームに移籍した2年目外国人投手」のように、「NPBには1年いるけどデータは少ない」という選手の予測値が平均寄りに出ても、それは能力の限界ではなくデータ不足による補正です。
この問題を可視化するため、ダッシュボードの選手名横に「NPB1年」「NPB2年」バッジを自動表示するようにしました(対象: 打者125名・投手180名)。バッジがついている選手の予測値は参考程度に留め、前年成績や他の情報と合わせて判断してください。
✅ 実装済み: 選手名の表記揺れ正規化
NPBのデータでは、同一選手が年度によって異なる漢字表記で記録される場合があります。たとえば 宮﨑 敏郎(﨑=U+FA11 の互換漢字)と 宮崎 敏郎(崎=U+5D0E)が混在し、そのまま処理すると別の選手として扱われてしまう問題がありました。
wRC+の複数年推移グラフで「一部の選手だけグラフが表示されない」原因がこれでした。
データパイプライン(sabermetrics.py)の保存前に正規化を適用しています:
VARIANT_MAP = str.maketrans("﨑髙濵澤邊齋齊國島嶋櫻", "崎高浜沢辺斎斉国島島桜")
def normalize_player_name(name: str) -> str:
return str(name).replace("\u3000", " ").strip().lstrip("*").strip().translate(VARIANT_MAP)
- CJK互換漢字 → 標準字(
﨑→崎、髙→高など)に変換 - データソース由来の先頭
*を除去 - 全角スペース → 半角スペースに統一
この正規化により、宮崎敏郎選手のwRC+推移が1年分→11年分として正しく表示されるようになりました。
残る課題
| アプローチ | 内容 | 難易度 |
|---|---|---|
| 過去実績平均 | 歴代NPB外国人選手の初年度成績を集計して割り当て | ★★☆ |
| 出身リーグ変換係数 | MLB・KBO → NPB の換算係数を適用 | ★★★ |
| ドラフト順位別平均 | 1位・2位以下で初年度期待値を変える | ★★☆ |
計算外選手が多いチームほど予測の不確実性が高く、「モデルの下位予測でも、記録のない選手たちの活躍次第で順位は十分に変わりうる」という情報自体が重要な示唆です。
Marcel法のもう一つの限界: 若手の急成長
Marcel法の年齢調整は +0.3%/年(27歳ピーク基準) と非常に小さく、急激な成長には追いつきません。
23〜26歳の選手が「殻を破る」ような場合、モデルは過去3年の平均に引き戻すため、実際の成績を大幅に下回る予測になります。新外国人・新人と同様に、若手ブレイク候補が多いチームほどモデルの予測は保守的に出る傾向があります。この限界は信頼区間(オレンジの予測幅)には現在反映されていないため、実際の順位との差がさらに広がる可能性もあります。
まとめ
| 項目 | 内容 |
|---|---|
| データ | baseball-data.com + npb.jp(2015-2025、5種類) |
| Marcel法精度(2025) | 打者OPS MAE=.063 / 投手ERA MAE=0.78 |
| ML精度(2025) | 打者OPS MAE=.063 / 投手ERA MAE=0.92 |
| ピタゴラス勝率 | NPB最適 k=1.72、MAE=3.20勝 |
| API | FastAPI 8エンドポイント、Docker対応 |
| ダッシュボード | Streamlit 7ページ、Plotlyチャート、日英対応 |
投手ERA予測ではMarcel法がMLを上回り、打者OPS予測では両者がほぼ同等でした。「最新のAIが必ずしも勝つわけではない」という事実をNPBデータで確認できたのが一番の収穫でした。
オースティン選手のように2024年は誤差.165の大外れ、翌2025年は誤差.008の的中と、同じ選手でもデータ次第で精度が大きく変わります。筒香選手の復活のように予測が追いつかないケースも含め、予測モデルの限界を理解した上で活用することが大切だと感じています。
→ GitHub: https://github.com/yasumorishima/npb-prediction
データ出典: プロ野球データFreak / NPB公式サイト