大きな変化はボール球を振らせにくいのか?

「大きく変化する変化球は打者に見極められてしまう」と言われることがある。実際ボール変化量によって打者のO-Swing%(ボールゾーンスイング率)はどのように変動するのだろうか。

先行研究

デルタ・ベースボール・リポート4(水曜社:2021年)『変化量とRun Valueが示す本当に有効なボールとは? (八代 久通)』ではスライダーを変化タイプ別に分けてそれぞれのコース別スイング率を算出している。この研究によるとスライダーの場合、DROP=縦変化が小さい(落差が大きい)スライダーは低めのボールゾーンでスイングされやすくGLOVE=(スライダー方向への変化)が大きいと左右のボールゾーンでスイングされやすくなるという傾向が示されている。また結果は示されていないものの変化量とスイングが連動する傾向にあることが言及されており例としてストレート(フォーシーム)であれば縦変化が大きい(ライズしている)ものは高めのボールゾーンでスイングされやすくなるという傾向があるようだ。

検証

ここでは前述の先行研究に倣った方法で検証をしていく。

1.球種別に球速を3MPH(約5km/h)単位で分割し、各球速帯の平均変化量を算出する。

2.各球速帯の平均変化量と比較して、上下の変化量(落下量が少ない/多い)、左右の変化量(投手のグラブ側に変化する/投手のリリース腕側に変化する)によって投球を分類する。

変化グループについては以下のように行う。

変化グループ

各球種をこれらの変化グループ別に分けてボールゾーン率について調査する。

フォーシーム

画像6

フォーシームは球速が93MPH(約150km/h)を超えると他を突き放して縦変化がRISEのO-Swing%が大きく上昇する。O-Swing%を増加させるには球速と縦変化量の両立が必要なようだ。

シンカー(ツーシーム)

画像6

シンカー(ツーシーム)は基本的にARM-DROPがどの球速帯でもO-Swing%が高い。ARMへの動きが大きいとボール球を振らせやすいようだ。

スライダー

画像6

スライダーは基本的にDROP、もしくはGLOVE側への変化が大きいほうがNOMALよりO-Swing%が高くなる。ストレートに近い軌道であるARM-RISEはどの球速帯でもO-Swing%が最も低くボール球を振らせにくい変化量となっている。

カーブ

画像6

カーブは78MPH(約125km/h)未満の場合は縦横共に変化の大きなGLOVE-DROPに分があるものの78マイルを超えると変化タイプごとの差は小さくなる。球速が速くなればどの変化タイプでもボールゾーンスイング率が高くなることからボール球を振らせるには球速が重要な要素と言えそうだ。

チェンジアップ(スプリット)

画像6

チェンジアップは基本的にARM-DROPがボール球を振らせやすいようだ。DROPタイプは高速になればなるほどスイング率が上がる一方でARM-RISEタイプは球速が上がるほどむしろボール球を振ってもらえなくなる。ストレートに近い軌道であるGLOVE-RISEタイプはスライダーと同様にボール球を振らせにくい傾向にあるようだ。

ストレートに近い軌道より大きな変化がボールゾーンでの空振りを誘う

巷では「大きな変化は打者に見極められてボール球を振ってもらえない」と言われることがあるがデータではむしろ逆の傾向があらわれている。各球種でO-Swing%が高い組み合わせは球速は高速かつその球種の動く方向(フォーシームならRISE,スライダーならGLOVE-DROP,シンカーならARM,チェンジアップならDROP)への変化が大きいものだ。

Rのコード

#dfには2017-2020のstatcastのデータが入っている

df <- df %>%
 select(game_year,game_pk,at_bat_number,pitch_number,pitcher,pitcher_name,player_name,
        balls,strikes,plate_x,plate_z,zone,launch_speed,launch_angle,
        woba_value,pitch_type,pfx_x,pfx_z,release_speed,estimated_woba_using_speedangle,description,type,delta_run_exp,p_throws,stand)

df <- df %>%
 arrange(game_pk,at_bat_number,pitch_number)

