見出し画像

プロ野球選手の能力を評価するプログラム構築手順書

割引あり

前書き(長いw)

セイバーメトリクスなど野球選手の定量的、統計的な評価の仕方ができてだいぶ経ちます。
数値で評価できることは比較がしやすく、感覚的になんとなくの評価が少なくなり、選手の能力評価に良い側面があります。
もちろん感覚的な部分の評価も重要でデータでは測れない部分の能力をもつけることも重要です。
どちらかではなく、バランスです。

定量的に評価する部分では、セイバーメトリクスでは様々な割合だったり、数値が出てきます。
ぱっと見ても分かりにくい指標があったりします。
簡単なOPSでも、長打率と出塁率を足し合わせて0.7ぐらいは良いと判断するのかは目安の評価を見てみないとわからないものです。
そこで、セイバーメトリクスの評価をレーダーチャートで出してくれるプログラムを作成しました。


評価レーダーチャート

作成するコードは無料で公開しています。
評価の閾値はセイバーメトリクスの評価を参考にしていますが、独自で閾値入りたい場合は関数の閾値を変えればできますので、自分用にコードをカスタマイズしても大丈夫です。

データの取得はスクレイピングでプロ野球公式の選手一覧から取りますので、1軍に出たことのあるプロ野球選手であれば、プログラムを実行してデータを取得、評価することが可能になります。

このプログラム自体は1回の事項でURLへのアクセスは1回になりますので、意図しない過剰なサイトへのアクセスの危険性はほとんどありませんが、プログラムの連続実行には、サイトへの過負荷をかけないようお気をつけください。
例えば、複数選手をプログラムを連続で実行するように変更して使用する場合などです。

データの取得元はプロ野球公式の選手成績情報になります。

前書きが長くなりましたが、プログラムを説明していきましょう。

バージョン・ライブラリ情報

Python情報、ライブラリは以下になります。

Python 3.9.17
seaborn                   0.12.2 
pandas                    2.1.0
japanize-matplotlib       1.1.3
numpy                     1.24.4
matplotlib                3.7.2

コード解説

コードは関数やブロックに分けて解説します。

ライブラリインポート

使用するライブラリのインポート部分です。
最低限使用するライブラリに留めています。

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import warnings
import numpy as np
import seaborn as sns
import japanize_matplotlib
warnings.simplefilter("ignore")

warningsライブラリはコート実行時の警告文の取り扱いを操作します。
結果出力時の警告文が邪魔になるので、非表示になるようにします。
ignoreに設定すると警告文が表示されなくなります。
開発時などではデータ処理やコピーをする際に出た時に確認すべきですが結果表示時に警告文だらけで邪魔になるので非表示にします。

warnings.simplefilter("ignore")

warningsの詳細については公式を参照することが早いです。

main処理

メイン処理自体は投手・野手の評価プログラムを呼び出す用に2行あるのみです。
引数として評価したい選手の成績が記載されているURLと評価したい成績の年度を指定します。

if __name__ == "__main__":
    pitcher_eval('https://npb.jp/bis/players/61365139.html',2023)
#     batter_eval('https://npb.jp/bis/players/71075138.html',2023)

基本的な処理の構造は投手も野手も同じです。
データを取得→データ加工→セイバーメトリクスの計算→レベル評価→可視化・データ保存
の処理フローです。
保存場所を同じにしてあるので、投手・野手を同時に評価する処理にする場合は保存場所を変える必要があります。

投手の評価プログラムはpitcher_eval関数にまとめています。

def pitcher_eval(url,year):
    data = pd.read_html(url)
    df = data[1]
    df.dropna(inplace=True)
    df['投球回'] = df['投球回'].str.replace('  ','').astype(float)
    df['年度'] = df['年度'].astype(int)
    df_t = df[df['年度']==year]
    df_t2 = SabermetricsPitcher(df_t)
    df_t3 = SabermetricsPitcher(df)
    df_rank = pd.DataFrame()
    df_t = df_t2[['FIP','BB/9','K/9','BABIP','LOB','HR/9']]
    print(df_t3)
    print(df_t.T.round(3))
    df_rank = pitcher_rank(df_t)    
    labels = df_rank.columns.tolist()
    values = df_rank.to_numpy().tolist()
    rader_chart(labels, values[0]) 
    df_rank.to_csv('data/rank_data.csv',index=False)
    df_t.to_csv('data/player_data.csv',index=False)
    df_t3.to_csv('data/player_data_ttl.csv')

野手はbatter_eval関数にまとめています。

def batter_eval(url,year):
    #野手 
    data = pd.read_html(url)
    df = data[1]
    df.fillna(0,inplace=True)
    df_t = df[df['年度']==year]
    df_t2 = SabermetricsBatter(df_t)
    df_t3 = SabermetricsBatter(df)
    df_rank = pd.DataFrame()
    df_t = df_t2[['OPS','PA/K','IsoP','IsoD','Spd','BABIP']]
    print(df_t3)
    print(df_t.T.round(3))
    df_rank = batter_rank(df_t)
    labels = df_rank.columns.tolist()
    values = df_rank.to_numpy().tolist()
    rader_chart(labels, values[0]) 
    df_rank.to_csv('data/rank_data.csv',index=False)
    df_t.to_csv('data/player_data.csv',index=False)
    df_t3.to_csv('data/player_data_ttl.csv')

上記の処理を順を追って説明をしていきます。

まずはサイトからデータを取得するための処理です。
記載してあるURLはプロ野球選手別成績情報のページです。
選手ごとページが分かれていますので、サイトで評価したい選手の成績情報が載っているページURLを指定します。

