← 記事一覧へ

四球率は制球力ではない

制球が良い投手とは誰か. 四球率で並べるのが手軽ですが, 四球は結果です. 打者が振らなかったこと, 審判の判定, 際どい球を避けた配球が全部混ざっています.

測りたいのは「狙った場所に投げられたか」です. ところが狙った場所は データに入っていません. 見えるのは投げた場所だけです.

そこで, 見えない狙いを投球位置の分布から推定することにします. 同じ投手が同じ球種を同じ状況で投げた球を集めれば, その真ん中あたりが狙いで, そこからの散らばりが実行の誤差だろう, という考え方です.

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

MIN_CELL <- 100   # 1つの条件でこの球数以上あるものだけ使う
d <- fread("../../data/statcast_2024_all.csv", showProgress = FALSE)

# 追跡の外れ値を落とす. 本塁から左右3ft, 高さ-1〜6ft の外は捨てる
p <- d[!is.na(plate_x) & !is.na(plate_z) & pitch_type != "" &
       abs(plate_x) < 3 & plate_z > -1 & plate_z < 6]

707,855球を使います (追跡の外れ値4,043球を除外).

状況を揃える

カウントによって狙いは動きます. まずリーグ全体でどれだけ動くかを見ます.

p[, cnt := fifelse(strikes > balls, "投手有利",
            fifelse(balls > strikes, "打者有利", "平行"))]

shift <- p[, .(mx = mean(plate_x), mz = mean(plate_z)), by = .(cnt, stand)]
カウント区分ごとの平均投球位置 (ft. mx は捕手から見て右が正)
cnt stand mx mz
投手有利 R 0.310 2.207
平行 R 0.182 2.317
打者有利 R 0.129 2.337
投手有利 L -0.188 2.269
平行 L -0.141 2.354
打者有利 L -0.125 2.361

右打者に対して, 投手有利のカウントでは平均が高さ2.21 ft, 打者有利では2.34 ft です. 追い込むと低く, 外に投げます. 0.13 ft と幅は小さいものの, 向きは一貫しています.

この寄りを制球の悪さとして数えてはいけません. 狙って動かしているぶんです. リーグ全体の寄りを引いておきます.

p[shift, on = .(cnt, stand),
  `:=`(cx = plate_x - i.mx, cz = plate_z - i.mz)]

条件は投手 × 球種 × 打者の左右とします. この単位で100球以上あるものだけ残します.

p[, cell := paste(pitcher, pitch_type, stand)]
big <- p[, .N, by = cell][N >= MIN_CELL]$cell
p <- p[cell %in% big]

2,358条件, 529人の投手が残りました.

狙いは別の球から推定する

ここが肝心です. 同じ球で狙いを推定し, 同じ球で誤差を測ってはいけません. 必ず「自分自身に近い」値が出て, 球数が少ない投手ほど良く見えます.

球を4つに分け, 一方で狙いを推定し, もう一方で誤差を測ります.

set.seed(1)
p[, fold := sample(rep_len(1:4, .N)), by = cell]

# fit 側で狙い所を k 個推定し, eval 側の球からその最寄りまでの距離を測る
err <- function(fit, ev, k) {
  if (nrow(fit) < 5 * k || nrow(ev) < 3) return(NA_real_)
  if (k == 1) {
    ctr <- matrix(c(mean(fit$cx), mean(fit$cz)), nrow = 1)
  } else {
    km <- tryCatch(kmeans(as.matrix(fit[, .(cx, cz)]), centers = k, nstart = 5),
                   error = function(e) NULL)
    if (is.null(km)) return(NA_real_)
    ctr <- km$centers
  }
  dx <- outer(ev$cx, ctr[, 1], "-")
  dz <- outer(ev$cz, ctr[, 2], "-")
  sqrt(mean(apply(dx^2 + dz^2, 1, min)))       # 最寄りの狙いまでの二乗平均平方根
}

