statcastデータにおけるlaunch_speed, launch_angleの欠測値について(前編)

※この記事で使用するコードはこちらに載せています。

先日、こちらのnoteを拝見しまして、

note.com

1. 打球速度(launch_speed)、打球角度(launch_angle)の欠測の扱い
2. 欠測値補完方法の違いによる打球速度、打球角度の変化

を自分でも検証したくなったので、詳しく見てみました。

※前編で1.、後半で2.を扱っていきます。



データを見る前に


statcastデータの欠測値についてTom Tango氏、Jim Albert氏の記事によると

Tangotiger Blog

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())

f:id:tsuyu_pon:20200402133601j:plain

ちょっとわかりにくいですが、頻度が異常に高い組み合わせがいくつかあるようです(図の青〇)。

これらの値がおそらく何かのグループによって振り分けられているのでしょう。




頻度上位の組み合わせ


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と元々欠測の組み合わせ」を欠測として扱っていきます。

先述したようにどこまでを欠測と見なしていいかはわからないので一度多めに欠測にしておき、補完前後を比較し、近い値をとっていたら欠測ではなさそうと判断することにします。



欠測について


欠測をリストワイズ除去(行ごと削除)すれば、分布自体はきれいに見えてしまいます。

f:id:tsuyu_pon:20200402140409p:plain

今回の場合、先ほどの補完済みデータを使ってもリストワイズ除去したデータを使っても、シーズンの平均打球速度・打球角度くらい大きな粒度でまとめれば、真の値が入っているデータを使えた場合と結果に大きなズレはないと思われます。


しかし例えば、

  • 選手の平均打球速度、打球角度のシーズン推移を見たい
  • 引っ張り方向にゴロを打ったときにヒットになる打球速度、打球角度を知りたい

など細かな値を知りたい場合は欠測の仕方によっては影響が大きくなってしまいます。




欠測メカニズムの種類


一口に欠測と言っても、様々な種類があります。

  • MCAR(Missing Completely At Random):完全にランダムな欠測
    • プレー内容によらず、10%の確率で欠測する場合など
    • この場合、リストワイズ除去したデータのみで解析しても、推定結果は不偏になる


  • MAR(Missing At Random):条件付きでランダムな欠測
    • 他の変数を条件としたときに欠測がランダムになる
    • ゴロのときは20%、ファールフライのときは40%の確率で欠測する場合など


  • NMAR(Not Missing At Random):ランダムではない欠測
    • 欠測している変数の値自体に依存して欠測する確率が変動する
    • 打球角度が大きくなるにつれて欠測する確率が高くなる場合など


今回のデータの場合、どの欠測メカニズムになっていると言えるでしょうか。

データを細かく見ていきたいと思います。




手がかりを探す

打球位置

先ほどの定義で欠測させたデータを使って、まず、(野手が最初に触った)打球位置をプロットしてみます。

色の濃淡は欠測割合の高さを示しています。

f:id:tsuyu_pon:20200402165009p:plain


打球結果


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

定義はコチラ↓
※投手目線

f:id:tsuyu_pon:20200402183436p:plain

ボール球、特に低めにおける欠測が多いのが目立ちます。




球種×ゾーン(一部)


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))


f:id:tsuyu_pon:20200403004623p:plain

※見た目が煩雑になるのを防ぐために球場、打球内容には数字を割り振っています


打球位置の図で見られたような内野後方のポップフライやファールフライ、犠牲バントのパターンが切り分けられています。また、球場ごとの違いも出てきています。



まとめ

  • 公開されているstatcastデータでは、欠測に対しなんらかの粒度でグループ平均値代入がされている
  • どこまでが欠測かはわからないが、確実にポップフライ・内野ファールフライ・犠牲バントでの欠測が多い

  • 共変量については、打球速度・打球角度の結果である打球位置・打球内容のほかに球場、投球ゾーンが影響してくるとわかった

  • 欠測を埋めることだけを考えるのであれば、欠測なし,ありでtrain, testデータに分け、GBDTで予測する形にしても問題なさそう(母集団パラメータの値を知りたいなら欠測値補完の手法を使うべき?)



後半はこれを手がかりに欠測値補完(余力があればGBDTも)していきたいと思います。



※この記事で使用するコードはこちらに載せています。