古典野球データ主義者がStatCastに抗ってみる


トラックマンをはじめとするデータ解析ツールが一般的になって久しい今日この頃。

あらゆる野球人が**成績（結果）よりも動き（過程）に重きを置く**ようになり、一つ一つのプレーに対する考え方、選手の評価方法が大きく変わりつつあります。

<br><br>
でも、昨今のこういう流れを何だかポジティブに受け入れられていない自分がいるように思えてならない...。
<br>
いや、正直なところそれが本音です。
<br>
だって私が子供のころから追っかけてきたのは、<br>**投手なら（勝/敗/防御率）、野手なら（打率/HR/打点/盗塁）というお決まりのセット**です。

これに嫌というほど触れてきたから、2010年の金本知憲（.241/16本/45打点）とか2011年の荒木雅博（.263/2本/24打点）みたいな成績を見ると、言葉では表現できない「良さ」を感じて涎が止まらなくなるのです。
<br>
それなのに、急に**平均回転数だの打球速度だの、バレル％だのスイートスポット％だの、xOBPだのxwOBAだの**と攻め立てられると、保守派の私はただただオロオロすることしかできないのです。
<br>
とはいっても、動作解析の隆盛がとどまることを知らない現代野球についていくためには、いつまでもStatCast的な指標から目を背けるわけにはいきません。

そこで、どの指標がどの成績と強い結びつきをもっているのか、あるいは全然関わりがないのかを、自分なりに明らかにしてみようと考えました。
<br>
もちろん、「この分析を通じてセイバーメトリクス関連の知見を深めたい」みたいな崇高な目標意識はありません。

一口に言えば、**「StatCastなんて意味ねえ！！　結局今まで通り試合での成績だけ見てればええんや！！！」**　っていうような結論がほしくて、やってみるだけです。



-理論編-　
-
野球というスポーツの一つの特徴として、**身体能力が成績に反映されづらい**という側面があります。

これは、ある意味では素晴らしい側面だと思います。
生まれ持った身体能力では劣っていても、野球という競技においては大活躍できる可能性が十分にあるのですから。
<br>
昨今の動作解析偏重の風潮というのは、こうした野球の魅力を形作っていた特徴を揺るがすものです。

だからこそ私は、<b>StatCastで収集されたデータがどれほど成績に影響を及ぼしているのか、どこまで信頼に足る数値なのかをはっきりさせたいというわけです。</b>

今回の検証では、必要なデータを取得・整理して、データの標準化を行ったうえで回帰分析を実行し、各説明変数の重み（パラメータ）を算出します。<b>目的変数は打率と本塁打数、説明変数はStatCastで示される動作解析系の数値とします。</b>

打率および本塁打数とStatCast系の指標の関係性を知りたい、願わくばあまり関係がないという結果がほしい、というのがスタート時点での考えです。

-取得編-　
-
案の定ですが、NPBに関してはStatCast系の数値が網羅されたサイトはなく、そもそも個人成績をまとめてCSVファイルでローカルに落とし込めるようなシステムがありません。

私は日本の野球を追いかけ続けてきたので、せっかくならNPBで検証したいと思っていましたが、個人レベルで今回のような分析を行うのはちょっと厳しそうです。

