← 記事一覧へ

導入

いわゆる”ノビのあるストレート”の性能を評価したいと思います.

投球データから, ボールの垂直上方向変化に注目. 空振り率の高さを計算して, ノビについて考えます.

データについて

もとは2013年のpitch f/xデータを使っていました. しかしデータ取得元だった MLB GameDayのXMLフィードは停止し, pitchRxパッケージもCRANからアーカイブされたため, 当時のデータを取り直すことはできません.

そこで Baseball Savant (Statcast) のデータに差し替えました. 2015年以降なら同等以上の内容が取れます.

library(data.table)
library(dplyr)
library(xtable)
library(magrittr)
library(ggplot2)

SEASON <- 2024
data_file <- file.path("../../data", sprintf("statcast_%d.csv", SEASON))
if (!file.exists(data_file)) {
  stop("Statcast データがありません: ", data_file,
       "\n  Rscript data/build-statcast.R ", SEASON, " で取得してください.")
}
source("../../R/statcast.R")
dat <- fread(data_file)
# Savant は "Gausman, Kevin" の順。表示に使うので読み込み直後に直す。
dat[, player_name := flip_name(player_name)]
dat_FF <- dat %>% dplyr::filter(pitch_type == "FF", !is.na(pfx_z))

pitch f/x から Statcast への対応:

2013 (pitch f/x) 2024 (Statcast) 備考
pitcher_name player_name
des description "Called Strike" → "called_strike" など
start_speed release_speed どちらも mph
pfx_z pfx_z 単位が違う. pitch f/x は inch, Statcast は feet

pfx_z は, 重力だけを受ける質点と比べてボールがどれだけ落ちずに来たかを表す量です. バックスピンが効いているほど大きくなるので, これを「ノビ」と見なします. 以下では cm に直して扱います.

手法

空振り率は, \[\frac{\text{Swing}}{\text{FF-Pitch} -\text{Called Strike}} \] とします.

ここで,

swing :: 空振り

FF-pitch :: ストレートの投球数

Called Strike :: 見逃しストライク

です.

集計

FT_TO_CM  <- 30.48   # pfx_z は feet
MPH_TO_KMH <- 1.609344

dat_FF_swingrate <-
  dat_FF %>%
  select(player_name, release_speed, description, pfx_z) %>%
  mutate(calledStrike   = (description == "called_strike"),
         swingingStrike = description %in% c("swinging_strike",
                                             "swinging_strike_blocked")) %>%
  group_by(player_name) %>%
  dplyr::summarise(nobi   = mean(pfx_z) * FT_TO_CM,
                   speed  = mean(release_speed, na.rm = TRUE) * MPH_TO_KMH,
                   calledStrike   = sum(calledStrike),
                   swingingStrike = sum(swingingStrike, na.rm = TRUE),
                   pitches = n(),
                   .groups = "drop") %>%
  dplyr::filter(pitches > 200) %>%
  mutate(swingRate = swingingStrike / (pitches - calledStrike))

対象は 362 人 (FFを200球以上投げた投手) です.

空振り率ランキング.

dat_FF_swingrate %>%
  arrange(desc(swingRate)) %>%
  select(player_name, swingRate) %>%
  head(20) %>%
  as.data.frame %>% xtable(digits = 4) %>% print("html")
player_name swingRate
1 Mason Miller 0.2280
2 Kris Bubic 0.2051
3 Michel Otañez 0.1991
4 Kirby Yates 0.1960
5 Edwin Uceta 0.1946
6 Yimi García 0.1943
7 Jeremiah Estrada 0.1932
8 Tyler Ferguson 0.1922
9 Jhoan Duran 0.1894
10 Evan Phillips 0.1882
11 Michael Kopech 0.1860
12 Ben Joyce 0.1852
13 Garrett Crochet 0.1835
14 Jeff Hoffman 0.1833
15 Raisel Iglesias 0.1830
16 Griffin Jax 0.1825
17 Tony Santillan 0.1808
18 Cade Smith 0.1775
19 Shawn Armstrong 0.1774
20 Cole Sands 0.1769

ノビのあるストレートランキング

dat_FF_swingrate %>%
  arrange(desc(nobi)) %>%
  select(player_name, nobi) %>%
  head(20) %>%
  as.data.frame %>% xtable(digits = 4) %>% print("html")
player_name nobi
1 Alex Vesia 51.5086
2 Cristian Javier 50.7288
3 Nick Pivetta 50.5807
4 Yuki Matsui 50.3650
5 Trevor Richards 50.1007
6 Triston McKenzie 50.0974
7 Tyler Anderson 50.0270
8 Kutter Crawford 49.9990
9 Jeremiah Estrada 49.5515
10 Ryan Pepiot 49.3281
11 Justin Verlander 48.9882
12 Nestor Cortes 48.6476
13 Justin Slaten 48.6252
14 Tobias Myers 48.5932
15 Landon Knack 48.5617
16 Kaleb Ort 48.4977
17 Dylan Lee 48.4890
18 Colin Poche 48.0554
19 Blake Snell 47.6112
20 Nick Nastrini 47.5644
## 平均.
dat_FF_swingrate %>%
  dplyr::summarise(nobi = mean(nobi)) %>%
  as.data.frame %>% xtable(digit = 4) %>% print("html")