data = pd.read_html('https://npb.jp/bis/players/11515157.html')

URL内の表形式で表現されている部分を取得してきます。
表が複数URL内にある場合はリスト内に追加されていきます。
プロ野球公式の選手別成績ページはリスト2番目に格納されますので、data[1]に必ず成績データが入っています。
リストない要素を代入させるとデータフレーム形式で格納されます。

    df = data[1]

欠損値は記録がない成績と通算の年度は空白ですので0で埋めます。

df.fillna(0,inplace=True)

なん年の成績をとってくるか指定しています。
前の行の処理で通算成績の年度は0に置換しているので、通算成績を評価したい場合はyear引数に0を指定するとよいです。
その選手の評価したい年度をyearに指定すると良いでしょう。

df_t = df[df['年度']==year]

df_t2には評価するための成績データが1行のみセイバーメトリクスを計算してデータフレームに列が追加されます。
df_t3には全ての成績データに対してセイバーメトリクスを計算して列を追加されます。
関数の中身については、セイバーメトリクス算出関数の章で説明します。

df_t2 = SabermetricsBatter(df_t)
df_t3 = SabermetricsBatter(df)

評価ランク用のデータフレームを用意します。

df_rank = pd.DataFrame()

評価に使うセイバメトリクスをdf_tに格納します。

df_t = df_t2[['OPS','PA/K','IsoP','IsoD','Spd','BABIP']]

セイバーメトリクスと成績データを出力します。

print(df_t3)
print(df_t.T.round(3))

出力は下記のようになります。

df_t3の出力。

print(df_t3)

結果
      年度  所属球団    登板    勝利    敗北  セーブ     H    HP   完投  完封勝  ...   自責点   防御率  \
0   2014  広島東洋  26.0  10.0   8.0  0.0   0.0   0.0  3.0  1.0  ...  68.0  4.05   
2   2015  広島東洋  51.0   3.0   8.0  2.0  20.0  22.0  2.0  0.0  ...  38.0  3.13   
4   2016  広島東洋  17.0   3.0   1.0  0.0   4.0   7.0  0.0  0.0  ...   8.0  3.32   
6   2017  広島東洋  24.0  10.0   2.0  0.0   0.0   0.0  0.0  0.0  ...  59.0  3.65   
8   2018  広島東洋  27.0  15.0   7.0  0.0   0.0   0.0  2.0  0.0  ...  53.0  2.62   
10  2019  広島東洋  26.0  11.0   9.0  0.0   0.0   0.0  6.0  2.0  ...  68.0  3.53   
12  2020  広島東洋  11.0   5.0   4.0  0.0   0.0   0.0  2.0  0.0  ...  31.0  4.41   
14  2021  広島東洋  23.0  10.0   5.0  0.0   0.0   0.0  1.0  1.0  ...  50.0  3.07   
16  2022  広島東洋  23.0   8.0   9.0  0.0   0.0   0.0  3.0  2.0  ...  71.0  4.72   
18  2023  広島東洋  23.0   6.0  11.0  0.0   0.0   0.0  0.0  0.0  ...  52.0  3.61   

         FIP       LOB      BB/9      HR/9      WHIP      K/BB        K/9  \
0   4.159735  0.711111  2.384106  1.192053  1.357616  2.900000   6.913907   
2   2.817525  0.661765  2.557287  0.412466  1.301558  3.129032   8.001833   
4   3.072830  0.729167  2.547170  0.849057  0.990566  4.000000  10.188679   
6   3.602094  0.699177  2.665289  0.743802  1.280992  2.534884   6.756198   
8   3.685934  0.782443  2.027473  1.087912  1.010989  3.878049   7.862637   
10  3.876788  0.776330  1.819757  1.143847  1.218949  3.885714   7.071057   
12  3.864849  0.678851  1.996830  0.855784  1.331220  2.714286   5.419968   
14  3.530397  0.740979  1.908345  0.738714  1.142271  3.290323   6.279070   
16  4.060044  0.700876  1.798668  1.199112  1.354552  3.592593   6.461880   
18  3.893994  0.759398  2.298762  1.044892  1.160991  3.121212   7.174923   

       BABIP  
0   0.283203  
2   0.284182  
4   0.213115  
6   0.264113  
8   0.221612  
10  0.277978  
12  0.280702  
14  0.255144  
16  0.294872  
18  0.242280  

df_t.Tの出力。
round(3)の部分は小数点4位を四捨五入して小数点3位まで出力するように指定しています。
小数点第2位までがよいならround(2)とすればよく、欲しい小数点第何位かによってround内の数値を変えるよ良いです。

print(df_t.T.round(3))

結果
          18
FIP    3.894
BB/9   2.299
K/9    7.175
BABIP  0.242
LOB    0.759
HR/9   1.045

ランクづけを行う関数batter_rank(投手の場合はpitcher_rank)です。
df_rankにランク評価の結果を格納します。

df_rank = batter_rank(df_t)

レーダーチャートを作成するまでの処理です。
レーダーチャート関数に関してはレーダーチャートの章で説明します。
labelsにカラム名、名称をリスト形式で格納します。
valuesにはランクのデータを格納します。
rader_chart関数でレーダーチャートを作成します。

labels = df_rank.columns.tolist()
values = df_rank.to_numpy().tolist()
rader_chart(labels, values[0]) 

各データをcsv出力します。
ファイル名は適宜変えてもらえれば良いでしょう。

df_rank.to_csv('data/rank_data.csv',index=False)
df_t.to_csv('data/player_data.csv',index=False)
df_t3.to_csv('data/player_data_ttl.csv')

投手も同様の処理です。

セイバーメトリクス算出関数(投手)

