1
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

プロ野球をPythonで分析する

Last updated at Posted at 2025-03-14

書籍「野球データでやさしく学べるPython入門 いきなり「グラフ作成」「顧客分析」ができる 」を読了し、久々にプログラミング熱が出てきたのでPythonでNPBの分析をすることにしました。
https://amzn.asia/d/hXLyxw6

以下の記事を参考にさせていただきながらコードを書きました。
https://qiita.com/wooooo/items/96982736e67e99a36254

コード生成AIで使いやすいものがないか探していたところ、windsurfというものが使い勝手が良かったのでこれを導入
https://codeium.com/windsurf

スクレイピングは割愛

出力したカラム名からスペースを削除し、AIコード生成に落とし込みやすいようにする

wRC+という指標を以上のデータから生成する


import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib

# チーム名の定義
team_names = {
    't': '阪神',
    'c': '広島',
    'db': 'DeNA',
    'g': '巨人',
    's': 'ヤクルト',
    'd': '中日',
    'b': 'オリックス',
    'm': 'ロッテ',
    'h': 'ソフトバンク',
    'f': '日本ハム',
    'e': '楽天',
    'l': '西武'
}

# データの読み込み
df = pd.read_csv('preseason_data2.csv')

# 打席数で絞り込み(意味のあるサンプルサイズにするため)
min_pa = 10
df_filtered = df[df['打席'] >= min_pa].copy()

# wOBAの計算
df_filtered['wOBA'] = (
    0.692 * (df_filtered['四球'] - df_filtered['故意四']) +
    0.73 * df_filtered['死球'] +
    0.865 * df_filtered['安打'] +
    1.334 * df_filtered['二塁打'] +
    1.725 * df_filtered['三塁打'] +
    2.065 * df_filtered['本塁打']
) / (df_filtered['打数'] + df_filtered['四球'] - df_filtered['故意四'] + df_filtered['死球'] + df_filtered['犠飛'])

# リーグ平均wOBAの計算
league_woba = df_filtered['wOBA'].mean()

# リーグ総得点とリーグ総打席の計算
league_runs = df_filtered['得点'].sum()
league_pa = df_filtered['打席'].sum()

# wRC+の計算
df_filtered['wRC+'] = ((df_filtered['wOBA'] - league_woba) / 1.24 / (league_runs / league_pa) + 1) * 100

# 選手wRC+ランキング
df_sorted = df_filtered.sort_values('wRC+', ascending=False)
top_30 = df_sorted[['選手', '球団', 'wRC+']].head(30).copy()
top_30['wRC+'] = top_30['wRC+'].round(1)
top_30['球団名'] = top_30['球団'].map(team_names)

# 球団別wRC+の計算
team_stats = df_filtered.groupby('球団').agg({
    '打席': 'sum',
    'wOBA': lambda x: np.average(x, weights=df_filtered.loc[x.index, '打席'])
}).reset_index()

team_stats['wRC+'] = ((team_stats['wOBA'] - league_woba) / 1.24 / (league_runs / league_pa) + 1) * 100
team_sorted = team_stats.sort_values('wRC+', ascending=False)
team_sorted['wRC+'] = team_sorted['wRC+'].round(1)
team_sorted['打席'] = team_sorted['打席'].astype(int)
team_sorted['球団名'] = team_sorted['球団'].map(team_names)

# 選手ランキングのグラフ作成
plt.figure(figsize=(15, 8))
bars = plt.bar(range(len(top_30)), top_30['wRC+'], color='skyblue')
plt.xticks(range(len(top_30)), [f"{row['選手']}\n({row['球団名']})" for _, row in top_30.iterrows()], rotation=45, ha='right')
plt.ylabel('wRC+')
plt.title('選手別wRC+ランキング Top30 (最小打席数: {})'.format(min_pa))
plt.grid(axis='y', linestyle='--', alpha=0.7)

# 値のラベルを追加
for bar in bars:
    height = bar.get_height()
    plt.text(bar.get_x() + bar.get_width()/2, height,
             f'{height:.1f}', ha='center', va='bottom')

plt.axhline(y=100, color='r', linestyle='--', alpha=0.3)  # リーグ平均の線
plt.tight_layout()
plt.savefig('player_wrc_plus_ranking.png', dpi=300, bbox_inches='tight')

# 球団ランキングのグラフ作成
plt.figure(figsize=(12, 6))
bars = plt.bar(range(len(team_sorted)), team_sorted['wRC+'], color='lightgreen')
plt.xticks(range(len(team_sorted)), team_sorted['球団名'], rotation=45, ha='right')
plt.ylabel('wRC+')
plt.title('球団別平均wRC+')
plt.grid(axis='y', linestyle='--', alpha=0.7)

