見出し画像

打者大谷選手のおかげで、Angelsはどれだけ勝ち星を伸ばすことができたか?

みなさん。こんにちは!とーらすです。
昨日は兵庫チャンピオンシップでしたね。逃げて、持ったまんまでゴールするとは、ミトノオーはかなり強いですね。このままJDDも勝ちそうです。ちなみに私は、◎ミトノオー○キリンジで、三連単当てました!

というわけで本題に入りましょう。前回の記事で得点と勝率の関係式により、優勝チームとの勝率の差から優勝に必要な得点数がわかるので、その得点数を補うための選手を獲得すれば良いというお話をしました。その際、各選手のチームへの得点貢献度を単純に打点数で見ていました・・・。
しかし、それでいいのでしょうか?というのは、シーズン100打点記録する一方で30併殺を喫する選手は本当に100得点も価値があるのでしょうか。また、シーズン30打点しか記録しない一方で、進塁打やバンドで陰ながらチームを支える選手は30得点しか価値がないのでしょうか。

そういった背景から、打点数に代わる各選手のチームへの得点貢献度を表す指標として得点価値があります。今日の記事では、得点価値の説明と2021年・2022年のMLBのデータを参考に打者大谷選手の得点価値を見ていきたいと思います。

1921年以降のほぼ全てのMLBの試合の詳細を集計したデータは、RETROSHEETというサイトでダウンロードできます。このデータは、イニング・攻撃中のチーム・アウトカウント・ランナーの状況等が記録されています。ただし、pythonなどで分析できるようなファイル形式で保存されていないため、このサイトを参考にcsv形式に変換しましょう。(なお、chadwickというライブラリが必要で、Macならbrew install chadwickでインストールできます。)

まず初めに、得点価値を求めるために必要な得点期待値について説明します。得点期待値とは、アウトカウントとランナーの組合せごとの、イニングの残りで期待される得点数のことです。例えば、1アウト2塁の場面が5回あったとします。その5回について、残りのイニング(2アウト分の機会)で、それぞれ0点、2点、1点、0点、0点得点したとすると、1アウト2塁の場面での得点期待値は、$${\frac{0 + 2 + 1 + 0 + 0}{5} = 0.6}$$点となります。
では、MLBの全試合のデータをもとに、アウトカウントとランナーの24通り(アウトカウントは0、1、2の3通り、ランナーはそれぞれの塁にいるかいないかなので、$${2 \times 2 \times 2 = 8}$$通り)の組合せごとの得点期待値を求めましょう。得点期待値を求めるpythonコードは下記です。

import pandas as pd
import numpy as np