セイバーメトリクスに関する説明は下記記事を参照してください。
各処理には例外処理を入れており、もし成績データが抜けていたりした場合や0で割ることになってしまった場合にエラーで処理が止まらないようにしています。

def SabermetricsPitcher(df):
    """
    ピッチャーのセイバーメトリックス指標を計算して、データ加工する
    FIP:野手による影響を受けない結果(被本塁打、三振、四死球など)のみで投手の能力を評価した指標
    QS%:先発登板数に対するQSの割合
    LOB:ランナーを出した状態で失点しなかった指標
    
    """
    try:
        df['FIP'] = (13.0*df['本塁打']+(df['四球']+df['死球'])*3.0-df['三振']*2.0)/df['投球回']+3.12
    except KeyError:
        print('key error exception: can not make FIP')
    try:
        df['LOB'] = (df['安打']+df['四球']+df['死球']-df['失点'])/(df['安打']+df['四球']+df['死球']-1.4*df['本塁打'])
    except KeyError:
        print('key error exception: can not make LOB')
    try:
        df['BB/9']= df['四球']*9/df['投球回']
    except KeyError:
        print('key error exception: can not make BB/9')
    try:
        df['HR/9']= df['本塁打']*9/df['投球回']
    except KeyError:
        print('key error exception: can not make HR/9')
    try:
        df['QS%'] = df['QS']/df['先発']    
    except KeyError:
        print('key error exception: can not make QS%')
    try:
        #(与四球 + 被安打) ÷ 投球回
        df['WHIP']=(df['四球']+df['安打'])/df['投球回']
    except KeyError:
        print('key error exception: can not make WHIP')
    try:
        df['K/BB']=df['三振']/df['四球']
    except KeyError:
        print('key error exception: can not make K/BB')
    try:
        df['K/9'] = df['三振']*9/df['投球回']
    except KeyError:
        print('key error exception: can not make K/9')        
    try:
        # (安打 - 本塁打) ÷ (打数 - 奪三振 - 本塁打 + 犠飛)
        df['BABIP']=(df['安打']-df['本塁打'])/(df['打者']-df['三振']-df['本塁打'])
    except KeyError:
        print('key error exception: can not make BABIP')

    except ZeroDivisionError:
        print('0除算がありました。')
    
    finally:
        print('計算終了')
        
    return df

セイバーメトリクス算出関数(野手)

野手のセイバーメトリクスに関する説明は下記記事を参照してください。

野手も投手と同様の処理です。
野手成績のセイバーメトリクスが算出されデータフレームの列に追加されていきます。
簡易RFの守備指標のセイバーメトリクスの計算も含まれていますが、プロ野球成績のページには守備に関する成績が記録されていないので、基本的に算出はできず例外処理になります。
簡易RFを算出するためにはデータを別から取得してくる必要があります。

def SabermetricsBatter(df):
    """
    バッターのセイバーメトリックス指標を計算して、データ加工する
    OPS:得点力
    IsoP:パワー指標
    IsoD:選球眼
    BB/K:選球眼
    BIBIP:運の要素を取り出す指標
    A:出塁能力
    B:進塁能力
    C:出塁機会
    RC:得点力
    RC27:この選手が9人いたとして、1試合の得点能力に換算した式
    AB/HR:1本塁打打つまでにかかる打数
    PA/K:三振するまでにかかる打数
    PS:機動力とパワーを兼ね添えてるかを見る
    Spd:走塁能力
    簡易RF:レンジファクター、守備寄与率
    """
    try:
        df['OPS'] = df['長打率']+df['出塁率']  #得点力 
    except KeyError:
        print('KeyError: can not make OPS')
    try:
        df['IsoP'] = df['長打率']-df['打率'] #パワー 
    except KeyError:
        print('KeyError: can not make IsoP')
    try:
        df['IsoD'] = df['出塁率']-df['打率'] #選球眼 
    except KeyError:
        print('KeyError: can not make IsoD')
    try:
        df['BB/K'] = df['四球']/df['三振'] #選球眼 
    except KeyError:
        print('KeyError: can not make BB/K')
    try:
        df['BABIP'] = (df['安打']-df['本塁打'])/(df['打数']-df['三振']-df['本塁打']+df['犠飛'])
    except KeyError:
        print('KeyError: can not make BABIP')
    try:
        A = df['安打']+df['四球']+df['死球']-df['盗塁刺']-df['併殺打'] #出塁能力 
        B = df['塁打']+0.26*(df['四球']+df['死球'])+0.53*(df['犠飛']+df['犠打'])+0.64*df['盗塁']-0.03*df['三振']#進塁能力
        C = df['打数']+df['四球']+df['死球']+df['犠飛']+df['犠打']#出塁機会
        df['RC']=(A+2.4*C)*(B+3*C)/(9*C)-0.9*C
        TO = df['打数']-df['安打']+df['犠打']+df['犠飛']+df['盗塁刺']+df['併殺打']
        df['RC27'] = 27*df['RC']/TO #この選手が9人いたとして 、1試合の得点能力に換算した式
    except KeyError:
        print('KeyError: can not make RC27')
    try:
        df['AB/HR'] = df['打数']/df['本塁打']#1本塁打打つまでにかかる打数
    except KeyError:
        print('KeyError: can not make AB/HR')
    try:
        df['PA/K'] = df['打数']/df['三振']#三振するまでにかかる打数
    except KeyError:
        print('KeyError: can not make PA/K')
    try:
        df['PS'] = (df['本塁打']*2*df['盗塁'])/(df['本塁打']+df['盗塁'])#機動力とパワーを兼ね添えてるかを見る     
    except KeyError:
        print('KeyError: can not make PS')
    
    try:
        steel_ratio=((df['盗塁']+3)/(df['盗塁']+df['盗塁刺']+7)-0.4)*20
        try_steel=np.sqrt((df['盗塁']+df['盗塁刺'])/(df['安打']+df['四球']+df['死球']))/0.07
        three_base_ratio=df['三塁打']/(df['打数']-df['本塁打']-df['三振'])/0.02*10
        run_ratio=((df['得点']-df['本塁打'])/(df['安打']+df['四球']+df['死球']-df['本塁打'])-0.1)/0.04
        df['Spd']=(steel_ratio+try_steel+three_base_ratio+run_ratio)/4.0
        
    except KeyError:
        print('KeyError: can not make Spd')
        
    try:
        df['簡易RF']=(df['刺殺']+df['補殺'])/df['試合']
    except KeyError:
        print('KeyError: can not make RF')
        
    try:
        df['BB%']=df['四球']/df['打席']
    except KeyError:
        print('KeyError: can not make BB%')
    
    except ZeroDivisionError:
        print('0除算がありました。')
    
    finally:
        print('計算終了')

    return df