df <- df %>%
 mutate(count = paste(balls,"-",strikes),
        lead_count = lead(count),
        pitch_result = case_when(
          description == "swinging_strike" ~ "strike",
          description == "swinging_strike_blocked" ~ "strike",
          description == "called_strike" ~ "strike",
          description == "foul_tip" ~ "strike",
          description == "bunt_foul_tip" ~ "strike",
          description == "foul" ~ "foul",
          description == "foul_bunt" ~ "strike",
          description == "ball" ~ "ball",
          description == "blocked_ball" ~ "ball",
          description == "pitchout" ~ "ball",
          description == "hit_by_pitch" ~ "HBP",
          type == "X" ~ "X"),
        plate_x_inches = ifelse(stand == "R",12 * plate_x,-12 * plate_x),
        pfx_x = ifelse(p_throws == "R",pfx_x,-pfx_x),
        plate_z_inches = 12 * (plate_z),
        pitch_type = case_when(
          pitch_type == "FF" ~ "FF", 
          pitch_type == "FT" | pitch_type == "SI" ~ "SI",
          pitch_type == "SL"  ~ "SL",
          pitch_type == "FC"  ~ "FC",
          pitch_type == "CU" | pitch_type == "KC" ~ "CU",
          pitch_type == "CH" | pitch_type == "FS" ~ "CH"),
        strike_count = ifelse(strikes == 2,"2strikes","non2strikes"),
        throw_stand = ifelse(p_throws == "R" & stand == "R" | p_throws == "L" & stand == "L" ,"same","difference"),
        swing_denom = case_when( 
          description == "swinging_strike" ~ 1,
          description == "swinging_strike_blocked" ~ 1,
          description == "foul_tip" ~ 1,
          description == "foul" ~ 1,
          description == "hit_into_play" ~ 1,
          description == "hit_into_play_score" ~ 1,
          description == "hit_into_play_no_out" ~ 1,
          TRUE ~ 0),
        swstr = case_when( 
          description == "swinging_strike" ~ 1,
          description == "swinging_strike_blocked" ~ 1,
          description == "foul_tip" ~ 1,
          TRUE ~ 0),
        contact = case_when( 
          description == "foul" ~ 1,
          description == "hit_into_play" ~ 1,
          description == "hit_into_play_score" ~ 1,
          description == "hit_into_play_no_out" ~ 1,
          TRUE ~ 0),
        ball_zone = ifelse(zone == 11 | zone == 12 | zone == 13 | zone == 14 ,1,0),
        strike_zone = ifelse(ball_zone == 0,1,0),
        ball_contact = ifelse(ball_zone == 1 & contact == 1 ,1,0),
        strike_contact = ifelse(strike_zone == 1 & contact == 1 ,1,0),
        ball_swing_denom = case_when( 
          ball_zone == 1 & description == "swinging_strike" ~ 1,
          ball_zone == 1 & description == "swinging_strike_blocked" ~ 1,
          ball_zone == 1 & description == "foul_tip" ~ 1,
          ball_zone == 1 & description == "foul" ~ 1,
          ball_zone == 1 & description == "hit_into_play" ~ 1,
          ball_zone == 1 & description == "hit_into_play_score" ~ 1,
          ball_zone == 1 & description == "hit_into_play_no_out" ~ 1,
          TRUE ~ 0),
        strike_swing_denom = case_when( 
          strike_zone == 1 & description == "swinging_strike" ~ 1,
          strike_zone == 1 & description == "swinging_strike_blocked" ~ 1,
          strike_zone == 1 & description == "foul_tip" ~ 1,
          strike_zone == 1 & description == "foul" ~ 1,
          strike_zone == 1 & description == "hit_into_play" ~ 1,
          strike_zone == 1 & description == "hit_into_play_score" ~ 1,
          strike_zone == 1 & description == "hit_into_play_no_out" ~ 1,
          TRUE ~ 0),
        strike_denom = case_when( 
          description == "swinging_strike" ~ 1,
          description == "swinging_strike_blocked" ~ 1,
          description == "foul_tip" ~ 1,
          description == "called_strike" ~ 1,
          TRUE ~ 0),
        called_strike = ifelse(description == "called_strike",1,0),
        PutAway_denom = ifelse(strikes == 2,1,0),
        PutAway = case_when( 
          strikes==2 & description == "swinging_strike" ~ 1,
          strikes==2 & description == "swinging_strike_blocked" ~ 1,
          strikes==2 & description == "foul_tip" ~ 1,
          strikes==2 & description == "called_strike" ~ 1,
          TRUE ~ 0),
        batted_denom = ifelse(type == "X",1,0),
        sweet_spot = ifelse(type == "X" & launch_angle >=8 & launch_angle <= 32,1,0),
        hard_hit = ifelse(type == "X" & launch_speed >= 95 ,1,0))

count <- c("0 - 0","0 - 1","0 - 2","1 - 0","1 - 1","1 - 2","2 - 0","2 - 1","2 - 2","3 - 0","3 - 1","3 - 2")
count_wOBAvalue <- c(0.331,0.281,0.207,0.374,0.313,0.233,0.444,0.372,0.282,0.588,0.488,0.387)

Count_wOBAvalue <- data.frame(count = count ,count_wOBAvalue = count_wOBAvalue)

df <- left_join(df,Count_wOBAvalue)

lead_Count_wOBAvalue <- Count_wOBAvalue %>%
 rename(lead_count = count,
        lead_count_wOBAvalue = count_wOBAvalue)

df <- left_join(df,lead_Count_wOBAvalue)

df <- df %>%
 mutate(PV_value = case_when(
   strikes == 2 & pitch_result == "strike" ~ (0 - count_wOBAvalue)/1.15 ,
   balls == 3 & pitch_result == "ball" ~ (0.7 - count_wOBAvalue)/1.15,
   description == "hit_by_pitch" ~ (0.7 -count_wOBAvalue)/1.15,
   pitch_result == "strike" ~ (lead_count_wOBAvalue - count_wOBAvalue)/1.15,
   pitch_result == "ball" ~ (lead_count_wOBAvalue - count_wOBAvalue)/1.15,
   pitch_result == "foul" ~ (lead_count_wOBAvalue - count_wOBAvalue)/1.15,
   pitch_result == "X" ~ (woba_value - count_wOBAvalue)/1.15))

