得点期待値行列
野球において、それぞれの塁にはランナーいる場合と、いない場合があります。するとランナーのパターンは8通りになります(ランナーなしからランナー満塁まで)
一方、アウトカウントは0アウトから2アウトの3通りになります。
そうすると、ランナーとアウトの取りうる組み合わせは8×3で24通りになります(ノーアウトランナーなしから2アウト満塁まで)
野球のゲームというのはこのパターンが変動するゲームであると言えます。
そうすると、過去の試合を遡り、アウトカウントとランナーの組み合わせで、イニングの残り(=3アウトになるまで)での平均得点を計算し求められる値が得点期待値となります。そして、それを表形式に表したものが得点期待値行列となります。
過去のデータから、このアウトカウント・ランナーのパターンの場合、平均何点入るか?ということから今のプレーを分析することができるのです。
データの入手
まずはじめに、2021年のEvent Filesをretrosheetから準備しました。
Retrosheetについては下記のリンクから。
Event Filesは、それぞれの試合の打者ごとのイベントのデータになります。
このデータを上手に加工して、得点期待値を求めていきたいと思います。
イニングの残りで記録された得点
イニングの残りでの得点数を計算するには、ある打席までのホーム・ビジター両チームの得点数とあるイニングの終わりにおける両チームの得点数を知る必要があります
RUNS.ROI =(あるイニング終了時の得点)ー(現在の得点)
このRUNS.ROIを求めることを目的として計算を進めます。
まず、2021年のEvent filesのデータに、mutate関数を用いてアウェーチーム、ホームチームの合計得点をRUNSとして計算します。
data2021 %>% mutate(RUNS = AWAY_SCORE_CT + HOME_SCORE_CT) -> data2021
次に、HALF.INNINGとして、試合ID、イニング、表か裏かというデータを作成します
data2021 %>% mutate(HALF.INNING = paste(GAME_ID, INN_CT, BAT_HOME_ID))->data2021
そうすると ”ANA202104010 1 0” ”アナハイムで行われた2021年4月1日 1回 表” みたいなデータができます。
各プレーにおける得点を記録するRUNS.SCOREDという変数を作成します
Event FilesにはBAT_DEST_IDという項目があります。これは1なら安打、2なら2塁打、3なら3塁打、4なら本塁打
また、RUN_DEST_IDというのもあり、これは1なら1塁へ進塁、2なら2塁へ、3なら3塁へ、4以上は得点となります。
data2021 %>% mutate(RUNS.SCORED = (BAT_DEST_ID>3)+(RUN1_DEST_ID >3)+ (RUN2_DEST_ID>3)+(RUN3_DEST_ID > 3))->data2021
つづいて、HALF.INNING(試合のID+イニング+表か裏かのデータ)で集計をします。
そして、各イニングでの①アウトカウント数(これは大半が3アウト)、②各イニングでの得点数、③イニング開始時の得点、④イニング終了時の得点を算出します。
data2021%>% group_by(HALF.INNING)%>% summarize(Out.Inning = sum(EVENT_OUTS_CT), Runs.Inning = sum(RUNS.SCORED), Runs.Start = first(RUNS), MAX.RUNS = Runs.Inning + Runs.Start) -> half_innings
このデータをdata2021に結合します。
data2021%>% inner_join(half_innings, by="HALF.INNING")->data2021
そして、RUNS.ROI =(そのイニングの最終的な得点)ー(現在の得点)を算出します。
data2021%>% mutate(RUNS.ROI = MAX.RUNS - RUNS) -> data2021
そうすると、各打席にイニングの残りでの得点を導き出すことができます。
行列の作成
続いて、ランナーの状況を求めていきましょう。
BASE_RUN_IDはランナーにはランナーの名前が入ってきます。ここにデータがあれば1としていきます。
data2021%>% mutate(BASES = paste(ifelse(BASE1_RUN_ID > "",1,0), ifelse(BASE2_RUN_ID > "",1,0), ifelse(BASE3_RUN_ID > "",1,0),sep = "")-> data2021
そうすると結果100=ランナー1塁や、101ランナー1塁3塁みたいなデータになります。
そして、アウトカウントも付け加えます。
data2021%>%
mutate(STATE = paste(BASES, OUTS_CT))->data2021
現在のプレーでのランナー変化を調べます。
RUN_DEST_ID、BAT_DEST_IDは現在のプレーでランナー、バッターがどこに進塁したかを示します。1なら結果1塁に進んだor留まったという意味になります。それを利用して現プレーによって変化したランナーNRUNNERを導きます。
data2021%>% mutate(NRUNNER1= as.numeric(RUN1_DEST_ID == 1|BAT_DEST_ID ==1), NRUNNER2= as.numeric(RUN1_DEST_ID ==2|RUN2_DEST_ID ==2| BAT_DEST_ID==2), NRUNNER3= as.numeric(RUN1_DEST_ID==3|RUN2_DEST_ID==3| RUN3_DEST_ID==3|BAT_DEST_ID==3))->data2021
アウトカウントの変動を算出します。
data2021%>% mutate(NOUTS = OUTS_CT + EVENT_OUTS_CT)->data2021
新たなランナーのパターンNEW.BASESとアウトのパターンの状況を加えたものNEW.STATEを作ります。
data2021%>% mutate(NEW.BASES = paste(NRUNNER1,NRUNNER2,NRUNNER3,sep=""), NEW.STATE = paste(NEW.BASES,NOUTS))->data2021
そうすると、ノーアウトランナー1塁 ”100 0” から
送りバントでワンアウト2塁 ”010 1”みたいなのがわかるようになります。
ここで、変化があったプレーのみ抽出します。
data2021%>% filter((STATE != NEW.STATE)|(RUNS.SCORED > 0))->data2021
続いて、3アウトまで到達したイニングをのみを抽出します。これで小さなバイアスを除きます。
data2021%>% filter(Out.Inning == 3)->data2021C
そうしましたら、ランナー・アウトの状況であるSTATEでグループ化し、ROIの平均を算出します。
data2021%>% group_by(STATE)%>% summarize(Mean = mean(RUNS.ROI))->RUNS
アウトカウントを追加し、順番を変更します。
RUNS%>% mutate(Outs = substr(STATE, 5,5)) arrange(Outs)->RUNS
0アウト | 1アウト | 2アウト | |
ランナー無 | 0.52 | 0.27 | 0.1 |
1塁 | 0.92 | 0.54 | 0.23 |
2塁 | 1.15 | 0.7 | 0.33 |
3塁 | 1.38 | 0.95 | 0.38 |
1、2塁 | 1.54 | 0.92 | 0.45 |
1、3塁 | 1.76 | 1.13 | 0.49 |
2、3塁 | 2.13 | 1.4 | 0.59 |
満塁 | 2.46 | 1.69 | 0.83 |
これで、得点期待値の行列を求めることができましたね!!
最近思うのは、ツーアウト2塁でチャンス!みたいなこと言われるけど、そんなことないよね。