セイバーメトリクス評価関数

セイバーメトリクスの値をランクづけするための評価関数です。
評価関数といいますが単純な数値に対する条件分岐です。
ランクづけはセイバーメトリクスの評価を参考にしていますが、独自で変えたいこともあるでしょう。
その時はこの関数の条件を変更すれば、ランクの範囲を変えられます。
ランクは0から6の7段階で評価するようにしています。

例えばOPSの数値に対する評価はWikipediaに載っています。
各セイバーメトリクスの数値に対する評価を見ながら、条件分岐の数値を変更すると良いでしょう。

def OPS_rank(ops):
    if ops > 0.8999:
        return 6.0
    elif ops > 0.8333:
        return 5.0
    elif ops > 0.7666:
        return 4.0
    elif ops > 0.6999:
        return 3.0
    elif ops > 0.6333:
        return 2.0
    elif ops > 0.5666:
        return 1.0
    else:
        return 0.0
    
def IsoP_rank(isop):
    if isop >= 0.28:
        return 6.0
    elif isop >= 0.26:
        return 5.0
    elif isop >= 0.24:
        return 4.0
    elif isop >= 0.22:
        return 3.0
    elif isop >= 0.20:
        return 2.0
    elif isop >= 0.18:
        return 1.0
    else:
        return 0.0

def IsoD_rank(isod):
    if isod >= 0.10:
        return 6.0
    elif isod >= 0.09:
        return 5.0
    elif isod >= 0.08:
        return 4.0
    elif isod >= 0.07:
        return 3.0
    elif isod >= 0.06:
        return 2.0
    elif isod >= 0.05:
        return 1.0
    else:
        return 0.0

def Spd_rank(spd):
    if spd >= 8.0:
        return 6.0
    elif spd >= 7.0:
        return 5.0
    elif spd >= 6.0:
        return 4.0
    elif spd >= 5.0:
        return 3.0
    elif spd >= 4.0:
        return 2.0
    elif spd >= 3.0:
        return 1.0
    else:
        return 0.0

def BABIP_rank(babip):
    if babip >= 0.330:
        return 0.0
    elif babip >= 0.320:
        return 1.0
    elif babip >= 0.310:
        return 2.0
    elif babip >= 0.300:
        return 3.0
    elif babip >= 0.290:
        return 4.0
    elif babip >= 0.280:
        return 5.0
    else:
        return 6.0
    
def PA_K_rank(pa_k):
    if pa_k >= 9.0:
        return 6.0
    elif pa_k >= 8.0:
        return 5.0
    elif pa_k >= 7.0:
        return 4.0
    elif pa_k >= 6.0:
        return 3.0
    elif pa_k >= 5.0:
        return 2.0
    elif pa_k >= 4.0:
        return 1.0
    else:
        return 0.0
    
def fip_rank(fip):
    if fip <= 2.0:
        return 6.0
    elif fip <= 2.5:
        return 5.0
    elif fip <= 3.0:
        return 4.0
    elif fip <= 3.5:
        return 3.0
    elif fip <= 4.0:
        return 2.0
    elif fip <= 4.5:
        return 1.0
    else:
        return 0

def ctl_rank(bb9):
    if bb9 <= 1.5:
        return 6.0
    elif bb9 <= 2.0:
        return 5.0
    elif bb9 <= 2.5:
        return 4.0
    elif bb9 <= 3.0:
        return 3.0
    elif bb9 <= 3.5:
        return 2.0
    elif bb9 <= 4.0:
        return 1.0
    else:
        return 0.0
    
def qs_rank(qs):
    if qs >= 0.7:
        return 6.0
    elif qs >= 0.6:
        return 5.0
    elif qs >= 0.5:
        return 4.0
    elif qs >= 0.4:
        return 3.0
    elif qs >= 0.3:
        return 2.0
    elif qs >= 0.2:
        return 1.0
    else:
        return 0.0

def HR_rank(hr9):
    if hr9 <= 0.5:
        return 6.0
    elif hr9 <= 0.75:
        return 5.0
    elif hr9 <= 1.00:
        return 4.0
    elif hr9 <= 1.25:
        return 3.0
    elif hr9 <=1.50:
        return 2.0
    elif hr9 <= 1.75:
        return 1.0
    else:
        return 0.0

def lob_rank(lob):
    if lob >= 0.775:
        return 6.0
    elif lob >= 0.750:
        return 5.0
    elif lob >= 0.725:
        return 4.0
    elif lob >= 0.700:
        return 3.0
    elif lob >= 0.675:
        return 2.0
    elif lob >= 0.650:
        return 1.0
    