command <- function(k, fit_folds, ev_folds) {
  z <- p[, .(e = err(.SD[fold %in% fit_folds], .SD[fold %in% ev_folds], k), n = .N),
         by = .(pitcher, cell), .SDcols = c("cx", "cz", "fold")]
  z[!is.na(e), .(ctrl = weighted.mean(e, n), n = sum(n)), by = pitcher]
}

ctrl は狙いからの平均的なずれ (ft) です. 小さいほど制球が良いことになります.

指標として使えるかを先に確かめる

順位を出す前に, 同じ投手の別の球から作っても同じ値になるかを見ます. ならないなら, それは投手の性質ではなく, その日の偶然を測っています.

fold 1→2 で作った値と, fold 3→4 で作った値を突き合わせます.

rel <- function(k) {
  A <- command(k, 1, 2); B <- command(k, 3, 4)
  merge(A, B, by = "pitcher", suffixes = c("_a", "_b"))
}
m1 <- rel(1); m2 <- rel(2)
sb <- function(r) 2 * r / (1 + r)      # Spearman-Brown (全球を使ったときの信頼性)

狙い所を1か所とした場合, 分割標本の相関は0.592, Spearman-Brown で補正すると0.744です.

投球数を増やせば相関は上がるはずです. 上がらないなら手続きが壊れています.

curve <- rbindlist(lapply(c(200, 300, 400, 600, 800, 1000, 1400), function(thr) {
  a <- m1[n_a >= thr]; b <- m2[n_a >= thr]
  data.table(閾値 = thr,
             r = c(cor(a$ctrl_a, a$ctrl_b), cor(b$ctrl_a, b$ctrl_b)),
             狙い所 = c("1か所", "2か所"),
             n = c(nrow(a), nrow(b)))
}))

ggplot(curve, aes(x = 閾値, y = r, colour = 狙い所)) +
  geom_line() + geom_point() +
  ylim(0, 1) +
  xlab("対象にする最低投球数") + ylab("分割標本の相関") +
  ggtitle("投球数を増やすと安定する")

200球以上で0.645, 1400球以上で0.824と単調に上がります. 指標としての形はできています.

狙い所を2つにしても良くならない

投手は内と外に投げ分けます. 狙いを1か所とみなすのは乱暴で, 2か所にしたほうが実態に近いはずです. 実際, ずれの大きさは小さくなります.

狙い所を1か所とした場合と2か所とした場合
狙い所 ずれの中央値 分割標本の相関 Spearman-Brown
1か所 1.102 0.592 0.744
2か所 0.825 0.514 0.679

ずれの中央値は1.102 ft から 0.825 ft へ小さくなります. 当然です. 狙い所を増やせば, どんな投球もどれかの近くに来るからです.

問題は相関のほうです. 0.592 から0.514 へ下がります. 上の図で試した7通りの閾値のうち, 2か所の線が1か所の線を下回ったのは 7通りでした.

当てはまりは良くなったのに, 測っているものは不安定になりました. 2か所目の狙いは投手の性質ではなく, その半分の球にたまたま出た塊を拾っています. 複雑にした結果, 偶然を推定する部分が増えたわけです.

2つの指標の相関は0.856で, 順位はほぼ同じです. 同じものを測るなら, 安定するほうを使ってください. 以降は1か所で進めます.

四球率とどう違うか

pa <- d[!is.na(events) & events != "",
        .(PA = .N, BB = sum(events == "walk")), by = pitcher][PA >= 100]
pa[, BBpct := BB / PA]

zone <- p[, .(zone = mean(abs(plate_x) <= 0.83 &
                          plate_z >= sz_bot & plate_z <= sz_top)), by = pitcher]

cmp <- merge(merge(full1, pa, by = "pitcher"), zone, by = "pitcher")
ggplot(cmp, aes(x = ctrl, y = BBpct)) +
  geom_point(alpha = 0.5) +
  geom_smooth(method = "lm", se = FALSE) +
  xlab("狙いからの平均的なずれ (ft)") + ylab("四球率 (四球 / 打席)") +
  ggtitle("ずれが大きい投手ほど四球を出す")