Group <- seq(72,99,3)

df$velocity_bin <- with(df,cut(release_speed,Group))

pitch_type <- df %>%
 group_by(pitch_type,velocity_bin)%>%
 dplyr::summarise(mean_x = mean(pfx_x,na.rm=TRUE),
                  mean_z = mean(pfx_z,na.rm=TRUE))

df <- left_join(df,pitch_type)

df <- df %>%
 mutate(
   h_mov = case_when(
     pfx_x <= mean_x +0.25 & pfx_x >= mean_x - 0.25 ~ "NOMAL",
     pfx_x > mean_x + 0.25 ~ "GLOVE",
     pfx_x < mean_x - 0.25 ~ "ARM",
     TRUE ~ "ELSE"),
   v_mov = case_when(
     pfx_z <= mean_z + 0.25 & pfx_z >= mean_z - 0.25 ~ "NOMAL",
     pfx_z > mean_z + 0.25 ~ "RISE",
     pfx_z < mean_z - 0.25 ~ "DROP",
     TRUE ~ "ELSE"),
   move_type = paste(h_mov,"-",v_mov))


move_type_RV <- df %>%
 group_by(velocity_bin,pitch_type,move_type)%>%
 dplyr::summarise(N=n(),
                  RV_100 = round(mean(delta_run_exp,na.rm=TRUE)*100,2),
                  PV_100 = round(-mean(PV_value,na.rm=TRUE)*100,2),
                  mean_velo = round(mean(release_speed,na.rm=TRUE),1),
                  mean_pfx_x = round(mean(pfx_x,na.rm=TRUE),1),
                  mean_pfx_z = round(mean(pfx_z,na.rm=TRUE),1),
                  Ball_zone = sum(ball_zone ,na.rm = TRUE),
                  sum_called_strike = sum(called_strike ,na.rm=TRUE),
                  Strike = sum(strike_denom,na.rm=TRUE),
                  sum_Strike_Zone = sum(strike_zone ,na.rm=TRUE),
                  Swing = sum(swing_denom,na.rm =TRUE),
                  O_Swing = sum(ball_swing_denom,na.rm =TRUE),
                  Z_Swing = sum(strike_swing_denom,na.rm=TRUE),
                  Zonepct = round(sum_Strike_Zone/N*100,1),
                  called_strikepct = round(sum_called_strike/N*100,1),
                  Strikepct = round(Strike/N*100,1),
                  Swingpct = round(Swing / N*100,1),
                  O_Swingpct = round(O_Swing / Ball_zone*100,1),
                  Z_Swingpct = round(Z_Swing / sum_Strike_Zone*100,1),
                  Contactpct = round(sum(contact,na.rm=TRUE)/Swing*100,1),
                  O_Contactpct = round(sum(ball_contact,na.rm=TRUE)/O_Swing*100,1),
                  Z_Contactpct = round(sum(strike_contact,na.rm=TRUE)/Z_Swing*100,1),
                  SwStrpct = round(sum(swstr,na.rm=TRUE)/N*100,1),
                  PutAwaypct = round(sum(PutAway,na.rm=TRUE)/sum(PutAway_denom,na.rm=TRUE)*100,1),
                  HardHitpct = round(sum(hard_hit,na.rm=TRUE)/sum(batted_denom)*100,1),
                  SweetSpotpct = round(sum(sweet_spot,na.rm=TRUE)/sum(batted_denom)*100,1))%>%
 select(velocity_bin,pitch_type,move_type,mean_velo,mean_pfx_x,mean_pfx_z,
        N,Zonepct,Strikepct,O_Swingpct,Z_Swingpct,Contactpct,O_Contactpct,Z_Contactpct,SwStrpct,
        HardHitpct,SweetSpotpct,PutAwaypct,RV_100,PV_100)%>%
 filter(N >= 300)

write_csv(move_type_RV,"mobetype_RV.csv")

pitch_type_O_Swing <- move_type_RV %>%
 filter(pitch_type == "FF")%>%
 filter(velocity_bin != "NA",velocity_bin != "(72,75]",velocity_bin != "(81,84]")%>%
 filter(move_type == "NOMAL - NOMAL" | move_type == "ARM - RISE"| move_type == "ARM - DROP"|
        move_type == "GLOVE - RISE" |  move_type == "GLOVE - DROP")%>%
 mutate(move_type = factor(move_type))

ggplot(pitch_type_O_Swing ,aes(x=velocity_bin,y=O_Swingpct,colour=move_type,group=move_type,shape=move_type))+
 geom_line() +
 geom_point(size=4)+
 labs(title = "FF O-swing%",subtitle = "MLB 17-20",x="Velocity bin",y="O-Swing%",colour="move_type",
      caption = "source:statcast")

この記事が気に入ったらサポートをしてみませんか?