def kbb_rank(kbb):
    if kbb >= 8.0:
        return 6.0
    elif kbb >= 7.0:
        return 5.0
    elif kbb >= 6.0:
        return 4.0
    elif kbb >= 5.0:
        return 3.0
    elif kbb >= 4.0:
        return 2.0
    elif kbb >= 3.0:
        return 1.0
    else:
        return 0.0
    
def k_rank(k):
    if k >= 9.0:
        return 6.0
    elif k >= 8.0:
        return 5.0
    elif k >= 7.0:
        return 4.0
    elif k >= 6.0:
        return 3.0
    elif k >= 5.0:
        return 2.0
    elif k >= 4.0:
        return 1.0
    else:
        return 0.0    

ランクデータ格納関数

投手のレーダーチャート作成用のデータフレームを作成する関数です。

def pitcher_rank(df):
    
    rank = pd.DataFrame({'投手総合':[fip_rank(df['FIP'].values)],
                        'コントロール':[ctl_rank(df['BB/9'].values)],
                        '奪三振能力':[k_rank(df['K/9'].values)],
                          '運':[BABIP_rank(df['BABIP'].values)],
                        '対ランナー':[lob_rank(df['LOB'].values)],
                        '本塁打回避':[HR_rank(df['HR/9'].values)],
                        })
    
    return rank     

野手のレーダーチャート作成用のデータフレームを作成する関数です。

def batter_rank(df):
    
    rank = pd.DataFrame({'得点力':[OPS_rank(df['OPS'].values)],
                        'ミート':[PA_K_rank(df['PA/K'].values)],
                        'スピード':[Spd_rank(df['Spd'].values)],
                          '運':[BABIP_rank(df['BABIP'].values)],
                        '選球眼':[IsoD_rank(df['IsoD'].values)],
                        'パワー':[IsoP_rank(df['IsoP'].values)],
                        })

    return rank

レーダーチャート作成関数

レーダーチャートを作成する関数です。
項目表示用のlabelsとランクの数値データをvaluesを引数にレーダーチャートを出力する関数です。
また、作成したレーダーチャートはこのプログラムの置いてあるディレクトリのdataディレクトリ内に保存します。
dataディレクトリを作成していない場合は下記コードを使用するとエラーになるので、保存場所を変えるかdataディレクトリを変えましょう。
各処理に関しては関数内にコメント文で記載しています。

def rader_chart(labels, values):
    """
    セイバーメトリックス指標から能力レーダーチャートをプロットする関数。
    
    """
    # 多角形を閉じるためにデータの最後に最初の値を追加する。
    radar_values = np.concatenate([values, [values[0]]])
    # プロットする角度を生成する。
    angles = np.linspace(0, 2 * np.pi, len(labels)+1, endpoint=True)
    # メモリ軸の生成
    rgrids = [0,1,2,3,4,5,6]
    
    fig = plt.figure(facecolor="w")
    # 極座標でaxを作成。
    ax = fig.add_subplot(1, 1, 1, polar=True)
    # レーダーチャートの線を引く
    ax.plot(angles, radar_values)
    # レーダーチャートの内側を塗りつぶす
    ax.fill(angles, radar_values, alpha=0.2)
    # 項目ラベルの表示
    ax.set_thetagrids(angles[:-1] * 180 / np.pi, labels)
    # 円形の目盛線を消す
    ax.set_rgrids([])
    # 一番外側の円を消す
    ax.spines['polar'].set_visible(False)
    # 始点を上(北)に変更
    ax.set_theta_zero_location("N")
    # 時計回りに変更(デフォルトの逆回り)
    ax.set_theta_direction(-1)
    
    # 多角形の目盛線を引く
    for grid_value in rgrids:
        grid_values = [grid_value] * (len(labels)+1)
        ax.plot(angles, grid_values, color="gray",  linewidth=0.5)
        
    # メモリの値を表示する
    for t in rgrids:
        # xが偏角、yが絶対値でテキストの表示場所が指定される
        ax.text(x=0, y=t, s=t)

    # rの範囲を指定
    ax.set_rlim([min(rgrids), max(rgrids)])
    ax.set_title("選手能力値", pad=10)
    plt.savefig('data/rader_chart.png')
    plt.show()


全体コード

最後にコードの全てを記します。
下記コードをコピペすれば、保存先ディレクトリ等を調整すればすぐプログラムを利用できます。
プログラムの変更等、利用はご自由にお使いください。
コードの利用、改良などコードに関する責任は自己責任になりますのでご了承ください。

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import warnings
import numpy as np
import seaborn as sns
import japanize_matplotlib
warnings.simplefilter("ignore")