nobi
1 39.7794

プロット

縦方向の変化と空振り率との関係を, プロットしてみます.

dat_FF_swingrate %>%
  ggplot(aes(x = nobi, y = swingRate)) +
  geom_point(size = 3) + stat_smooth(method = "lm") +
  xlab("hop-up (cm)") +
  ggtitle("Hop-Up vs Swing-Rate  (Four-seam Fast)") +
  theme(plot.title = element_text(face = "bold", size = 24))

やっぱりスピードのほうが大事なんですかね? 初速と空振り率の関係を, プロットしてみます.

dat_FF_swingrate %>%
  ggplot(aes(x = speed, y = swingRate)) +
  geom_point(size = 3) + stat_smooth(method = "lm") +
  xlab("release speed (km/h)") +
  ggtitle("Speed vs Swing-Rate  (Four-seam Fast)") +
  theme(plot.title = element_text(face = "bold", size = 24))

おまけ. ストレートとノビ.

dat_FF_swingrate %>%
  ggplot(aes(x = speed, y = nobi)) +
  geom_point(size = 3) + stat_smooth(method = "lm") +
  xlab("release speed (km/h)") + ylab("hop-up (cm)") +
  ggtitle("Speed vs Hop-Up  (Four-seam Fast)") +
  theme(plot.title = element_text(face = "bold", size = 24))

相関の大きさ

見た目だけだと分からないので, 相関係数を出します.

with(dat_FF_swingrate, {
  cat("ノビ  vs 空振り率: r =", round(cor(nobi, swingRate), 3), "\n")
  cat("球速  vs 空振り率: r =", round(cor(speed, swingRate), 3), "\n")
  cat("球速  vs ノビ    : r =", round(cor(speed, nobi), 3), "\n")
})
## ノビ  vs 空振り率: r = 0.061 
## 球速  vs 空振り率: r = 0.373 
## 球速  vs ノビ    : r = 0.164

ノビとの相関は0.061と弱く, 球速との相関0.373のほうがずっと大きい.

ノビと球速を分ける

ここで止めてはいけません. ノビと球速には相関があります (r = 0.164). 速い球ほどノビも大きい傾向があるので, ノビの弱い相関は「球速の影響が漏れてきているだけ」かもしれません.

両方を同時に入れた回帰で分けます.

fit <- lm(swingRate ~ nobi + speed, data = dat_FF_swingrate)
summary(fit)$coefficients |> round(5)
##             Estimate Std. Error  t value Pr(>|t|)
## (Intercept) -0.44303    0.07289 -6.07772  0.00000
## nobi         0.00000    0.00029 -0.01119  0.99108
## speed        0.00365    0.00049  7.50721  0.00000

球速をそろえると, ノビの係数はほぼちょうど0になります (推定値-0.00000, p = 0.99).

一方の球速は係数0.00365 (標準誤差0.00049) で, はっきり効いています. km/hあたり空振り率が0.0037上がる, つまり 10km/h速いと3.7ポイント上がる計算です.

さきほどの弱い相関0.061は, ノビ自体の効果ではなく球速との相関を通じた見かけのものでした.

dat_FF_swingrate |>
  mutate(球速帯 = cut(speed, breaks = quantile(speed, 0:3/3),
                      labels = c("遅い", "普通", "速い"), include.lowest = TRUE)) |>
  ggplot(aes(x = nobi, y = swingRate)) +
  geom_point(size = 2, alpha = 0.6) +
  stat_smooth(method = "lm", formula = y ~ x) +
  facet_wrap(~ 球速帯) +
  xlab("hop-up (cm)") +
  ggtitle("球速帯を分けると, ノビの傾きは消える")

球速帯ごとに分けて見ても, ノビの傾きはほぼ平らです.

考察

この分析の範囲では, ストレートの空振りを決めているのは球速のほうで, ノビ (垂直変化量) 単独の寄与は見つかりませんでした.

ただし言えることは限られます.

  • 調整済み決定係数は0.134で, 空振り率のばらつきの大半はこの2変数では説明できていません. 変化球との組み合わせ, リリースの位置や角度, 打者との相性などが 効いているはずです.
  • 見ているのは投手ごとの平均値どうしの関係です. 同じ投手の中で 「ノビた球のほうが空振りを取れるか」は別の問いで, 1球単位のデータで 調べる必要があります.
  • 因果の向きは分かりません.
  • 対象は362人で, FFを200球以上投げた投手に限っています.