仕方なく、MLBの公式サイトからデータを取得することにします。
>[MLB公式サイト](https://www.mlb.com/)


ほしいデータは、MLB.com　→　STATS　→　Statcast Leaders　からダウンロードできます。
さらに今回はデータに含める指標を自分で選択したいので、Cusom Leaderboard のページに移ります。

![スクリーンショット 2024-05-18 104517.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3186880/b17b6e26-883d-f0e7-0af1-b5897dbec55a.png)


今回取得する指標は以下の通りです。

|変数名 |内容  |備考  |
|---|---|---|
|player_name  |名前 |  |
|player_id  |選手ID  | |
|year  |年度  |2023年  |
|player_age  |年齢  |最年長はミゲルカブレラ(40)  |
|ab  |打数  |  |
|pa  |打席数  |  |
|hit  |安打  | |
|single  |単打  |  |
|double  |二塁打  |  |
|triple  |三塁打  |  |
|home_run  |本塁打  |  |
|strikeout  |三振  |  |
|walk  |四球|  |
|k_percent  |三振率  |  |
|bb_percent  |四球率  |  |
|battinng_avg  |打率  |  |
|slg_percent  |長打率  |  |
|on_base_percent  |出塁率  |  |
|on_base_plus_slg  |OPS  |  |
|babip  |インプレー打率  |本塁打を除くフェアゾーンに飛んだ打球が安打になった割合  |
|rbi  |打点  |  |
|xba  |打率の期待値  |  |
|xslg  |長打率の期待値  |  |
|woba  |得点貢献度  |1打席あたりにどれだけチームの得点増加に貢献したかを表す指標。安打や四球など出塁を伴う要素に得点価値を加重して算出  |
|xwoba  |得点貢献度の期待値  |  |
|xobp  |出塁率の期待値  |  |
|xiso  |（長打率ー打率）の期待値  |  |
|exit_velocity_avg  |平均打球速度  | |
|launch_angle_avg  |平均打球角度  |  |
|sweet_spot_percent  |スイートスポット率  |打ち出し角度が8度から32度の割合  |
|barrel_batted_rate  |バレルゾーン率  |得点に繋がりやすい打球速度と打球角度の組み合わせ（バレルゾーン）に入る割合  |
|hard_hit_percent  |ハードヒット率  |打ち出し速度が95mph以上の割合  |
|avg_best_speed  |打球速度の上位50％  |  |
|avg_hyper_speed  |調整打球速度  |88マイル未満の打球は全て88マイルとし、それ以上はそのままにした平均打球速度  |
|whiff_percent  |空振り率  | |
|swing_percent  |スイング率  |  |

なお、外れ値をなるべく減らすため、今回は2023シーズンに250打席以上立っている選手に限定してデータを抽出しました。




-整理編-　
-
公式サイトからダウンロードしたファイルを見てみると、姓名が別々のカラムに入ってたので、player_nameとしてスプレッドシートで統合しました。

![スクリーンショット 2024-05-18 105246.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3186880/58425c48-68f9-c3c4-12f9-4f99ad2950fe.png)

このまま分析に移ってもよいのですが、各種成績の期待値（x○○系）と実際の成績の関連性が気になったので、少しだけ確認してみます。
<br>
Bigqueryにデータフレームを読みこませて、適当にSQLを投げて一部選手の成績を抽出しました。

以下は打率・長打率・出塁率が期待値より高い選手を期待値との乖離順で表示させたものです。

![スクリーンショット 2024-05-11 101758.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3186880/5accadae-c512-9c09-61ba-b98e0c0c808b.png)
![スクリーンショット 2024-05-11 102018.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3186880/3103cb16-7e2c-f820-051f-e74d7850fa34.png)
![スクリーンショット 2024-05-11 102144.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3186880/52b2918f-c74e-af15-30a7-422acc17d3ae.png)

打率・出塁率で1位に来ているのはホセ・アルトゥーベです。ある意味めちゃめちゃイメージ通りですね。あのフィジカルでメジャートップレベルの打撃成績を維持しているのは、Statcastのデータから見ても驚く結果のようです。

長打率で1位なのはフェルナンド・タティス・Jrでした。身体能力の塊みたいな選手ですが、Statcast指標から予測される成績は意外と大したことないようですね。

逆に、各成績が期待値より低い選手のランキングも見てみます。

![スクリーンショット 2024-06-18 115711.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3186880/d5fed881-69ee-042a-420a-a1cbc4821579.png)
![スクリーンショット 2024-06-18 115816.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3186880/09793007-e49c-cb0c-0da9-0462e3285fa0.png)
![スクリーンショット 2024-06-18 115905.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3186880/f6539b8a-79ae-9a4c-2eb9-7cebd04559d1.png)

打率・出塁率ではウラディミール・ゲレーロ・ジュニアが目立ちます。
2018年に全米プロスペクト1位、2021年には堂々のホームラン王を獲得した圧倒的スケールの持ち主ですが、2023シーズンは打率.264、26本塁打と不完全燃焼な印象でした。

長打率ではアーロン・ジャッジが1位。
2023シーズンは6割を超える素晴らしい長打率を記録しましたが、期待値が.712と異常だったのでこういう結果になりました。可哀そうというべきかどうなのか・・・。

これだけ見てると、Statcastデータをもとに算出された期待値というのはあてにならないなと思ってしまいますね。まあ、乖離が大きい選手を抽出しているせいなんですが。

-分析編-　
-
今回は、ひとまずRStudioで重回帰分析もやってみることにします。


重回帰分析を行う前の準備段階として、以下の記事を参考にしてデータフレームの標準化を行いました。
>[Rでdata frameを標準化するメモ（関数作った＆パッケージ化した）](https://indenkun.hatenablog.com/entry/2021/04/08/091255)

今回使用するデータフレームには数値型ではない値（選手の氏名など）が含まれているため、そのままscale関数を適用しようとすると以下のエラーが表示されます。
```R
Error in colMeans(x, na.rm = TRUE) : 'x' must be numeric
```

だから列を限定してscale関数を適用させるのですが、これをやるとデータフレームとしての構造が崩れてしまい、このあとやりたい操作（lm関数による回帰分析）ができなくなってしまいます。

なので、次の手順で標準化および回帰分析をすることになります。
<b>
①指定した列のみscale関数を適用させたものを新しい変数に格納する
②このままだとただの列のまとまりなので、cbind関数を適用して標準化していない列といま作った変数を結合する
③データフレームの状態を確認したうえで、lm関数により重回帰分析を行う
</b>
具体的には以下のようになります。
```R
#元のデータはbatterdfに格納
batterdf.sc <- scale(batterdf[5:36]) #5列目から36列を標準化する
batterdf.new_sc <- cbind(batterdf[1:4], batterdf.sc) #標準化されてない部分と結合してデータフレームに戻す
head(batterdf.new_sc) #データフレームになっていることを確認する
```
最後の行の出力結果は以下の通りです。
![スクリーンショット 2024-05-25 110300.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3186880/b2ef7329-b629-947f-1719-19be01881738.png)

ちゃんとデータフレームになっているようです。標準化もしっかりされてます。
この状態で、lm関数による回帰分析を実行します。

冒頭で紹介したように、この分析における目的変数は打率と本塁打数、説明変数はStatCastで示される動作解析系の数値と設定しているので、以下のように記述します。

```R
HR.lm <- lm (home_run~exit_velocity_avg+launch_angle_avg+sweet_spot_percent+barrel_batted_rate+hard_hit_percent
             +avg_best_speed+avg_hyper_speed+whiff_percent+swing_percent,data=batterdf.new_sc)
summary(HR.lm)

AVG.lm <- lm (batting_avg~exit_velocity_avg+launch_angle_avg+sweet_spot_percent+barrel_batted_rate+hard_hit_percent
              +avg_best_speed+avg_hyper_speed+whiff_percent+swing_percent,data=batterdf.new_sc)
summary(AVG.lm)
```
<br>

本塁打数が目的変数の場合の各指標の重みは以下の通り。
![スクリーンショット 2024-06-02 114222.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3186880/78ae464d-4627-ad74-e331-d04193c158e6.png)
![スクリーンショット 2024-06-02 114033.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3186880/fb8877a8-0df8-c4a6-7371-7bb6861551cc.png)

<br>

打率が目的変数の場合の各指標の重みは以下の通り。

![スクリーンショット 2024-06-02 114205.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3186880/695e780e-2630-aa3b-58a6-abb559b82898.png)
![スクリーンショット 2024-06-02 114047.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3186880/63c26766-d536-f08a-9615-d148a8733373.png)
<br>
Rだけだと味気ないので、せっかくだからPythonライブラリを使った重回帰分析もやってみます。

こちらはGoogleColaboratoryで動かしてみます。環境構築不要でデータ分析できるのは良いですよね。

基本的には<a href="https://free.kikagaku.ai/">株式会社キカガクの学習講座</a>に従って、以下の手順で進めます。

<b>①データセット読み込み
②外れ値除去
③学習用データセットとテスト用データセットの分割
④学習用データセットを標準化
⑤モデル構築
⑥検証、可視化
<b/>

本塁打数を目的変数とした場合の操作を記載します。

①データセット読み込み
```Python
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

df = pd.read_csv("/content/mlb2023batters.csv")
```

②外れ値除去
```Python
hrdf = df.reindex(columns=['exit_velocity_avg', 'launch_angle_avg', 'sweet_spot_percent', 'barrel_batted_rate', 'hard_hit_percent', 'avg_best_speed', 'avg_hyper_speed', 'whiff_percent', 'swing_percent', 'home_run'])
h_mean = hrdf.mean() #平均値
h_sigma = hrdf.std() #標準偏差
h_cols = hrdf.columns #列の取得

_df = hrdf
for col in h_cols:
  # 3σ 法の上限値の設定
  low = h_mean[col] - 3 * h_sigma[col]
  high = h_mean[col] + 3 * h_sigma[col]
  # 条件で絞込
  _df = _df[(_df[col] >= low) & (_df[col] <= high)]
```

③学習用データセットとテスト用データセットの分割

```Python
x = _df.iloc[:, :-1] #各選手のStatcastの数値
t = _df.iloc[:, -1 #各選手のHR数

from sklearn.model_selection import train_test_split
x_train, x_test, t_train, t_test = train_test_split(x, t, test_size=0.4, random_state=1)
```
④学習用データセットを標準化

```Python
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
scaler.fit(x_train, t_train)

x_train2 = scaler.transform(x_train)
x_test2 = scaler.transform(x_test)
```

⑤モデル構築
```Python
model = LinearRegression().fit(x_train2, t_train)
```

⑥検証、可視化
```Python
model.score(x_test2, t_test) #決定係数は約0.59
np.set_printoptions(precision=5, suppress=True) #有効数字5桁、指数表示禁止
model.coef_
```

各指標の重みとグラフ化した図は以下の通り。

![スクリーンショット 2024-06-20 145536.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3186880/08849e38-238c-99e3-2427-8674e2176c04.png)

![スクリーンショット 2024-06-02 113305.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3186880/4869fb34-4007-22d4-ce40-be2b32b8c362.png)

打率が目的変数の場合についても同じ操作を行いました。
分析結果は以下の通り。

![スクリーンショット 2024-06-20 145615.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3186880/88fbf4d2-3d41-ee0f-eb61-42df3d19cdb1.png)
![スクリーンショット 2024-06-02 112811.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3186880/00af6841-5e15-2587-aab5-bf9ca6ca9321.png)


-考察編-
-

まず、本塁打数を目的変数とした場合の分析結果を見てみましょう。

各指標の重みをみると、やはりバレル率の高さが一際目を引きます。

が、そもそもバレルというのは打球角度と打球速度の組み合わせとそれに対応する結果を統計的に調査し、そのなかで圧倒的に長打になりやすい組み合わせとして規定された指標です。
言うなれば、<b>成績ドリブンな指標</b>であるわけです。

なので、バレル率と本塁打数の間に強い相関があるのはある意味当たり前の話。

それ以外の指標でいうと、平均打球速度の係数が割と大きくなっています。

ですが、こちらも結果をそのまま受け入れるわけにはいきません。

打球速度の指標というのはStatcast開始当初から存在するわけですが、全体の平均をとってしまうと速度にバラつきがある選手とずっと同じくらいの選手が同じものとして扱われてしまうため、選手の価値を測る指標としては疑問視されてきたんですね。

そのなかで色々と試行錯誤が重ねられて、現在は速度上位50％の打球だけで平均をとる<b>EV50（avg_best_speed）</b>、88mph以下の打球は全て88mphだとみなして平均をとる<b>Adjuted EV（avg_hyper_speed）</b>という2つの指標がLeaderboardに加えられています。

この辺の詳しい話は以下の記事を参照してください。
>[打球分類ごとのEscape Velocity～フライボール革命の幹～](https://note.com/calm_phlox375/n/na1effa926cf1?sub_rt=share_pw)

まあてなわけで、全ての打球速度の平均をとっただけの値というのは、もはやあまり意味がないものとしてみなされているんですよね。

今回の分析においては、平均打球速度こそ割と相関がありそうな感じで出てますが、EV50やAdjusted EVに関しては係数が小さな値になっており、あまりつながりがないように見えます。

こうなってくると、打球速度だけでホームランをよく打つ選手か、そうでないかを見極めるのは難しいという話になってきますね。

ただ唯一信頼して良さそうなのは、空振り率の重みです。
空振り率と本塁打数の間には負の相関があり、空振りが多い選手は比較的HRが少ないという結論は。とりあえず間違いなさそうですね。
<br>
続いて、打率を目的変数にとった場合の分析もみてみます。

まず相関が強そうな指標として気になるのは、<b>スイートスポット率</b>です。

この指標は打ち出し角度が8度から32度の割合を表しています。
ゴロよりフライ性の打球の方が価値は高いという考えが広く浸透していますが、今回の分析ではそれを裏付けるような結果となりました。

そして最も係数が大きいのは先ほど紹介したAdjusted EVです。
平均打球速度だけで見るとあまり関係がなさそうですが、速い打球の比重を上げてみると強い相関がみられました。これはなかなか重要な発見になるかもしれません。

また、こちらでも空振り率とは強い負の相関がみられます。
空振りの多い選手は打率が低い、その逆もしかり、というのはイメージ通りですね。
<br>
以上の考察を踏まえて、ひとまず次の2つの結論を出しました。


<b>１．「空振りが多い選手はダメ！バットに当てることからすべてが始まる」</b>
<b>２．「全ての解析データをそのまま示すタイプの指標はあてにならない。人間側での調整が必要」</b>

１に関しては、読んで字のごとくです。
<b>バットに当て、ボールを前に飛ばさなければ何も始まりません。</b>それができていない段階で理想的な打球角度やら打球速度やらを探求するのはナンセンスです。

２の方についても、ある意味当たり前といえば当たり前のことなのかもしれません。
全ての打球速度の平均をとったExitVerocityAvgや全ての打球角度の平均をとったLaunchAngleAvgを見るだけでは、その選手の価値や能力は判断できないのです。

EV50・Adjusted EVやスイートスポット率のように、特定の範囲の数値に重きを置いてその割合を計測したり、バレル率のように複数の指標を組み合わせたりすることによって、真に価値ある打球をどのくらい放っているかが明瞭になってきます。

Statcastで取得したデータをそのまま受け取るのではなく、<b>試合での成績とのつながりが分かりやすい形に加工し・調整を行って、初めて動作解析系の数値は生き生きとその選手の特性を語ってくれるのです。</b>

-結語-
-
素人がとりあえずやってみた野球データ分析、いかがだったでしょうか。

正直、己の知識不足のために踏み込めてない領域がかなりあるので、新たな発見といえるような成果を出すところまでは到達できてません。

とはいえ、個人的にはやってよかったと思います。
実際に分析をして記事にまとめるとなればくまなく調べるほかないので、元々興味がなかったStatcastの項目や最近導入されたセイバーメトリクス系の指標についてもかなり知識が深まりましたからね。

本当は投手成績も含めてもっと広い範囲を調査したかったのですが、キリがないのでかなり限定的になってしまいました。

気が向いたら、違うテーマでまたやろうかな…。

<b>ていうか、NPBの成績で分析させてくれ！！早く公式に動作解析系の数値を掲載して、CSVでダウンロードできるようにしてくれ！！！</b>
<br>
以上、古典野球データ主義者がStatCastに抗ってみるコーナーでした。

それではまた。