def SabermetricsBatter(df):
    """
    バッターのセイバーメトリックス指標を計算して、データ加工する
    OPS:得点力
    IsoP:パワー指標
    IsoD:選球眼
    BB/K:選球眼
    BIBIP:運の要素を取り出す指標
    A:出塁能力
    B:進塁能力
    C:出塁機会
    RC:得点力
    RC27:この選手が9人いたとして、1試合の得点能力に換算した式
    AB/HR:1本塁打打つまでにかかる打数
    PA/K:三振するまでにかかる打数
    PS:機動力とパワーを兼ね添えてるかを見る
    Spd:走塁能力
    簡易RF:レンジファクター、守備寄与率
    """
    try:
        df['OPS'] = df['長打率']+df['出塁率']  #得点力 
    except KeyError:
        print('KeyError: can not make OPS')
    try:
        df['IsoP'] = df['長打率']-df['打率'] #パワー 
    except KeyError:
        print('KeyError: can not make IsoP')
    try:
        df['IsoD'] = df['出塁率']-df['打率'] #選球眼 
    except KeyError:
        print('KeyError: can not make IsoD')
    try:
        df['BB/K'] = df['四球']/df['三振'] #選球眼 
    except KeyError:
        print('KeyError: can not make BB/K')
    try:
        df['BABIP'] = (df['安打']-df['本塁打'])/(df['打数']-df['三振']-df['本塁打']+df['犠飛'])
    except KeyError:
        print('KeyError: can not make BABIP')
    try:
        A = df['安打']+df['四球']+df['死球']-df['盗塁刺']-df['併殺打'] #出塁能力 
        B = df['塁打']+0.26*(df['四球']+df['死球'])+0.53*(df['犠飛']+df['犠打'])+0.64*df['盗塁']-0.03*df['三振']#進塁能力
        C = df['打数']+df['四球']+df['死球']+df['犠飛']+df['犠打']#出塁機会
        df['RC']=(A+2.4*C)*(B+3*C)/(9*C)-0.9*C
        TO = df['打数']-df['安打']+df['犠打']+df['犠飛']+df['盗塁刺']+df['併殺打']
        df['RC27'] = 27*df['RC']/TO #この選手が9人いたとして 、1試合の得点能力に換算した式
    except KeyError:
        print('KeyError: can not make RC27')
    try:
        df['AB/HR'] = df['打数']/df['本塁打']#1本塁打打つまでにかかる打数
    except KeyError:
        print('KeyError: can not make AB/HR')
    try:
        df['PA/K'] = df['打数']/df['三振']#三振するまでにかかる打数
    except KeyError:
        print('KeyError: can not make PA/K')
    try:
        df['PS'] = (df['本塁打']*2*df['盗塁'])/(df['本塁打']+df['盗塁'])#機動力とパワーを兼ね添えてるかを見る     
    except KeyError:
        print('KeyError: can not make PS')
    
    try:
        steel_ratio=((df['盗塁']+3)/(df['盗塁']+df['盗塁刺']+7)-0.4)*20
        try_steel=np.sqrt((df['盗塁']+df['盗塁刺'])/(df['安打']+df['四球']+df['死球']))/0.07
        three_base_ratio=df['三塁打']/(df['打数']-df['本塁打']-df['三振'])/0.02*10
        run_ratio=((df['得点']-df['本塁打'])/(df['安打']+df['四球']+df['死球']-df['本塁打'])-0.1)/0.04
        df['Spd']=(steel_ratio+try_steel+three_base_ratio+run_ratio)/4.0
        
    except KeyError:
        print('KeyError: can not make Spd')
        
    try:
        df['簡易RF']=(df['刺殺']+df['補殺'])/df['試合']
    except KeyError:
        print('KeyError: can not make RF')
        
    try:
        df['BB%']=df['四球']/df['打席']
    except KeyError:
        print('KeyError: can not make BB%')
    
    except ZeroDivisionError:
        print('0除算がありました。')
    
    finally:
        print('計算終了')

    return df

def SabermetricsPitcher(df):
    """
    ピッチャーのセイバーメトリックス指標を計算して、データ加工する
    FIP:野手による影響を受けない結果(被本塁打、三振、四死球など)のみで投手の能力を評価した指標
    QS%:先発登板数に対するQSの割合
    LOB:ランナーを出した状態で失点しなかった指標
    """
    try:
        df['FIP'] = (13.0*df['本塁打']+(df['四球']+df['死球'])*3.0-df['三振']*2.0)/df['投球回']+3.12
    except KeyError:
        print('key error exception: can not make FIP')
    try:
        df['LOB'] = (df['安打']+df['四球']+df['死球']-df['失点'])/(df['安打']+df['四球']+df['死球']-1.4*df['本塁打'])
    except KeyError:
        print('key error exception: can not make LOB')
    try:
        df['BB/9']= df['四球']*9/df['投球回']
    except KeyError:
        print('key error exception: can not make BB/9')
    try:
        df['HR/9']= df['本塁打']*9/df['投球回']
    except KeyError:
        print('key error exception: can not make HR/9')
    try:
        df['QS%'] = df['QS']/df['先発']    
    except KeyError:
        print('key error exception: can not make QS%')
    try:
        #(与四球 + 被安打) ÷ 投球回
        df['WHIP']=(df['四球']+df['安打'])/df['投球回']
    except KeyError:
        print('key error exception: can not make WHIP')
    try:
        df['K/BB']=df['三振']/df['四球']
    except KeyError:
        print('key error exception: can not make K/BB')
    try:
        df['K/9'] = df['三振']*9/df['投球回']
    except KeyError:
        print('key error exception: can not make K/9')        
    try:
        # (安打 - 本塁打) ÷ (打数 - 奪三振 - 本塁打 + 犠飛)
        df['BABIP']=(df['安打']-df['本塁打'])/(df['打者']-df['三振']-df['本塁打'])
    except KeyError:
        print('key error exception: can not make BABIP')

    except ZeroDivisionError:
        print('0除算がありました。')
    
    finally:
        print('計算終了')
        
    return df

