framingをGAMで考える
はじめに
これはスポーツアナリティクス Advent Calendar 2019 21日目の記事です。
これを読んでくださる方はSports Analyst Meetup(通称spoana)に来てくださった方が多いんですかね?
spoanaは来年も引き続き開催していきますので、来たことのある方もそうでない方もぜひご参加ください。
また、会場提供してくださる方も募集しております。よろしくお願いいたします。
framing(フレーミング)とは
さて、本題に戻ります。catcher framingの話です。
知らない方もいると思いますので、簡単に説明します。
framing(以下フレーミング)とは、ストライクゾーンギリギリの際どいボールを、捕球動作や捕球体勢などを工夫することによって審判に「ストライク」と判定させる捕球技術。
…ということを知ったうえで百聞は一見に如かず、コチラをご覧ください。
【MLB】フレーミング・世界トップ8 ~MLB Best Framing~
なんとなくご理解いただけましたでしょうか?
そもそもframing(frame)には「枠」「でっち上げる」という意味が含まれていますし、ぴったりな言葉ですよね。 *1
実際、先日DELTAさんが今年のNPB捕手を評価した際も評価指標としてフレーミングを使っていました。
このテーマを選んだ理由
捕手が球審の意思決定に影響を与えるかを考えるなんてワクワクするじゃん!と思って、フレーミングの動画をいろいろ見てたのですが、捕手単体というよりはバッテリーが球審に与える影響と考えた方が自然かなと思いました。
ストライクの定義は
「打者の肩の上部とユニフォームのズボンの上部との中間点に引いた水平のラインを上限とし、ひざ頭の下部のラインを下限とする本塁上の空間をいう。このストライクゾーンは打者が投球を打つための姿勢で決定されるべきである。」
球審はこの空間を一部分でも通過した(と判定した)投球をストライクと宣告する(べきな)ので、捕手の捕球には影響を受けないはず。にも関わらずフレーミングの影響は"ある"という先行研究が多いということは捕球だけでなく、投球軌道にも少なからず影響を受けているのではないか?
…ということで
フレーミングはバッテリーの共同作業か
という視点で分析していきたいと思います。
※投手と捕手、球審をそれぞれランダム効果として扱う研究はありましたが、投球軌道を加味している研究は見つからなかったので、その観点から分析してみます。
使用したデータ
2019年のMLBにおけるStatcastのデータを使います。
MLBの詳細なデータはNPBと違い、公開されていて簡単にアクセス可能になっています。
スクレイピングには私が作成した
statcastrパッケージを使いました。
※install.packagesではなく、remotes::install_githubでパッケージをインストールしてください。
remotes::install_github("pontsuyu/statcastr")
分析手順
コチラに載っているフレーミングの分析手順を参考にしています。
ベースを通過したときの座標・投手の利き腕・打者位置を使って、フレーミングできたかをGAMでモデリング
1.の変数に、カウントやボールの変化量、球速(リリース時)などを加え同様にモデリング
検索でヒットした研究では、分析手法としてGLMM、GAM、GAMM、階層ベイズを使っているものが多かったですが、今回は自分の勉強も兼ねてGA2Mを使ってみます。
(参考記事)
3. 2.で得られた推定確率を使ってフレーミングした球に対し、その価値を以下の指標で評価してみます。
集計
ストライクゾーン
2019年における各ゾーンのストライク判定確率はこんな感じでした。(捕手目線、単位はcm)
利き腕と打者位置の左右が違うと外角が少し広いようです。
クロスファイヤーの影響ですかね。おもしろい。
フレーミング
水色がストライクゾーン外なのにストライク判定された球です。
(描いたストライクゾーンは平均なので、一部、内に入って見えるものもあります)
ちなみに前田投手、田中投手、ダルビッシュ投手はこんな感じ。
キャッチャー
フレーミング数降順に並べると
team | catcher_name | framing | catch_N | framing/N |
---|---|---|---|---|
MIL | Yasmani Grandal | 475 | 8347 | 5.69 |
PHI | J.T. Realmuto | 393 | 8737 | 4.50 |
SD | Austin Hedges | 382 | 6047 | 6.32 |
BOS | Christian Vazquez | 355 | 7598 | 4.67 |
MIA | Jorge Alfaro | 355 | 7456 | 4.76 |
CLE | Roberto Perez | 337 | 7138 | 4.72 |
TOR | Danny Jansen | 333 | 6691 | 4.98 |
NYM | Wilson Ramos | 331 | 8010 | 4.13 |
SF | Buster Posey | 318 | 6398 | 4.97 |
CHC | Willson Contreras | 313 | 6504 | 4.81 |
ATL | Tyler Flowers | 303 | 5289 | 5.73 |
STL | Yadier Molina | 288 | 7052 | 4.08 |
ARI | Carson Kelly | 279 | 5776 | 4.83 |
CIN | Tucker Barnhart | 276 | 5599 | 4.93 |
SEA | Omar Narvaez | 274 | 6238 | 4.39 |
WSH | Yan Gomes | 272 | 5777 | 4.71 |
HOU | Robinson Chirinos | 261 | 6926 | 3.77 |
NYY | Gary Sanchez | 260 | 5793 | 4.49 |
COL | Tony Wolters | 237 | 6574 | 3.61 |
BAL | Pedro Severino | 234 | 5967 | 3.92 |
GrandalやRealmuto、Hedgesはさすがですね。
ここからはデータ量を絞るため、フレーミング数TOP12人に絞って分析していきます。
分析
フレーミングはご覧いただいた通り、そうそう起きることではないので、不均衡データとなります。
not framing | framing |
---|---|
81084(95.0%) | 4183(5.0%) |
そのため、upuraさんの記事を参考にダウンサンプリングして、10回モデリングし、予測結果を平均したものを最終的な推定結果としました。
1. 投球座標・投手の利き腕・打者位置を使って、フレーミングできたかをGAMでモデリング
# remotes::install_github("pontsuyu/statcastr") library(statcastr) library(tidyverse) library(mgcv) library(broom) library(ROCR) (一部省略) pred_res <- list() temp <- dat %>% filter(framing_flg==1) for(i in 1:10){ temp2 <- dat %>% filter(framing_flg==0) %>% sample_n(NROW(temp)) temp_dat <- rbind(temp, temp2) # GAM model1 <- gam(framing_flg ~ s(px) + s(pz) + s(px, pz) + p_throws + stand, data = temp_dat, family = binomial) print(summary(model1)) # モデリングに使ったデータでpredict temp_pred <- predict(model1, type = "response") pred <- prediction(c(temp_pred), temp_dat$framing_flg) perf <- performance(pred, "auc") print(paste("AUC:", unlist(perf@y.values))) # 元データでpredict test_pred <- predict(model1, newdata = dat, type = "response") pred <- prediction(c(test_pred), dat$framing_flg) perf <- performance(pred, "auc") print(paste("test AUC:", unlist(perf@y.values))) pred_res <- c(pred_res, list(test_pred)) } dat <- cbind(dat, pred1 = rowMeans(bind_cols(pred_res)))
(一部結果)
Family: binomial Link function: logit Formula: framing_flg ~ s(px) + s(pz) + s(px, pz) + p_throws + stand Parametric coefficients: Estimate Std. Error z value Pr(>|z|) (Intercept) -7.49555 0.55504 -13.505 <2e-16 *** p_throwsR 0.12156 0.08251 1.473 0.141 standR 0.03409 0.08547 0.399 0.690 --- Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1 Approximate significance of smooth terms: edf Ref.df Chi.sq p-value s(px) 8.959 8.999 109.3 <2e-16 *** s(pz) 8.958 8.999 147.2 <2e-16 *** s(px,pz) 26.999 27.000 647.9 <2e-16 *** --- Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1 R-sq.(adj) = 0.651 Deviance explained = 59.1% UBRE = -0.4221 Scale est. = 1 n = 8366 [1] "AUC: 0.941452570708859" [1] "test AUC: 0.942231714370219" Family: binomial Link function: logit Formula: framing_flg ~ s(px) + s(pz) + s(px, pz) + p_throws + stand Parametric coefficients: Estimate Std. Error z value Pr(>|z|) (Intercept) -9.73758 0.73779 -13.198 <2e-16 *** p_throwsR -0.09243 0.08781 -1.053 0.293 standR 0.07517 0.08817 0.853 0.394 --- Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1 Approximate significance of smooth terms: edf Ref.df Chi.sq p-value s(px) 8.957 8.999 127.4 <2e-16 *** s(pz) 8.974 8.999 138.5 <2e-16 *** s(px,pz) 26.988 27.000 587.7 <2e-16 *** --- Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1 R-sq.(adj) = 0.675 Deviance explained = 61.2% UBRE = -0.45028 Scale est. = 1 n = 8366 [1] "AUC: 0.94688477872454" [1] "test AUC: 0.941812682120984"
投球位置だけでほぼ説明できてますね。
利き腕、打者位置もフレーミングTOP選手に絞ってるので、関係なくなってしまったのかもしれないです。
2. 1.の変数にカウントやボールの変化量、球速(リリース時)などを加え同様にモデリング
投球位置の横方向、縦方向は交互作用を考えないと角を表現しにくいのは直感的にわかりますが、カウントやボールの変化量、球速を加えるとどこまで考える必要があるのでしょうか…。
いろいろ値を変えながら、良さそうなものを人力で探すのもいいですが、いかんせん手間がかかります。
そこで、ここではGA2Mを使って、フレーミングに効いてくる交互作用項を探索してみたいと思います。
(参考記事)
Rでも出来るようになったらしいのですが、上手くいかなかったのでPythonで実装しました。
import numpy as np import pandas as pd from interpret.glassbox import ExplainableBoostingClassifier from interpret import show r = np.arange(10) + 1 for i in r: dat = pd.read_csv("temp_dat"+str(i)+".csv") # ダウンサンプリングした10ファイル x = dat[['px','pz','henka_x','henka_z','p_throws','stand','strikes','balls','outs_when_up','release_speed']] y = dat['framing_flg'] ebm = ExplainableBoostingClassifier(random_state=42, interactions=5) ebm.fit(x, y) explanation = ebm.explain_global() show(explanation)
これだけです。
(一部結果)
こんな感じに10ファイル分、特徴重要度や特徴の値がどの範囲だと影響を与えているのかがわかります。
ストライクは増えるほどフレーミングしにくく(ストライクをとりにくく)、
ボールは増えるほどフレーミングしやすく(ストライクをとりやすく)なってます。
漸減、漸増しているのはおもしろいですね。
この結果を使って、交互作用つきの変数選択をしていきたいと思います。
正直、GA2Mにおいてこういう使い方が合ってるのかはわかりません…。
変数選択
GA2Mの結果10個を眺めてみると投球位置(px, pz)は交互作用含め必須のようです。
先ほどのGAMの結果通り、これだけで十分なほど説明できるらしい。
次点でstrikes, px*p_throws, px*stand、つまり、
- ストライクカウント
- 利き腕と投球位置x(先述のクロスファイヤーの影響?)
- 打者位置と投球位置x(打者位置ごとの外角の広さを表現?)
です。良さげです。
この結果を使ってもう一度GAMをやってみます。
pred_res <- list() temp <- dat %>% filter(framing_flg==1) for(i in 1:10){ temp2 <- dat %>% filter(framing_flg==0) %>% sample_n(NROW(temp)) temp_dat <- rbind(temp, temp2) # GAM model2 <- gam(framing_flg ~ s(px) + s(pz) + s(px, pz) + strikes + px:p_throws + px:stand, data = temp_dat, family = binomial) print(summary(model2)) # モデリングに使ったデータでpredict temp_pred <- predict(model2, type = "response") pred <- prediction(c(temp_pred), temp_dat$framing_flg) perf <- performance(pred, "auc") print(paste("AUC:", unlist(perf@y.values))) # 元データでpredict test_pred <- predict(model2, newdata = dat, type = "response") pred <- prediction(c(test_pred), dat$framing_flg) perf <- performance(pred, "auc") print(paste("test AUC:", unlist(perf@y.values))) pred_res <- c(pred_res, list(test_pred)) } dat<- cbind(dat, pred2 = rowMeans(bind_cols(pred_res)))
Family: binomial Link function: logit Formula: framing_flg ~ s(px) + s(pz) + s(px, pz) + strikes + px:p_throws + px:stand Parametric coefficients: Estimate Std. Error z value Pr(>|z|) (Intercept) -9.330056 0.859293 -10.858 < 2e-16 *** strikes -0.363723 0.053159 -6.842 7.8e-12 *** px:p_throwsL 0.704989 0.414163 1.702 0.0887 . px:p_throwsR 0.688740 0.414082 1.663 0.0963 . px:standR 0.033020 0.003843 8.592 < 2e-16 *** --- Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1 Approximate significance of smooth terms: edf Ref.df Chi.sq p-value s(px) 7.803 7.979 118.5 <2e-16 *** s(pz) 8.997 9.000 228.5 <2e-16 *** s(px,pz) 26.994 27.000 646.3 <2e-16 *** --- Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1 Rank: 49/50 R-sq.(adj) = 0.693 Deviance explained = 63.3% UBRE = -0.47998 Scale est. = 1 n = 8366 [1] "AUC: 0.95357916784517" [1] "test AUC: 0.946732555607097" Family: binomial Link function: logit Formula: framing_flg ~ s(px) + s(pz) + s(px, pz) + strikes + px:p_throws + px:stand Parametric coefficients: Estimate Std. Error z value Pr(>|z|) (Intercept) -7.474502 0.697652 -10.714 < 2e-16 *** strikes -0.329700 0.050758 -6.496 8.28e-11 *** px:p_throwsL 1.436310 0.522935 2.747 0.00602 ** px:p_throwsR 1.424914 0.522947 2.725 0.00643 ** px:standR 0.033271 0.003727 8.928 < 2e-16 *** --- Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1 Approximate significance of smooth terms: edf Ref.df Chi.sq p-value s(px) 7.155 7.774 94.19 <2e-16 *** s(pz) 8.979 8.998 133.01 <2e-16 *** s(px,pz) 27.000 27.000 556.22 <2e-16 *** --- Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1 Rank: 49/50 R-sq.(adj) = 0.662 Deviance explained = 60.3% UBRE = -0.43774 Scale est. = 1 n = 8366 [1] "AUC: 0.945024111745396" [1] "test AUC: 0.945746932789921"
AUCが先ほどより微増してますし、変数もこれで良さそうです。
この結果を使って評価していきます。
キャッチャー評価
3. 2.で得られた推定確率を使ってフレーミングした球に対し、その価値を以下の指標で評価
フレーミング得点価値はコチラに載っていたものを使用します。
カウント | フレーミング得点価値 |
---|---|
0-0 | 0.080 |
0-1 | 0.092 |
0-2 | 0.199 |
1-0 | 0.112 |
1-1 | 0.117 |
1-2 | 0.241 |
2-0 | 0.156 |
2-1 | 0.098 |
2-2 | 0.339 |
3-0 | 0.173 |
3-1 | 0.251 |
3-2 | 0.590 |
このカウントからストライクを増やしたときの防いだ失点だと思ってください。
ランキング結果
team | catcher_name | sum_framing_run_value |
---|---|---|
MIL | Yasmani Grandal | 10.763427 |
SD | Austin Hedges | 10.378748 |
PHI | J.T. Realmuto | 9.944429 |
BOS | Christian Vazquez | 9.515722 |
SF | Buster Posey | 8.485142 |
TOR | Danny Jansen | 8.471106 |
NYM | Wilson Ramos | 7.579195 |
CLE | Roberto Perez | 7.285535 |
CHC | Willson Contreras | 6.980723 |
ATL | Tyler Flowers | 6.901930 |
MIA | Jorge Alfaro | 6.850372 |
STL | Yadier Molina | 6.316730 |
先ほどの順位はこちら
team | catcher_name | framing | catch_N | framing/N |
---|---|---|---|---|
MIL | Yasmani Grandal | 475 | 8347 | 5.69 |
PHI | J.T. Realmuto | 393 | 8737 | 4.50 |
SD | Austin Hedges | 382 | 6047 | 6.32 |
BOS | Christian Vazquez | 355 | 7598 | 4.67 |
MIA | Jorge Alfaro | 355 | 7456 | 4.76 |
CLE | Roberto Perez | 337 | 7138 | 4.72 |
TOR | Danny Jansen | 333 | 6691 | 4.98 |
NYM | Wilson Ramos | 331 | 8010 | 4.13 |
SF | Buster Posey | 318 | 6398 | 4.97 |
CHC | Willson Contreras | 313 | 6504 | 4.81 |
ATL | Tyler Flowers | 303 | 5289 | 5.73 |
STL | Yadier Molina | 288 | 7052 | 4.08 |
Grandalは不動の1位ですが、多少順位が変動してますね。
ちなみにバッテリーで見るとこんな感じ。
team | pitcher_name | catcher_name | sum_framing_run_value |
---|---|---|---|
SD | Joey Lucchesi | Austin Hedges | 1.8572258 |
BOS | David Price | Christian Vazquez | 1.8165831 |
NYM | Steven Matz | Wilson Ramos | 1.7600996 |
CHC | Jon Lester | Willson Contreras | 1.7513555 |
MIL | Zach Davies | Yasmani Grandal | 1.6911975 |
SF | Madison Bumgarner | Buster Posey | 1.5669637 |
MIA | Caleb Smith | Jorge Alfaro | 1.5570445 |
PHI | Jake Arrieta | J.T. Realmuto | 1.5231500 |
TOR | Marcus Stroman | Danny Jansen | 1.4430287 |
BOS | Eduardo Rodriguez | Christian Vazquez | 1.4092936 |
まとめ
- 先行研究とはちょっと違う視点でフレーミングを評価してみました
- ダウンサンプリング + バギングをしてみました
- GA2Mを使ってGAMの変数選択をしてみました
引き続き、転職活動中です
課題
- ダウンサンプリングのサンプリング方法や回数はまだまだ改良の余地はありそう
- モデリングの速さを鑑みて、データをかなり絞ったので、絞らずやってみたい
なにか間違っているところがあればそっと教えてください。
それでは、良いお年を!!
*1:まさに心理学や経済学の世界におけるフレーミング効果(意思決定する際、情報の内容が同じであっても、問題や質問の提示方法によって結果が異なること)ですね