セイバーメトリクスの視点による失点しにくい(打たれにくい)カーブ
カーブは野球の歴史の中でも古くからある変化球だ。その使い方は多種多様で縦の変化の大きなドロップ、緩急をつけるために遅くするスローカーブ、緩急ではなく高速化させたパワーカーブなど様々だ。そんな多種多様なカーブで失点しにくいカーブとは何かをセイバーメトリクスの視点から探る。
xwOBA
まずはxwOBAを見ていく。xwOBAとは三振や四死球と言った非打球結果については実際のイベントの得点価値を打球については打球の速度と角度から推定した得点価値を基に加重した指標で各イベントに得点に基づいた加重を行う精度の高い出塁率(OPSと考えても良い)のようなものだ。投手にとっては低ければ低いほど失点のリスクが低いことを意味する。
球速は5km/h、変化量は縦横7.5cm(約ボール1個分)で区切りそれぞれの球速・変化量のxwOBAを算出した。2015-2020年のMLBのカーブのxwOBAの平均値である.256を中間値としてカラースケールを行っている。値が低い(失点リスクが低い)ほど青色が濃く値が高い(失点リスクが高い)ほど赤色が濃くなるよう調整している。
まず目につくのは球速が速ければ速いほどどの変化量でもxwOBAの失点リスクが下がる点だ。特に130km/hを超えたあたりから顕著で135km/hを超えるとどの変化量でも失点リスクが低い。一方で球速の遅い130km/h未満のカーブは縦の変化量か横の変化量が50cmを超えるようなよほど変化の大きいカーブでない限り失点のリスクが平均的なカーブより高い。カーブで失点を防ぐには緩急や変化量よりも一定の速度までの高速化が重要と言えそうだ。
xwOBAcon
ここからはなぜ球速が上がるとカーブの失点リスクが低下していくのかを見ていくことにする。まずは打球の失点リスクについて見ていく。ここで使うのはxwOBAconという指標だ。さきほどのxwOBAを打球に限定したもので打球の速度と角度から打球の失点リスクを推定したものだ。
先程と同じく球速は5km/h、変化量は縦横7.5cm(約ボール1個分)で区切りそれぞれの球速・変化量のxwOBAconを算出した。2015-2020年のMLBのカーブのxwOBAconの平均値である.353を中間値としてカラースケールを行っている。値が低い(失点リスクが低い)ほど青色が濃く値が高い(失点リスクが高い)ほど赤色が濃くなるよう調整している。
どの球速帯、変化量でもはっきりとした傾向はない。カーブで打球のリスク管理を行うためにやれることはあまりないのかもしれない。
Whiff%
次に打球以外の要素、ここではWhiff%を見ていく。Whiff%とは打者がスイングを試みたときにどれだけ空振りをとったかを表す指標だ。(空振り/スイング)空振りを多く取れていればリスクである打球そのものを発生させずかつストライクを稼ぐことで失点のリスクを減らすことができる。
球速は5km/h、変化量は縦横7.5cm(約ボール1個分)で区切りそれぞれの球速・変化量のWhiff%を算出した。2015-2020年のMLBのカーブのWhiff%の平均値である32.2%を中間値としてカラースケールを行っている。値が低い(空振りを取れていない)ほど青色が濃く値が高い(空振りを取れている)ほど赤色が濃くなるよう調整している。
球速115~130km/hまでは全体的に青い色が目立つ一方で130km/hを超えると全体的に赤い色が目立つようになり135km/hを超えると全体的に赤くなる。この傾向は先程のxwOBAの球速帯別の失点リスクとリンクしている。つまりカーブの球速が上がると失点リスクが低下するのは打球によるところより空振りの増加によるところが大きいと言えそうだ。
Called Strike%
カーブの球速が遅い事のメリットとして打者が手を出してこないのでカウントを稼げるということが挙げられる。実際にゾーン内の投球で見逃しストライクをとった割合を算出したところ球速が遅いほど見逃しのストライクは(ゾーン内に入れることさえできれば)取りやすいようだ。
Count Up%
しかしストライクを取る手段は見逃しストライクだけではない。見逃しであろうと空振りであろうとボール球を振らせようとストライクはストライクだ。そこで種類にかかわらずストライクを稼いだ割合を算出する。
まずはCount Up%を見ていく。Count Up%とは2ストライク未満の投球を分母にしてストライクを稼いだ割合(ファウル含む)だ。カーブのCount Up%の平均値である47%を中間値としてカラースケールをしている。
CountUp%では球速が遅い方がカウントを稼ぎやすいという傾向は見られなかった。むしろ130km/hを超えるとカウントを稼ぎやすいようだ。遅いカーブは見逃しストライクを取りやすいようだが空振りストライクを取りにくい等の他の要素で相殺してしまい結果としてストライクをそれほど稼げてはいないようだ。
Put Away%
続いてはPutAway%を見ていく。Put Away%とは2ストライクからの投球で三振を取った割合だ。カーブのPut Away%の平均値である26.9%を中間値としてカラースケールをしている。
Put Away%は球速が速い方が高くなる傾向にあるようだ。特に130km/hを超えたあたりから顕著になる。カーブで三振を取るには球速が速い方が良さそうだ。
Run Value/100
(3/19 追記)
追加でRun Value/100を見ていく。ここでのRun Valueとはカウント・走者状況を基にした得点期待値が1球単位でどれだけ変動したかを得点価値としたものだ。そのRun Valueを100球当たりのスケールにしたものがRun Value/100である。Run Valueがマイナスであればその分失点を抑止したという意味になり例えばストライクやアウトを取ればマイナス、ボールや走者を出せばプラスとなる。2015-2020年のMLBのカーブのRun Value/100の平均値である-0.06を中間値としてカラースケールを行っている。値が低い(失点リスクが低い)ほど青色が濃く値が高い(失点リスクが高い)ほど赤色が濃くなるよう調整している。
xwOBA同様、RV/100でも球速が失点を防ぐうえで重要なようだ。特に135km/hを超えて縦変化量が-22.5cm未満になると失点抑止しやすいようだ。
まとめ
・カーブの球速は失点リスクと強い関係があり速くなれば速くなるほど低くなる。
・カーブの変化量と失点リスクの関係は低く小さくても大きくてもそれほど変わらない。
・カーブの打球の失点リスクは球速・変化量ともはっきりとした低下させる傾向は見つからない。
・カーブの空振り率は球速と強い関係があり球速が速ければ速いほど空振りを多く取れる。
・見逃しストライクは遅いカーブの方が取りやすい。
・見逃しに限らないストライク全体の稼ぎやすさでは球速が速いカーブの方が取りやすい。
・球速の速いカーブの方が三振は取りやすい。
カーブで失点リスクを減らしたければ変化量をいじるより球速を上げることが近道なようだ。目安としては130km/hを超えたあたりから失点しにくくなり135km/hを超えると自由落下より沈みさえすれば(縦変化量0cm未満)どんな変化量でも失点リスクはかなり低くなるようだ。
今回の分析にはBaseball Savantから入手した2015-2020年のstatcastのデータを使用している。
以下、今回の分析に用いたRのコード。
library(tidyverse)
#dfには2015-2020年のstatcastデータが入っている。各列のtypeは変換済み
df <- df%>%
select(pitch_type,type,release_speed,game_year,pfx_x,pfx_z,pitcher,p_throws,description,
launch_angle,launch_speed,iso_value,babip_value,events,woba_value,woba_denom,estimated_woba_using_speedangle,
zone,strikes)
#xwOBAvalue,cmに変換したボール変化量,球速をkm/hに設定、打球タイプのフラッグを作る
df <- df %>%
mutate(xwoba_value = ifelse(type == "X",estimated_woba_using_speedangle,woba_value),
pfx_x_r_cm = 30.48 * (pfx_x),
pfx_z_cm = 30.48 * (pfx_z),
velocity = 1.609 * (release_speed),
GB_FL = ifelse(launch_angle < 10 ,1,0),
LD_FL = ifelse(launch_angle >= 10 & launch_angle < 25 ,1,0),
FB_FL = ifelse(launch_angle >= 25 & launch_angle < 50 ,1,0),
PU_FL = ifelse(launch_angle >=50 ,1,0),
BB_type = case_when(
launch_angle < 10 ~ "GB",
launch_angle >= 10 & launch_angle < 25 ~ "LD",
launch_angle >= 25 & launch_angle < 50 ~ "FB",
launch_angle >=50 ~ "PU"),
OBP_value = case_when(
events == "null" ~ "0",
events == "single" ~ "1",
events == "double" ~ "1",
events == "triple" ~ "1",
events == "home_run" ~ "1",
events == "force_out" ~ "0",
events == "field_out" ~ "0",
events == "sac_fly" ~ "0",
events == "grounded_into_double_play" ~ "0",
events == "field_error" ~ "1",
events == "fielders_choice_out" ~ "0",
events == "fielders_choice" ~ "1",
events == "hit_by_pitch" ~ "1",
events == "walk" ~ "1",
events == "strikeout" ~ "0",
events == "strikeout_double_play" ~ "0",
events == "double_play" ~ "0",
events == "sac_bunt" ~ "0"),
OBP_value = as.double(OBP_value),
Zone_pitch = ifelse(zone == 1 | zone ==2 | zone ==3 | zone == 4 | zone ==5 | zone == 6 | zone == 7 |zone == 8 | zone == 9 ,1,0),
called_strike_FL = ifelse(description == "called_strike" ,1,0),
strike_FL = ifelse(type == "S",1,0))
#右投手と左投手の値を揃える
df <- df %>%
mutate(pfx_x_l_cm = -1 * (pfx_x_r_cm))
df <- df %>%
mutate(pfx_x_cm = ifelse(p_throws == "R",pfx_x_r_cm,pfx_x_l_cm))
Group <- seq(-75 , 75 , 7.5)
Group_2 <- seq(0,160,5)
df$pfx_z_cm_bin <- with(df, cut(pfx_z_cm, Group))
df$pfx_x_cm_bin <- with(df, cut(pfx_x_cm, Group))
df$velocity_bin <- with(df, cut(velocity, Group_2))
henka_xwOBA <- df %>%
group_by(velocity_bin,pfx_z_cm_bin,pfx_x_cm_bin) %>%
filter(pitch_type == "CU" | pitch_type == "KC"| pitch_type == "CS") %>%
dplyr::summarise(xwOBA_value = sum(xwoba_value, na.rm = TRUE),
wOBA_denom = sum(woba_denom, na.rm = TRUE),
xwOBA = xwOBA_value / wOBA_denom,
OBP = sum(OBP_value, na.rm = TRUE) / wOBA_denom,
ISO = mean(iso_value, na.rm = TRUE))
henka_xwOBAcon <- df %>%
group_by(velocity_bin,pfx_z_cm_bin,pfx_x_cm_bin) %>%
filter(type == "X",pitch_type == "CU" | pitch_type == "KC"| pitch_type == "CS") %>%
dplyr::summarise(xwOBAcon_value = sum(xwoba_value, na.rm = TRUE),
wOBAcon_denom = sum(woba_denom, na.rm = TRUE),
xwOBAcon = xwOBAcon_value / wOBAcon_denom,
GB_pct = sum(GB_FL, na.rm = TRUE) / wOBAcon_denom *100,
LD_pct = sum(LD_FL, na.rm = TRUE) / wOBAcon_denom *100,
FB_pct = sum(FB_FL, na.rm = TRUE) / wOBAcon_denom *100,
PU_pct = sum(PU_FL, na.rm = TRUE) / wOBAcon_denom *100,
ISOcon = mean(iso_value, na.rm = TRUE),
OBPcon = sum(OBP_value, na.rm = TRUE) / wOBAcon_denom,
mean_velo = mean(launch_speed, na.rm = TRUE)*1.609)
#スウィングが1の列を作る
df<- df %>% mutate(swing_denom = case_when(
description == "swinging_strike" ~ "1",
description == "swinging_strike_blocked" ~ "1",
description == "called_strike" ~ "0",
description == "foul_tip" ~ "1",
description == "bunt_foul_tip" ~ "0",
description == "foul" ~ "1",
description == "foul_bunt" ~ "0",
description == "ball" ~ "0",
description == "blocked_ball" ~ "0",
description == "pitchout" ~ "0",
description == "hit_by_pitch" ~ "0",
description == "hit_into_play" ~ "1",
description == "hit_into_play_score" ~ "1",
description == "hit_into_play_no_out" ~ "1",
description == "missed_bunt" ~ "0"),
swing_denom = as.double(swing_denom))
#空振りが1の列を作る
df<- df %>% mutate(swing_miss = case_when(
description == "swinging_strike" ~ "1",
description == "swinging_strike_blocked" ~ "1",
description == "called_strike" ~ "0",
description == "foul_tip" ~ "1",
description == "bunt_foul_tip" ~ "0",
description == "foul" ~ "0",
description == "foul_bunt" ~ "0",
description == "ball" ~ "0",
description == "blocked_ball" ~ "0",
description == "pitchout" ~ "0",
description == "hit_by_pitch" ~ "0",
description == "hit_into_play" ~ "0",
description == "hit_into_play_score" ~ "0",
description == "hit_into_play_no_out" ~ "0",
description == "missed_bunt" ~ "0"),
swing_miss = as.double(swing_miss))
#ストライクが1の列を作る
df<- df %>% mutate(Putaway = case_when(
description == "swinging_strike" ~ "1",
description == "swinging_strike_blocked" ~ "1",
description == "called_strike" ~ "1",
description == "foul_tip" ~ "1",
description == "bunt_foul_tip" ~ "1",
description == "foul" ~ "0",
description == "foul_bunt" ~ "0",
description == "ball" ~ "0",
description == "blocked_ball" ~ "0",
description == "pitchout" ~ "0",
description == "hit_by_pitch" ~ "0",
description == "hit_into_play" ~ "0",
description == "hit_into_play_score" ~ "0",
description == "hit_into_play_no_out" ~ "0",
description == "missed_bunt" ~ "0"),
Putaway = as.double(Putaway))
#Whiff%,Zone_strike%を計算
henka_pitch <- df %>%
group_by(velocity_bin,pfx_z_cm_bin,pfx_x_cm_bin) %>%
filter(pitch_type == "CU" | pitch_type == "KC"| pitch_type == "CS") %>%
dplyr::summarise(Swing = sum(swing_denom, na.rm = TRUE),
Swing_miss = sum(swing_miss, na.rm = TRUE),
Whiff_pct = Swing_miss / Swing *100)
#Count_UPを計算
henka_count <- df %>%
group_by(velocity_bin,pfx_z_cm_bin,pfx_x_cm_bin) %>%
filter(strikes != 2 & pitch_type == "CU" | pitch_type == "KC"| pitch_type == "CS") %>%
dplyr::summarise(non_strikes2_denom = n(),
CountUp_pct = sum(strike_FL, na.rm = TRUE) / non_strikes2_denom * 100)
#Put Away%を計算
henka_Putaway <- df %>%
#group_by(velocity_bin,pfx_z_cm_bin,pfx_x_cm_bin) %>%
filter(strikes == 2 & pitch_type == "CU" | pitch_type == "KC"| pitch_type == "CS") %>%
dplyr::summarise(strikes2_denom = n(),
PutAway_pct = sum(Putaway, na.rm = TRUE) / strikes2_denom * 100)
henka <- left_join(henka_xwOBA,henka_xwOBAcon) %>%
select(velocity_bin,pfx_z_cm_bin,pfx_x_cm_bin,wOBA_denom,xwOBA,OBP,ISO,wOBAcon_denom,xwOBAcon,mean_velo,GB_pct,LD_pct,FB_pct,PU_pct,OBPcon,ISOcon)
henka <- left_join(henka,henka_pitch) %>%
select(velocity_bin,pfx_z_cm_bin,pfx_x_cm_bin,wOBA_denom,xwOBA,OBP,ISO,
wOBAcon_denom,xwOBAcon,mean_velo,GB_pct,LD_pct,FB_pct,PU_pct,OBPcon,ISOcon,
Swing,Whiff_pct)
henka <- left_join(henka,henka_count) %>%
select(velocity_bin,pfx_z_cm_bin,pfx_x_cm_bin,wOBA_denom,xwOBA,OBP,ISO,
wOBAcon_denom,xwOBAcon,mean_velo,GB_pct,LD_pct,FB_pct,PU_pct,OBPcon,ISOcon,
Swing,Whiff_pct,
non_strikes2_denom,CountUp_pct)
henka <- left_join(henka,henka_Putaway) %>%
select(velocity_bin,pfx_z_cm_bin,pfx_x_cm_bin,wOBA_denom,xwOBA,OBP,ISO,
wOBAcon_denom,xwOBAcon,mean_velo,GB_pct,LD_pct,FB_pct,PU_pct,OBPcon,ISOcon,
Swing,Whiff_pct,
non_strikes2_denom,CountUp_pct,
strikes2_denom,PutAway_pct)
write_csv(henka,"球速と変化量(カーブ).csv")
この記事が気に入ったらサポートをしてみませんか?