def rader_chart(labels, values):
    """
    セイバーメトリックス指標から能力レーダーチャートをプロットする関数。
    
    """
    # 多角形を閉じるためにデータの最後に最初の値を追加する。
    radar_values = np.concatenate([values, [values[0]]])
    # プロットする角度を生成する。
    angles = np.linspace(0, 2 * np.pi, len(labels)+1, endpoint=True)
    # メモリ軸の生成
    rgrids = [0,1,2,3,4,5,6]
    
    fig = plt.figure(facecolor="w")
    # 極座標でaxを作成。
    ax = fig.add_subplot(1, 1, 1, polar=True)
    # レーダーチャートの線を引く
    ax.plot(angles, radar_values)
    # レーダーチャートの内側を塗りつぶす
    ax.fill(angles, radar_values, alpha=0.2)
    # 項目ラベルの表示
    ax.set_thetagrids(angles[:-1] * 180 / np.pi, labels)
    # 円形の目盛線を消す
    ax.set_rgrids([])
    # 一番外側の円を消す
    ax.spines['polar'].set_visible(False)
    # 始点を上(北)に変更
    ax.set_theta_zero_location("N")
    # 時計回りに変更(デフォルトの逆回り)
    ax.set_theta_direction(-1)
    
    # 多角形の目盛線を引く
    for grid_value in rgrids:
        grid_values = [grid_value] * (len(labels)+1)
        ax.plot(angles, grid_values, color="gray",  linewidth=0.5)
        
    # メモリの値を表示する
    for t in rgrids:
        # xが偏角、yが絶対値でテキストの表示場所が指定される
        ax.text(x=0, y=t, s=t)

    # rの範囲を指定
    ax.set_rlim([min(rgrids), max(rgrids)])
    ax.set_title("選手能力値", pad=10)
    plt.savefig('data/rader_chart.png')
    plt.show()
    
def OPS_rank(ops):
    if ops > 0.8999:
        return 6.0
    elif ops > 0.8333:
        return 5.0
    elif ops > 0.7666:
        return 4.0
    elif ops > 0.6999:
        return 3.0
    elif ops > 0.6333:
        return 2.0
    elif ops > 0.5666:
        return 1.0
    else:
        return 0.0
    
def IsoP_rank(isop):
    if isop >= 0.28:
        return 6.0
    elif isop >= 0.26:
        return 5.0
    elif isop >= 0.24:
        return 4.0
    elif isop >= 0.22:
        return 3.0
    elif isop >= 0.20:
        return 2.0
    elif isop >= 0.18:
        return 1.0
    else:
        return 0.0

def IsoD_rank(isod):
    if isod >= 0.10:
        return 6.0
    elif isod >= 0.09:
        return 5.0
    elif isod >= 0.08:
        return 4.0
    elif isod >= 0.07:
        return 3.0
    elif isod >= 0.06:
        return 2.0
    elif isod >= 0.05:
        return 1.0
    else:
        return 0.0

def Spd_rank(spd):
    if spd >= 8.0:
        return 6.0
    elif spd >= 7.0:
        return 5.0
    elif spd >= 6.0:
        return 4.0
    elif spd >= 5.0:
        return 3.0
    elif spd >= 4.0:
        return 2.0
    elif spd >= 3.0:
        return 1.0
    else:
        return 0.0

def BABIP_rank(babip):
    if babip >= 0.330:
        return 0.0
    elif babip >= 0.320:
        return 1.0
    elif babip >= 0.310:
        return 2.0
    elif babip >= 0.300:
        return 3.0
    elif babip >= 0.290:
        return 4.0
    elif babip >= 0.280:
        return 5.0
    else:
        return 6.0
    
def PA_K_rank(pa_k):
    if pa_k >= 9.0:
        return 6.0
    elif pa_k >= 8.0:
        return 5.0
    elif pa_k >= 7.0:
        return 4.0
    elif pa_k >= 6.0:
        return 3.0
    elif pa_k >= 5.0:
        return 2.0
    elif pa_k >= 4.0:
        return 1.0
    else:
        return 0.0
    
def fip_rank(fip):
    if fip <= 2.0:
        return 6.0
    elif fip <= 2.5:
        return 5.0
    elif fip <= 3.0:
        return 4.0
    elif fip <= 3.5:
        return 3.0
    elif fip <= 4.0:
        return 2.0
    elif fip <= 4.5:
        return 1.0
    else:
        return 0

def ctl_rank(bb9):
    if bb9 <= 1.5:
        return 6.0
    elif bb9 <= 2.0:
        return 5.0
    elif bb9 <= 2.5:
        return 4.0
    elif bb9 <= 3.0:
        return 3.0
    elif bb9 <= 3.5:
        return 2.0
    elif bb9 <= 4.0:
        return 1.0
    else:
        return 0.0
    
def qs_rank(qs):
    if qs >= 0.7:
        return 6.0
    elif qs >= 0.6:
        return 5.0
    elif qs >= 0.5:
        return 4.0
    elif qs >= 0.4:
        return 3.0
    elif qs >= 0.3:
        return 2.0
    elif qs >= 0.2:
        return 1.0
    else:
        return 0.0

def HR_rank(hr9):
    if hr9 <= 0.5:
        return 6.0
    elif hr9 <= 0.75:
        return 5.0
    elif hr9 <= 1.00:
        return 4.0
    elif hr9 <= 1.25:
        return 3.0
    elif hr9 <=1.50:
        return 2.0
    elif hr9 <= 1.75:
        return 1.0
    else:
        return 0.0

def lob_rank(lob):
    if lob >= 0.775:
        return 6.0
    elif lob >= 0.750:
        return 5.0
    elif lob >= 0.725:
        return 4.0
    elif lob >= 0.700:
        return 3.0
    elif lob >= 0.675:
        return 2.0
    elif lob >= 0.650:
        return 1.0
    
def kbb_rank(kbb):
    if kbb >= 8.0:
        return 6.0
    elif kbb >= 7.0:
        return 5.0
    elif kbb >= 6.0:
        return 4.0
    elif kbb >= 5.0:
        return 3.0
    elif kbb >= 4.0:
        return 2.0
    elif kbb >= 3.0:
        return 1.0
    else:
        return 0.0
    
