書籍「野球データでやさしく学べる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')