いわゆる”ノビのあるストレート”の性能を評価したいと思います.
投球データから, ボールの垂直上方向変化に注目. 空振り率の高さを計算して, ノビについて考えます.
もとは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, " で取得してください.")
}
dat <- fread(data_file)
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 | Miller, Mason | 0.2280 |
| 2 | Bubic, Kris | 0.2051 |
| 3 | Otañez, Michel | 0.1991 |
| 4 | Yates, Kirby | 0.1960 |
| 5 | Uceta, Edwin | 0.1946 |
| 6 | García, Yimi | 0.1943 |
| 7 | Estrada, Jeremiah | 0.1932 |
| 8 | Ferguson, Tyler | 0.1922 |
| 9 | Duran, Jhoan | 0.1894 |
| 10 | Phillips, Evan | 0.1882 |
| 11 | Kopech, Michael | 0.1860 |
| 12 | Joyce, Ben | 0.1852 |
| 13 | Crochet, Garrett | 0.1835 |
| 14 | Hoffman, Jeff | 0.1833 |
| 15 | Iglesias, Raisel | 0.1830 |
| 16 | Jax, Griffin | 0.1825 |
| 17 | Santillan, Tony | 0.1808 |
| 18 | Smith, Cade | 0.1775 |
| 19 | Armstrong, Shawn | 0.1774 |
| 20 | Sands, Cole | 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 | Vesia, Alex | 51.5086 |
| 2 | Javier, Cristian | 50.7288 |
| 3 | Pivetta, Nick | 50.5807 |
| 4 | Matsui, Yuki | 50.3650 |
| 5 | Richards, Trevor | 50.1007 |
| 6 | McKenzie, Triston | 50.0974 |
| 7 | Anderson, Tyler | 50.0270 |
| 8 | Crawford, Kutter | 49.9990 |
| 9 | Estrada, Jeremiah | 49.5515 |
| 10 | Pepiot, Ryan | 49.3281 |
| 11 | Verlander, Justin | 48.9882 |
| 12 | Cortes, Nestor | 48.6476 |
| 13 | Slaten, Justin | 48.6252 |
| 14 | Myers, Tobias | 48.5932 |
| 15 | Knack, Landon | 48.5617 |
| 16 | Ort, Kaleb | 48.4977 |
| 17 | Lee, Dylan | 48.4890 |
| 18 | Poche, Colin | 48.0554 |
| 19 | Snell, Blake | 47.6112 |
| 20 | Nastrini, Nick | 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("球速帯を分けると, ノビの傾きは消える")

球速帯ごとに分けて見ても, ノビの傾きはほぼ平らです.
この分析の範囲では, ストレートの空振りを決めているのは球速のほうで, ノビ (垂直変化量) 単独の寄与は見つかりませんでした.
ただし言えることは限られます.