def k_rank(k):
    if k >= 9.0:
        return 6.0
    elif k >= 8.0:
        return 5.0
    elif k >= 7.0:
        return 4.0
    elif k >= 6.0:
        return 3.0
    elif k >= 5.0:
        return 2.0
    elif k >= 4.0:
        return 1.0
    else:
        return 0.0    

def pitcher_rank(df):
    """
    ピッチャー格付けランクを数値で返す。
    FIP 投手総合
    6 - 2.0以下
    5 - 2.5以下
    4 - 3.0以下
    3 - 3.5以下
    2 - 4.0以下
    1 - 4.5以下
    0 - 4.5より高い
    
    コントロール
    RANK BB/9
    6 - 1.5以下
    5 - 2.0以下
    4 - 2.5以下
    3 - 3.0以下
    2 - 3.5以下
    1 - 4.0以下
    0 - 4.0より高い
    
    奪三振
    RANK K/9
    6 - 0.7以上
    5 - 0.6以上
    4 - 0.5以上
    3 - 0.4以上
    2 - 0.3以上
    1 - 0.2以上
    0 - 0.2未満
    
    本塁打回避
    RANK HR/9
    6 - 0.5以下
    5 - 0.75以下
    4 - 1.00以下
    3 - 1.25以下
    2 - 1.50以下
    1 - 1.75以下
    0 - 1.75より高い
    
    対ランナー
    RANK LOB
    6 - 0.775以上
    5 - 0.75以上
    4 - 0.725以上
    3 - 0.7以上
    2 - 0.675以上
    1 - 0.65以上
    0 - 0.65未満
    
    奪三振能力
    RANK K/9
    6 - 9以上
    5 - 8以上
    4 - 7以上
    3 - 6以上
    2 - 5以上
    1 - 4以上
    0 - 4未満
    
    運
    RANK BABIP
    6 - 0.280以下
    5 - 0.280以上
    4 - 0.290以上
    3 - 0.300以上
    2 - 0.310以上
    1 - 0.320以上
    0 - 0.330以上
    
    """
    
    rank = pd.DataFrame({'投手総合':[fip_rank(df['FIP'].values)],
                        'コントロール':[ctl_rank(df['BB/9'].values)],
                        '奪三振能力':[k_rank(df['K/9'].values)],
                          '運':[BABIP_rank(df['BABIP'].values)],
                        '対ランナー':[lob_rank(df['LOB'].values)],
                        '本塁打回避':[HR_rank(df['HR/9'].values)],
                        })
    
    return rank 
    
def batter_rank(df):
    
    rank = pd.DataFrame({'得点力':[OPS_rank(df['OPS'].values)],
                        'ミート':[PA_K_rank(df['PA/K'].values)],
                        'スピード':[Spd_rank(df['Spd'].values)],
                          '運':[BABIP_rank(df['BABIP'].values)],
                        '選球眼':[IsoD_rank(df['IsoD'].values)],
                        'パワー':[IsoP_rank(df['IsoP'].values)],
                        })

    return rank

def batter_eval(url,year):
    #野手 
    data = pd.read_html(url)
    df = data[1]
    df.fillna(0,inplace=True)
    df_t = df[df['年度']==year]
    df_t2 = SabermetricsBatter(df_t)
    df_t3 = SabermetricsBatter(df)
    df_rank = pd.DataFrame()
    df_t = df_t2[['OPS','PA/K','IsoP','IsoD','Spd','BABIP']]
    print(df_t3)
    print(df_t.T.round(3))
    df_rank = batter_rank(df_t)
    labels = df_rank.columns.tolist()
    values = df_rank.to_numpy().tolist()
    rader_chart(labels, values[0]) 
    df_rank.to_csv('data/rank_data.csv',index=False)
    df_t.to_csv('data/player_data.csv',index=False)
    df_t3.to_csv('data/player_data_ttl.csv')

def pitcher_eval(url,year):
    data = pd.read_html(url)
    df = data[1]
    df.dropna(inplace=True)
    df['投球回'] = df['投球回'].str.replace('  ','').astype(float)
    df['年度'] = df['年度'].astype(int)
    df_t = df[df['年度']==year]
    df_t2 = SabermetricsPitcher(df_t)
    df_t3 = SabermetricsPitcher(df)
    df_rank = pd.DataFrame()
    df_t = df_t2[['FIP','BB/9','K/9','BABIP','LOB','HR/9']]
    print(df_t3)
    print(df_t.T.round(3))
    df_rank = pitcher_rank(df_t)    
    labels = df_rank.columns.tolist()
    values = df_rank.to_numpy().tolist()
    rader_chart(labels, values[0]) 
    df_rank.to_csv('data/rank_data.csv',index=False)
    df_t.to_csv('data/player_data.csv',index=False)
    df_t3.to_csv('data/player_data_ttl.csv')
    
if __name__ == "__main__":
    pitcher_eval('https://npb.jp/bis/players/61365139.html',2023)
#     batter_eval('https://npb.jp/bis/players/71075138.html',2023)

プログラムダウンロード(有料)

今回のプログラムのnotebookファイル、pythonファイルは以下からダウンロードすることができます。
無料枠の公開されている上記のコードをコピペでも作れますので投げ銭用くらいです。
クリエイターが喜びます。
一応ファイルをダウンロードしたい場合は有料になります。

ここから先は

22字 / 2ファイル
この記事のみ ¥ 400〜

よろしければサポートをよろしくお願いします。サポートいただいた資金は活動費に使わせていただきます。