← 記事一覧へ

導入

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

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

データについて

もとは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("球速帯を分けると, ノビの傾きは消える")

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

考察

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

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

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