# 打席数と値のラベルを追加
for i, bar in enumerate(bars):
    height = bar.get_height()
    plt.text(bar.get_x() + bar.get_width()/2, height,
             f'{height:.1f}\n({team_sorted.iloc[i]["打席"]}打席)',
             ha='center', va='bottom')

plt.axhline(y=100, color='r', linestyle='--', alpha=0.3)  # リーグ平均の線
plt.tight_layout()
plt.savefig('team_wrc_plus_ranking.png', dpi=300, bbox_inches='tight')

FIPも算出する

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib

# CSVファイルを読み込む
df = pd.read_csv('preseason_data.csv')

# 投球回を小数点に変換(例: 2.1 -> 2.33)
def convert_ip(ip_str):
    if isinstance(ip_str, str) and '.' in ip_str:
        main, fraction = ip_str.split('.')
        return float(main) + float(fraction) / 3
    return float(ip_str)

df['投球回'] = df['投球回'].apply(convert_ip)

# チーム名の変換辞書
team_names = {
    't': '阪神',
    'c': '広島',
    'db': 'DeNA',
    'g': '巨人',
    's': 'ヤクルト',
    'd': '中日',
    'b': 'オリックス',
    'm': 'ロッテ',
    'h': 'ソフトバンク',
    'e': '楽天',
    'f': '日本ハム'
}

# チーム名を変換
df['チーム'] = df['球団'].map(team_names)

# チームごとの集計
team_stats = df.groupby('チーム').agg({
    '本塁打': 'sum',
    '四球': 'sum',
    '故意四': 'sum',
    '死球': 'sum',
    '三振': 'sum',
    '投球回': 'sum',
    '失点': 'sum'
}).reset_index()

# リーグ全体の定数を計算
total_hr = team_stats['本塁打'].sum()
total_bb = team_stats['四球'].sum()
total_ibb = team_stats['故意四'].sum()
total_hbp = team_stats['死球'].sum()
total_k = team_stats['三振'].sum()
total_ip = team_stats['投球回'].sum()
total_r = team_stats['失点'].sum()

# リーグ全体の失点率を計算(9イニングあたり)
league_ra = (total_r * 9) / total_ip

# リーグ全体のFIP要素を計算(定数なし)
league_fip_components = (13 * total_hr + 3 * (total_bb - total_ibb + total_hbp) - 2 * total_k) / total_ip

# リーグ定数を計算
league_constant = league_ra - league_fip_components

print("\nリーグ全体の統計:")
print(f"総投球回: {total_ip:.1f}")
print(f"総失点: {total_r}")
print(f"失点率(RA9): {league_ra:.3f}")
print(f"FIP要素: {league_fip_components:.3f}")
print(f"リーグ定数: {league_constant:.3f}")

# チームごとのFIPを計算
def calculate_fip(row):
    ip = row['投球回']
    hr = row['本塁打']
    bb = row['四球']
    ibb = row['故意四']
    hbp = row['死球']
    k = row['三振']
    
    fip_components = (13 * hr + 3 * (bb - ibb + hbp) - 2 * k) / ip
    return fip_components + league_constant

team_stats['FIP'] = team_stats.apply(calculate_fip, axis=1)
team_stats = team_stats.sort_values('FIP')

print("\n各チームのFIP:")
for _, row in team_stats.iterrows():
    print(f"{row['チーム']}: {row['FIP']:.3f}")

# 表の作成
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
table_data = team_stats[['チーム', 'FIP']].values
col_labels = ['チーム', 'FIP']
table = plt.table(cellText=[[f'{row[0]}', f'{row[1]:.3f}'] for row in table_data],
                 colLabels=col_labels,
                 loc='center',
                 cellLoc='center')
table.auto_set_font_size(False)
table.set_fontsize(9)
table.scale(1.2, 1.5)
plt.axis('off')

# 棒グラフの作成
plt.subplot(1, 2, 2)
bars = plt.bar(team_stats['チーム'], team_stats['FIP'])
plt.title('球団別FIP')
plt.xticks(rotation=45)
plt.ylabel('FIP')

# 各バーの上に数値を表示
for bar in bars:
    height = bar.get_height()
    plt.text(bar.get_x() + bar.get_width()/2., height,
             f'{height:.3f}',
             ha='center', va='bottom')

plt.tight_layout()
plt.savefig('team_fip.png', dpi=300, bbox_inches='tight')

以上で生成されたのがこれです
team_wrc_plus_ranking.png
team_fip.png
久しぶりにコード触って自分がエンジニアであることを実感しましたありがとうございました。

1
4
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
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?