statcastデータにおけるlaunch_speed, launch_angleの欠測値について(前編)
※この記事で使用するコードはこちらに載せています。
先日、こちらのnoteを拝見しまして、
2. 欠測値補完方法の違いによる打球速度、打球角度の変化
を自分でも検証したくなったので、詳しく見てみました。
※前編で1.、後半で2.を扱っていきます。
データを見る前に
statcastデータの欠測値についてTom Tango氏、Jim Albert氏の記事によると
Cleaning Statcast Data | Exploring Baseball Data with R
- 犠牲バントや鋭いゴロ、高いポップフライが欠測しやすい
- 打球内容ごとに欠測に対してグループ平均値代入をしているらしい
- ただその打球内容の粒度は公開されていないので、代入したデータがどこかはわからない
とのこと。
これを頭の片隅に置きつつ、データを見ていきたいと思います。
せめてどこに代入したかは教えてクレメンス(泣)
実際のデータを見てみる
使用データ
2017~2019年のレギュラーシーズンのデータのうち投球情報、打球位置がわかる打球データのみを使用しました。
行数:376,828
うち欠測値:34,157(9.06%)
[データ取得コード]
# devtools::install_github("pontsuyu/statcastr") library(statcastr) # statcastデータの取得 # dat17 <- scrape_statcast("2017-04-02", "2017-10-01") # dat18 <- scrape_statcast("2018-03-29", "2018-09-30") # dat19 <- scrape_statcast("2019-03-28", "2019-09-29") # dat <- rbind(dat17, dat18, dat19) # saveRDS(dat, "dat17to19.rds") dat <- readRDS("dat17to19.rds") glimpse(dat) # 球場情報の取得 gd <- dat$game_date %>% unique game_pks <- purrr::map_dfr(gd, get_game_pk_info) # saveRDS(game_pks, "game_pks.rds") # データを投球情報,打球位置がわかる打球に絞り、球場情報を結合 datx <- dat %>% filter(type == "X", !is.na(hc_x), !is.na(zone)) %>% mutate( hc_x = as.numeric(hc_x), hc_y = as.numeric(hc_y) ) %>% left_join(game_pks %>% select(game_pk, park=venue.name) %>% distinct())
ちょっとわかりにくいですが、頻度が異常に高い組み合わせがいくつかあるようです(図の青〇)。
これらの値がおそらく何かのグループによって振り分けられているのでしょう。
頻度上位の組み合わせ
launch_speed | launch_angle | 度数 | 累積度数 | 比率 |
---|---|---|---|---|
82.9 | -20.7 | 13515 | 13515 | 5.94 |
80.0 | 69.0 | 11926 | 25441 | 11.17 |
41.0 | -39.0 | 2950 | 28391 | 12.47 |
90.3 | -17.3 | 2425 | 30816 | 13.53 |
89.2 | 39.3 | 1316 | 32132 | 14.11 |
40.0 | -36.0 | 757 | 32889 | 14.45 |
83.0 | -21.0 | 291 | 33180 | 14.57 |
89.0 | 39.0 | 173 | 33353 | 14.65 |
90.4 | 14.6 | 146 | 33499 | 14.71 |
91.1 | 18.2 | 143 | 33642 | 14.78 |
90.2 | -13.0 | 141 | 33783 | 14.84 |
98.8 | 17.1 | 106 | 33889 | 14.88 |
91.0 | 18.0 | 92 | 33981 | 14.92 |
90.0 | -17.0 | 83 | 34064 | 14.96 |
90.0 | 15.0 | 78 | 34142 | 15.00 |
102.8 | 30.2 | 74 | 34216 | 15.03 |
93.1 | 32.0 | 50 | 34266 | 15.05 |
103.0 | 30.0 | 47 | 34313 | 15.07 |
81.0 | 65.0 | 41 | 34354 | 15.09 |
43.0 | -62.0 | 40 | 34394 | 15.11 |
99.0 | 17.0 | 39 | 34433 | 15.12 |
71.4 | 36.0 | 35 | 34468 | 15.14 |
86.0 | 67.0 | 33 | 34501 | 15.15 |
104.4 | 23.7 | 31 | 34532 | 15.17 |
94.3 | -12.1 | 18 | 34550 | 15.17 |
89.0 | 63.0 | 17 | 34567 | 15.18 |
98.4 | 18.0 | 17 | 34584 | 15.19 |
37.0 | 31.0 | 15 | 34599 | 15.20 |
NA | NA | 15 | 34614 | 15.20 |
84.0 | -20.0 | 12 | 34626 | 15.21 |
90.0 | -13.0 | 11 | 34637 | 15.21 |
Tom Tango氏やJim Albert氏の記事にあった組み合わせもありますが、それ以外の組み合わせも見受けられます。
今回はこの「TOP15と元々欠測の組み合わせ」を欠測として扱っていきます。
先述したようにどこまでを欠測と見なしていいかはわからないので一度多めに欠測にしておき、補完前後を比較し、近い値をとっていたら欠測ではなさそうと判断することにします。
欠測について
欠測をリストワイズ除去(行ごと削除)すれば、分布自体はきれいに見えてしまいます。
今回の場合、先ほどの補完済みデータを使ってもリストワイズ除去したデータを使っても、シーズンの平均打球速度・打球角度くらい大きな粒度でまとめれば、真の値が入っているデータを使えた場合と結果に大きなズレはないと思われます。
しかし例えば、
- 選手の平均打球速度、打球角度のシーズン推移を見たい
- 引っ張り方向にゴロを打ったときにヒットになる打球速度、打球角度を知りたい
など細かな値を知りたい場合は欠測の仕方によっては影響が大きくなってしまいます。
欠測メカニズムの種類
一口に欠測と言っても、様々な種類があります。
- MCAR(Missing Completely At Random):完全にランダムな欠測
- プレー内容によらず、10%の確率で欠測する場合など
- この場合、リストワイズ除去したデータのみで解析しても、推定結果は不偏になる
- MAR(Missing At Random):条件付きでランダムな欠測
- 他の変数を条件としたときに欠測がランダムになる
- ゴロのときは20%、ファールフライのときは40%の確率で欠測する場合など
- NMAR(Not Missing At Random):ランダムではない欠測
- 欠測している変数の値自体に依存して欠測する確率が変動する
- 打球角度が大きくなるにつれて欠測する確率が高くなる場合など
今回のデータの場合、どの欠測メカニズムになっていると言えるでしょうか。
データを細かく見ていきたいと思います。
手がかりを探す
打球位置
先ほどの定義で欠測させたデータを使って、まず、(野手が最初に触った)打球位置をプロットしてみます。
色の濃淡は欠測割合の高さを示しています。
打球結果
bb_type | 欠測 | 度数 | 欠測割合 |
---|---|---|---|
popup(ポップフライ) | 11940 | 27422 | 43.54 |
ground_ball(ゴロ) | 20162 | 168380 | 11.97 |
fly_ball(フライ) | 1488 | 86566 | 1.72 |
line_drive(ライナー) | 566 | 94459 | 0.60 |
events | 欠測 | 度数 | 欠測割合 |
---|---|---|---|
sac_bunt | 2019 | 2511 | 80.41 |
sac_bunt_double_play | 3 | 4 | 75.00 |
fielders_choice_out | 151 | 923 | 16.36 |
fielders_choice | 89 | 593 | 15.01 |
field_out | 25571 | 217833 | 11.74 |
force_out | 1255 | 10787 | 11.63 |
field_error | 448 | 4488 | 9.98 |
grounded_into_double_play | 772 | 10660 | 7.24 |
double_play | 75 | 1279 | 5.86 |
single | 3499 | 78708 | 4.45 |
sac_fly_double_play | 1 | 43 | 2.33 |
double | 245 | 25039 | 0.98 |
sac_fly | 28 | 3490 | 0.80 |
triple | 1 | 2413 | 0.04 |
home_run | 0 | 18045 | 0.00 |
triple_play | 0 | 12 | 0.00 |
犠牲バントはほとんど欠測で、(ここには載せていませんが)フィルダースチョイスも犠牲バントの延長で起きたものがほとんどでした。
総合すると、バッテリー間(バント)、一塁側・三塁側ファールゾーン(ファールフライ)、内野後方(ポップフライ)への打球は欠測が多そうです。
先述のTom Tango氏やJim Albert氏の記事にあったこととと同様の結果が得られました。
球場
park | 欠測 | 度数 | 欠測割合 |
---|---|---|---|
Wrigley Field | 1683 | 12399 | 13.57 |
Kauffman Stadium | 1457 | 13206 | 11.03 |
Dodger Stadium | 1293 | 11930 | 10.84 |
Citizens Bank Park | 1288 | 12341 | 10.44 |
Chase Field | 1285 | 12523 | 10.26 |
Minute Maid Park | 1214 | 12046 | 10.08 |
AT&T Park | 858 | 8773 | 9.78 |
Tropicana Field | 1097 | 11934 | 9.19 |
Oracle Park | 383 | 4198 | 9.12 |
Busch Stadium | 1142 | 12532 | 9.11 |
Guaranteed Rate Field | 1114 | 12272 | 9.08 |
Petco Park | 1083 | 11957 | 9.06 |
Fenway Park | 1146 | 12682 | 9.04 |
Safeco Field | 762 | 8431 | 9.04 |
Nationals Park | 1143 | 12655 | 9.03 |
PNC Park | 1170 | 13012 | 8.99 |
Marlins Park | 1106 | 12639 | 8.75 |
SunTrust Park | 1113 | 12763 | 8.72 |
Globe Life Park in Arlington | 1134 | 13072 | 8.68 |
Target Field | 1102 | 12849 | 8.58 |
Comerica Park | 1117 | 13249 | 8.43 |
Oriole Park at Camden Yards | 1110 | 13219 | 8.40 |
Miller Park | 986 | 12079 | 8.16 |
Citi Field | 1007 | 12360 | 8.15 |
Yankee Stadium | 974 | 12004 | 8.11 |
Rogers Centre | 1021 | 12644 | 8.07 |
Coors Field | 1057 | 13240 | 7.98 |
Angel Stadium | 654 | 8257 | 7.92 |
Great American Ball Park | 943 | 12120 | 7.78 |
T-Mobile Park | 306 | 4075 | 7.51 |
Oakland Coliseum | 880 | 12610 | 6.98 |
Progressive Field | 854 | 12263 | 6.96 |
影響ありそう。
機器の設置箇所、ホームチームの打球傾向などが絡んできそうです。
利き腕
p_throws | 欠測 | 度数 | 欠測割合 |
---|---|---|---|
L | 9533 | 102850 | 9.27 |
R | 24624 | 273978 | 8.99 |
大きな違いはなさそうです。
球速(一部)
release_speed(mph) | 欠測 | 度数 | 欠測割合 |
---|---|---|---|
85 | 1455 | 14744 | 9.87 |
88 | 1889 | 19394 | 9.74 |
87 | 1515 | 15666 | 9.67 |
83 | 1189 | 12408 | 9.58 |
79 | 620 | 6507 | 9.53 |
81 | 886 | 9332 | 9.49 |
84 | 1627 | 17321 | 9.39 |
86 | 1750 | 18812 | 9.30 |
78 | 583 | 6282 | 9.28 |
76 | 320 | 3453 | 9.27 |
89 | 1572 | 16952 | 9.27 |
82 | 1223 | 13256 | 9.23 |
90 | 2312 | 25038 | 9.23 |
80 | 916 | 10010 | 9.15 |
74 | 174 | 1908 | 9.12 |
こちらも大きな違いはなさそうです。
ゾーン
zone | 欠測 | 度数 | 欠測割合 |
---|---|---|---|
13 | 2878 | 21922 | 13.13 |
14 | 3282 | 25206 | 13.02 |
11 | 1850 | 16827 | 10.99 |
12 | 1444 | 14364 | 10.05 |
9 | 2976 | 30045 | 9.91 |
7 | 2606 | 27313 | 9.54 |
8 | 3904 | 41318 | 9.45 |
1 | 1754 | 19127 | 9.17 |
3 | 1437 | 17116 | 8.40 |
2 | 2137 | 25544 | 8.37 |
4 | 3054 | 41667 | 7.33 |
5 | 4006 | 56063 | 7.15 |
6 | 2829 | 40316 | 7.02 |
定義はコチラ↓
※投手目線
ボール球、特に低めにおける欠測が多いのが目立ちます。
球種×ゾーン(一部)
pitch_name | zone | 欠測 | 度数 | 欠測割合 |
---|---|---|---|---|
Split Finger | 14 | 85 | 413 | 20.58 |
Knuckle Curve | 13 | 100 | 538 | 18.59 |
Sinker | 14 | 243 | 1482 | 16.40 |
Cutter | 13 | 134 | 886 | 15.12 |
Slider | 13 | 418 | 2873 | 14.55 |
Sinker | 13 | 320 | 2233 | 14.33 |
Sinker | 9 | 328 | 2316 | 14.16 |
Knuckle Curve | 14 | 109 | 775 | 14.06 |
2-Seam Fastball | 14 | 260 | 1902 | 13.67 |
Curveball | 14 | 365 | 2727 | 13.38 |
Changeup | 13 | 769 | 5825 | 13.20 |
Split Finger | 13 | 124 | 961 | 12.90 |
Slider | 14 | 923 | 7171 | 12.87 |
Changeup | 14 | 630 | 4905 | 12.84 |
4-Seam Fastball | 11 | 948 | 7417 | 12.78 |
Curveball | 13 | 241 | 1895 | 12.72 |
2-Seam Fastball | 13 | 401 | 3255 | 12.32 |
Cutter | 11 | 86 | 698 | 12.32 |
Cutter | 14 | 225 | 1851 | 12.16 |
Sinker | 8 | 495 | 4114 | 12.03 |
球種よりも投球ゾーンから受ける影響が大きいようです。
打者左右
bats | 欠測 | 度数 | 欠測割合 |
---|---|---|---|
R | 20492 | 222690 | 9.20 |
L | 13665 | 154138 | 8.87 |
大きな違いはなさそうです。
以上から
- hc_x, hc_y(打球位置)
- bb_type(打球種類)
- events(打球結果)
- park(球場)
- zone(投球ゾーン)
打球結果だけでなく、場所の違い、投球位置によっても欠測確率は異なりそうだとわかりました。
欠測メカニズムはMARだと見なすことができそうです。
決定木
最後に、共変量の候補がわかったので後半に向けてシンプルに決定木を描き、欠測パターンを見ていきます。
# rとthetaはhc_x, hc_yを極座標変換したもの rp <- rpart(as.factor(flg) ~ park + events + bb_type + zone + r + theta, missingdata, method="class", control = rpart.control(cp = 0.005)) plotcp(rp) rp2 <- prune(rp, cp = 0.007) rp2 plot(as.party(rp2))
※見た目が煩雑になるのを防ぐために球場、打球内容には数字を割り振っています
打球位置の図で見られたような内野後方のポップフライやファールフライ、犠牲バントのパターンが切り分けられています。また、球場ごとの違いも出てきています。
まとめ
- 公開されているstatcastデータでは、欠測に対しなんらかの粒度でグループ平均値代入がされている
どこまでが欠測かはわからないが、確実にポップフライ・内野ファールフライ・犠牲バントでの欠測が多い
共変量については、打球速度・打球角度の結果である打球位置・打球内容のほかに球場、投球ゾーンが影響してくるとわかった
欠測を埋めることだけを考えるのであれば、欠測なし,ありでtrain, testデータに分け、GBDTで予測する形にしても問題なさそう(母集団パラメータの値を知りたいなら欠測値補完の手法を使うべき?)
後半はこれを手がかりに欠測値補完(余力があればGBDTも)していきたいと思います。
※この記事で使用するコードはこちらに載せています。