### 各プレーごとの残りのイニングにおける得点(ROI)を求める関数
def make_roi(data):
    ### 各プレーでのhomeとawayの得点数の合計をRUNSカラムとして保存
    data["RUNS"] = data["AWAY_SCORE_CT"] + data["HOME_SCORE_CT"]
    
    ### 各プレーでの得点をSCOREDカラムとして保存
    ### BAT_DEST_ID、RUN1_DEST_IDは各打者・ランナーの進塁情報を意味し、3を超えると得点が記録される
    data["SCORED"] = (data["BAT_DEST_ID"] > 3)*1 + (data["RUN1_DEST_ID"] > 3)*1 + (data["RUN2_DEST_ID"] > 3)*1 + (data["RUN3_DEST_ID"] > 3)*1
    
    ### 必要なカラムだけ抽出
    half_inning = data[["GAME_ID", "INN_CT", "BAT_HOME_ID", "OUTS_CT", "RUNS", "SCORED"]].copy()
    
    ### 各イニングの最初の得点を取得・・・①
    runs_start = half_inning.groupby(["GAME_ID", "INN_CT", "BAT_HOME_ID"], as_index = False).min()[["GAME_ID", "INN_CT", "BAT_HOME_ID", "RUNS"]]

    ### 各イニングでの得点の合計を取得・・・②
    runs_inning = half_inning.groupby(["GAME_ID", "INN_CT", "BAT_HOME_ID"], as_index = False).sum()[["GAME_ID", "INN_CT", "BAT_HOME_ID", "SCORED"]]

    ### ①と②をマージし、各イニング終了時の得点数をMAX_RUNSカラムで保存・・・③
    max_runs = pd.merge(runs_start, runs_inning, on = ["GAME_ID", "INN_CT", "BAT_HOME_ID"], how = "inner")
    max_runs["MAX_RUNS"] = max_runs["RUNS"] + max_runs["SCORED"]
    
    ### dataフレームワークと③をマージ
    data = pd.merge(data, max_runs[["GAME_ID", "INN_CT", "BAT_HOME_ID", "MAX_RUNS"]], on = ["GAME_ID", "INN_CT", "BAT_HOME_ID"], how = "inner")

    ### イニングの残りにおける得点をROIカラムとして保存
    data["ROI"] = data["MAX_RUNS"] - data["RUNS"]
    
    ### 各イニングのアウト数をInningカラムに保存・・・④
    innings = data[["GAME_ID", "INN_CT", "BAT_HOME_ID", "EVENT_OUTS_CT"]].groupby(["GAME_ID", "INN_CT", "BAT_HOME_ID"], as_index = False).sum()[["GAME_ID", "INN_CT", "BAT_HOME_ID", "EVENT_OUTS_CT"]]
    innings["Inning"] = innings["EVENT_OUTS_CT"]
    
    ### dataフレームワークと④をマージ
    data = pd.merge(data, innings[["GAME_ID", "INN_CT", "BAT_HOME_ID", "Inning"]], on = ["GAME_ID", "INN_CT", "BAT_HOME_ID"], how = "left")
    
    ### 各プレーのランナーの情報をBASESカラムとして保存(例えば、ランナー1塁だと、"100")
    data["BASE"] = (1-(data["BASE1_RUN_ID"].isnull())*1).astype(str) + (1-(data["BASE2_RUN_ID"].isnull())*1).astype(str) + (1-(data["BASE3_RUN_ID"].isnull())*1).astype(str)
    data["STATE"] = data["BASE"] + data["OUTS_CT"].astype(str)
    
    ### 各プレー終了後の各塁のランナーの情報をNRUNNERに保存
    data["NRUNNER1"] = (data["BAT_DEST_ID"] == 1) | (data["RUN1_DEST_ID"] == 1)
    data["NRUNNER2"] = (data["BAT_DEST_ID"] == 2) | (data["RUN1_DEST_ID"] == 2) | (data["RUN2_DEST_ID"] == 2)
    data["NRUNNER3"] = (data["BAT_DEST_ID"] == 3) | (data["RUN1_DEST_ID"] == 3) | (data["RUN2_DEST_ID"] == 3) | (data["RUN3_DEST_ID"] == 3)
    
    ### 各プレー終了後のアウトカウントをNEW_OUTSカラムに保存
    data["NEW_OUTS"] = (data["OUTS_CT"] + data["EVENT_OUTS_CT"]).astype(str)
    ### 各プレー終了後のランナー・アウトカウントの情報をNEW_STATEカラムに保存
    data["NEW_STATE"] = (data["NRUNNER1"]*1).astype(str) + (data["NRUNNER2"]*1).astype(str) + (data["NRUNNER3"]*1).astype(str) + data["NEW_OUTS"]
    
    return data

### 24通りの得点期待値を求める関数
def make_score_matrix(data):
    ### STATEに変化があるプレーもしくは得点が記録されたプレーのみ抽出
    data_r = data[(data["NEW_STATE"] != data["STATE"]) | (data["SCORED"] > 0)].copy()
    ### 3つのアウトが記録された完全なハーフイニングのみのデータを抽出
    data_r = data_r[data_r["Inning"] == 3]
    ### 得点期待値を算出
    matrix = data_r.groupby(["STATE"], as_index = False).mean()[["STATE", "ROI"]]
    
    return matrix

### カラム名の取得
head = pd.read_csv("http://bayesball.github.io/baseball/fields.csv")### データの読み込み

### 2021年
df2021 = pd.read_csv("./all2021.csv", header = None, names = np.array(head["Header"]))
df2021 = make_roi(df2021)
### 2021年の得点期待値
matrix2021 = make_score_matrix(df2021)

### 2022年
df2022 = pd.read_csv("./all2022.csv", header = None, names = np.array(head["Header"]))
df2022 = make_roi(df2022)
### 2022年の得点期待値
matrix2022 = make_score_matrix(df2022)

2021年の得点期待値(matrix2021)を見てみると面白いことがわかります。ノーアウト1塁(BASEが100、OUTSが0)の得点期待値は0.91ですが、ワンアウト2塁(BASEが010、OUTSが1)になると、得点期待値が0.53に下がります。野球好きの方はピンときたかもしれませんが、ノーアウト1塁からの送りバントって得点期待値を下げてしまうので、あまり意味がないんですよね。これが送りバントが不要と言われる理由ですね。ただ、これはMLBのデータなので、NPBのデータで得点期待値を出してみるとどうなるかは見てみたいですね。NPBのお偉いさん!野球データをオープンデータとして公開してください笑

得点期待値