四球率との相関は0.539です. 同じものを測ってはいませんが, 無関係でもありません. ずれの大きさで四球率の 29%ぶんが説明できます.

残りは打者の見極め, 審判, 配球の側にあります. 四球率を制球力の代わりに使うと, この部分まで投手の責任にすることになります.

ゾーン率との相関は-0.239と弱いままです. ゾーンに入れることと, 狙った場所に投げることは別です.

実際の分布を見る

指標の両端にいる投手の直球を並べます.

source("../../R/statcast.R")
pnames <- unique(p[, .(pitcher, player_name)])
pnames[, player_name := flip_name(player_name)]
ranked <- merge(full1[n >= 1000], pnames, by = "pitcher")[order(ctrl)]
pick <- ranked[c(1, .N)]

ff <- p[pitcher %in% pick$pitcher & pitch_type == "FF" & stand == "R"]
ff <- merge(ff, pick[, .(pitcher, ctrl)], by = "pitcher")
ff[, ラベル := sprintf("%s (ずれ %.2f ft)", player_name, ctrl)]

ggplot(ff, aes(x = plate_x, y = plate_z)) +
  annotate("rect", xmin = -0.83, xmax = 0.83, ymin = 1.6, ymax = 3.4,
           fill = NA, colour = "grey40") +
  geom_point(alpha = 0.25, size = 0.8) +
  stat_density_2d(colour = "steelblue", bins = 5) +
  coord_fixed(xlim = c(-2, 2), ylim = c(0, 5)) +
  facet_wrap(~ ラベル) +
  xlab("左右 (ft, 捕手から見て右が正)") + ylab("高さ (ft)") +
  ggtitle("右打者への直球 (枠は平均的なストライクゾーン)")

ずれが小さい投手 (1000球以上)
投手 投球数 ずれ
Kevin Gausman 2815 0.966
Logan Webb 3075 0.981
Joe Ryan 1955 0.982
Jack Flaherty 2441 0.991
Tyler Alexander 1401 0.991
Joe Ross 1011 0.992
Aaron Nola 3128 0.994
George Kirby 2790 0.998
Ryan Yarbrough 1486 1.004
Cody Bradford 1021 1.004

この指標で最も制球が良いのは Kevin Gausman (2,815球, ずれ 0.966 ft) です. 最も散らばっているのは Triston McKenzie (ずれ 1.324 ft) で, 差は0.358 ft ありました.

ただし上の分割標本での確かめのとおり, この順位は年をまたいで そのまま再現するものではありません. 上位10人を「制球の良い投手」として 読むのは構いませんが, 1位と3位の差を意味づけしないでください.

注意

  • 「狙い」は推定であって, 本当の狙いではありません. 投手が意図的に散らして いる場合と, 狙って外れている場合を区別できません. 狙い所を2か所にする試みは そこを埋めようとしたものですが, 上で見たとおり安定しませんでした.
  • 条件を投手 × 球種 × 打者の左右までしか揃えていません. カウントは リーグ全体の寄りを引いただけで, 投手ごとの使い分けは残っています.
  • 走者の有無を見ていません. セットポジションからの投球は条件が違いますが, 塁上の走者を表す列が取得データに入っていません (data/build-statcast.R の取得列に on_1b などがありません).
  • ずれは本塁上での位置で測っています. 捕手が構えた位置は分かりません. 変化の大きい球種ほど, 同じ「狙い」でも本塁上ではばらつきます.
  • 100球以上投げた条件だけが対象です. 持ち球のうち, たまにしか使わない球種は評価に入っていません.
  • 2024年の1シーズンだけです. 年をまたいだ安定性 (今年の制球が来年も続くか) は 見ていません.

関連