次に、本題の各選手の得点価値を求めましょう。各選手の得点価値は、その選手の各プレーの得点価値を合計することで求まります。各プレーの得点価値は、新しい得点価値と古い得点価値の差とその特定のプレーでの得点を足し合わせることで求めることができます。例えば、1アウトランナー1塁でヒットを打ち1アウトランナー1・3塁になった場合の得点価値は、$${1.13 - 0.53 = 0.6}$$となります。各選手の得点価値を求めるpythonコードは下記です。

### 各選手の各プレーの得点価値を求める関数
def make_run_value(data, matrix):
    ### STATEに変化があるプレーもしくは得点が記録されたプレーのみ抽出
    data_r = data[(data["NEW_STATE"] != data["STATE"]) | (data["SCORED"] > 0)].copy()
    
    ### STATEカラムのROIをマージ
    data_r = pd.merge(data_r, matrix[["STATE", "ROI"]], on = ["STATE"], how = "left", suffixes = ["", "_STATE"])
    
    ### NEW_STATEカラムのROIをマージ
    matrix["NEW_STATE"] = matrix["STATE"]
    data_r = pd.merge(data_r, matrix[["NEW_STATE", "ROI"]], on = ["NEW_STATE"], how = "left", suffixes = ["", "_NEW_STATE"])
    
    ### NEW_STATEが3アウトの時、ROI_NEW_STATEカラムが欠損となる。3アウト時のROIは0なので、欠損を0で補完
    data_r["ROI_NEW_STATE"] = data_r["ROI_NEW_STATE"].fillna(0)
    
    ### 新しい得点期待値と古い得点期待値の差とその特定のプレーでの得点を足し合わせた値を得点価値とし、RUN_VALUEカラムとして保存
    data_r["RUN_VALUE"] = data_r["ROI_NEW_STATE"] - data_r["ROI_STATE"] + data_r["SCORED"]
    
    return data_r

### 各選手の得点価値を求める関数
def player_run_value(data):
    ### 総打席数を求めるためのカラムを作成
    data["PA"] = 1
    
    ### 得点価値の合計をRE24、得点可能性の合計をSTARTカラムに保存
    ### 打撃イベントだけを抽出するため、BAT_EVENT_FLがTRUEのもののみ抽出
    data_agg = data[data["BAT_EVENT_FL"] == "T"].groupby("BAT_ID", as_index = False).sum()[["BAT_ID", "ROI_STATE", "PA", "RUN_VALUE"]]
    
    return data_agg

df2021 = make_run_value(df2021, matrix2021)
df2022 = make_run_value(df2022, matrix2022)

player_2021 = player_run_value(df2021)
player_2022 = player_run_value(df2022)

2021年の各選手の得点価値を見てみましょう。打者大谷選手の得点価値は53.6点で、MLB全体4位の得点価値を誇ります。10点=1勝とすると、打者大谷選手1人の力で5勝していることになります。さらに、投手大谷選手は2021年9勝してますので、2021年の大谷選手のMVPは文句なしといって問題なさそうです。
他の選手を見てみると、大谷選手とホームランダービーで争ったソト選手、ホームラン王のゲレーロ選手やタティス選手の得点価値も高いですね。また、コーリーシーガー選手の得点価値が得点期待値の割に高いですね。コーリーシーガー選手は95試合の出場で打率.306、本塁打16本、打点57で少し平凡な数字に見えますが、得点価値という観点では、かなり活躍した選手といえます。ちなみに、得点期待値がかなり高いのに、得点価値がマイナスになっている選手は、チャンスでことごとく凡退している選手ですね。パワプロなら確実にチャンス×がつく選手です。

各選手の得点期待値の合計と得点価値

2022年の各選手の得点期待値と得点価値を見てみましょう。やっぱりジャッジ選手はえぐいですね。1人で80点の得点価値を叩き出しています。打者大谷選手の得点価値(33.5点)の2倍以上と考えると、MVPはジャッジで妥当ですね。

各選手の得点期待値の合計と得点価値

というわけで、今回は打者大谷選手の2021年・2022年シーズンの得点価値を見てみました。Angelsは打者大谷選手のおかげで、2021年は5つ、2022年は3つ勝ち星を伸ばしたことがわかりました。少なくない?と思われたかもしれませんが、2021年のMLBの各選手の得点価値の平均は10、2022年は7.3(打席数が400以上の打者のみ)なので、MLBの平均的な選手と比べて、2021年は5.3倍、2022年は4.7倍、勝ち星を伸ばしています。(ジャッジ選手は、2022年のMLBの平均的な選手の約11倍勝ち星を伸ばしています。化け物!)

この調子で、野球のデータ分析についてnoteでまとめていきたいと思います。





この記事が気に入ったらサポートをしてみませんか?