← 記事一覧へ

「3割打ってる」はいつから信じていいのか

シーズン序盤に打率4割の打者がいます. 40打数16安打です. この打者は本当に良い打者なのか, それともまだ何も分かっていないのか.

指標によって答えが違います. 三振率は早く固まり, 打率はなかなか固まりません. どれくらい違うのかを数えます.

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

YEARS <- 2009:2013

cols <- fread("../../data/reference/retrosheet-columns.csv", header = FALSE)[[1]]
KEEP <- c("BAT_ID", "EVENT_CD", "AB_FL", "H_FL", "SF_FL", "BAT_EVENT_FL")
idx <- match(KEEP, cols)

dat <- rbindlist(lapply(YEARS, function(y) {
  d <- fread(sprintf("../../data/all%d.csv", y), select = idx,
             col.names = KEEP, showProgress = FALSE)
  d[, year := y][]
}))

pa <- dat[BAT_EVENT_FL == "T"]
pa[, `:=`(ab  = as.integer(AB_FL == "T"),
          hit = as.integer(H_FL > 0),
          tb  = as.integer(H_FL),
          k   = as.integer(EVENT_CD == 3),
          bb  = as.integer(EVENT_CD %in% c(14, 15)),
          hr  = as.integer(H_FL == 4),
          sf  = as.integer(SF_FL == "T"))]

指標は5つです. 分母がそれぞれ違うことに注意してください.

metrics_of <- function(d, by) {
  x <- d[, .(PA = .N, AB = sum(ab), H = sum(hit), TB = sum(tb),
             K = sum(k), BB = sum(bb), HR = sum(hr), SF = sum(sf)), by = by]
  x[AB > 0 & (AB - K - HR + SF) > 0,
    .(PA,
      打率      = H / AB,
      長打力    = (TB - H) / AB,        # ISO: 長打率 - 打率
      三振率    = K / PA,
      四球率    = BB / PA,
      インプレー打率 = (H - HR) / (AB - K - HR + SF)),   # BABIP
    by = by]
}

METRICS <- c("打率", "長打力", "三振率", "四球率", "インプレー打率")

翌年の自分を当てられるか

一番わかりやすい確かめ方は, 今年の値で来年の値を当てられるかです. 当てられるなら実力を測れていて, 当てられないなら偶然を見ていたことになります.

season <- metrics_of(pa, by = c("BAT_ID", "year"))
qual <- season[PA >= 400]

# 翌年の行を year-1 にずらして突き合わせる。
# copy() を忘れると := が qual 自身を書き換え、同じ行同士を結合して r=1 になる
nxt <- copy(qual)[, year := year - 1L]
pairs <- merge(qual, nxt, by = c("BAT_ID", "year"), suffixes = c("", "_翌年"))
yoy <- rbindlist(lapply(METRICS, function(m) {
  x <- pairs[[m]]; y <- pairs[[paste0(m, "_翌年")]]
  ok <- is.finite(x) & is.finite(y)
  ct <- cor.test(x[ok], y[ok])
  data.table(指標 = m, r = ct$estimate, lo = ct$conf.int[1], hi = ct$conf.int[2],
             n = sum(ok))
}))
setorder(yoy, -r)
ggplot(yoy, aes(x = r, y = reorder(指標, r))) +
  geom_vline(xintercept = 0, linetype = "dashed") +
  geom_pointrange(aes(xmin = lo, xmax = hi)) +
  xlim(0, 1) +
  xlab("今年と翌年の相関") + ylab("") +
  ggtitle(paste0("翌年との相関 (", min(YEARS), "-", max(YEARS), "年, 400打席以上, n=",
                 yoy$n[1], "組)"))

yoy[, .(指標, 相関 = sprintf("%.2f", r),
        `95%区間` = sprintf("%.2f - %.2f", lo, hi), 組数 = n)] %>%
  knitr::kable()
指標 相関 95%区間 組数
三振率 0.87 0.85 - 0.89 576
四球率 0.75 0.71 - 0.78 576
長打力 0.67 0.63 - 0.71 576
打率 0.42 0.35 - 0.49 576
インプレー打率 0.38 0.30 - 0.44 576

三振率が最も再現し (r = 0.87), インプレー打率が最も再現しません (r = 0.38).

打率が上位に来ないのが要点です. 打率は野球で最も語られる指標ですが, 翌年との相関は0.42しかありません.

なぜ差が出るのか

翌年との相関が低い理由は2つ考えられます.

  1. 偶然の割合が大きい — 打席数に対して, 結果のばらつきが大きい
  2. 実力そのものが年で変わる — 加齢や故障

前者だけを取り出すには, 同じシーズンの中で半分に割って比べます. 同じ年の同じ打者なので, 実力は変わっていないと見なせます.

set.seed(1)
pa[, u := runif(.N)]
setorder(pa, BAT_ID, year, u)
pa[, `:=`(idx = seq_len(.N), npa = .N), by = .(BAT_ID, year)]

# n打席ずつの2つの山に分けて相関を取る
split_half <- function(n) {
  d <- pa[npa >= 2 * n]
  a <- metrics_of(d[idx <= n],                  by = c("BAT_ID", "year"))
  b <- metrics_of(d[idx > n & idx <= 2 * n],    by = c("BAT_ID", "year"))
  m <- merge(a, b, by = c("BAT_ID", "year"), suffixes = c("_A", "_B"))
  rbindlist(lapply(METRICS, function(mm) {
    x <- m[[paste0(mm, "_A")]]; y <- m[[paste0(mm, "_B")]]
    ok <- is.finite(x) & is.finite(y)
    data.table(指標 = mm, 打席 = n, r = if (sum(ok) > 30) cor(x[ok], y[ok]) else NA_real_,
               人数 = sum(ok))
  }))
}

grid <- c(50, 75, 100, 150, 200, 250, 300, 350)
half <- rbindlist(lapply(grid, split_half))
ggplot(half, aes(x = 打席, y = r, colour = 指標)) +
  geom_hline(yintercept = 0.5, linetype = "dashed") +
  geom_line() + geom_point() +
  ylim(0, 1) +
  xlab("片側の打席数") + ylab("同一シーズン内の半分割相関") +
  ggtitle("何打席見れば安定するか (破線 r=0.5 が目安)")

半分割の相関が0.5を超えたところを「安定しはじめる打席数」と呼びます. 観測されたばらつきの半分が実力で説明できる, という意味です.

# r = 0.5 を横切る打席数を線形補間で求める
cross50 <- function(d) {
  d <- d[!is.na(r)][order(打席)]
  i <- which(d$r >= 0.5)[1]
  if (is.na(i)) return(NA_real_)
  if (i == 1) return(d$打席[1])
  x0 <- d$打席[i - 1]; x1 <- d$打席[i]; y0 <- d$r[i - 1]; y1 <- d$r[i]
  x0 + (0.5 - y0) * (x1 - x0) / (y1 - y0)
}
need <- half[, .(必要打席 = cross50(.SD)), by = 指標][order(必要打席)]
need[, .(指標, 必要打席 = ifelse(is.na(必要打席),
                                 paste0(max(grid), "打席でも届かない"),
                                 sprintf("%.0f打席", 必要打席)))] %>%
  knitr::kable(caption = "半分割相関が 0.5 に達する打席数")
半分割相関が 0.5 に達する打席数
指標 必要打席
三振率 50打席
四球率 114打席
長打力 165打席
打率 350打席でも届かない
インプレー打率 350打席でも届かない

三振率は50打席で安定します. 一方でインプレー打率は350打席まで見ても0.5に届きません.

冒頭の「40打数で打率4割」は, どの指標の基準にも遠く届きません. この時点で分かるのは, ほぼ何も無いということです.

縮めるとどうなるか

では序盤の成績をどう読むか. 「観測値をそのまま信じる」でも 「全部無視して平均と思う」でもなく, その間で重みを付けます.

# 前半200打席で後半を予測する。全体平均に向けてどれだけ縮めるのが最適か
d <- pa[npa >= 400]
first  <- metrics_of(d[idx <= 200],               by = c("BAT_ID", "year"))
second <- metrics_of(d[idx > 200 & idx <= 400],   by = c("BAT_ID", "year"))
ev <- merge(first, second, by = c("BAT_ID", "year"), suffixes = c("_前", "_後"))

shrink_rmse <- function(m, w) {
  x <- ev[[paste0(m, "_前")]]; y <- ev[[paste0(m, "_後")]]
  ok <- is.finite(x) & is.finite(y)
  mu <- mean(x[ok])
  sqrt(mean((y[ok] - (mu + w * (x[ok] - mu)))^2))
}

ws <- seq(0, 1, by = 0.05)
curve <- rbindlist(lapply(METRICS, function(m)
  data.table(指標 = m, w = ws, RMSE = vapply(ws, function(w) shrink_rmse(m, w), 0))))
best <- curve[, .(最適な重み = w[which.min(RMSE)],
                  そのまま = RMSE[w == 1][1] / min(RMSE)), by = 指標]
ggplot(curve, aes(x = w, y = RMSE, colour = 指標)) +
  geom_line() +
  facet_wrap(~ 指標, scales = "free_y") +
  guides(colour = "none") +
  xlab("観測値をどれだけ信じるか (0=全員平均, 1=そのまま)") +
  ggtitle("前半200打席から後半200打席を予測したときの誤差")

best[order(-最適な重み)][, .(指標,
      最適な重み = sprintf("%.2f", 最適な重み),
      `そのまま使うと誤差が` = sprintf("%.0f%%増", 100 * (そのまま - 1)))] %>%
  knitr::kable(caption = "前半200打席をどれだけ信じるべきか")
前半200打席をどれだけ信じるべきか
指標 最適な重み そのまま使うと誤差が
三振率 0.80 4%増
四球率 0.70 8%増
長打力 0.55 14%増
打率 0.25 25%増
インプレー打率 0.25 26%増

最も高いのは三振率の0.80ですが, それでも2割は平均に寄せたほうが当たります. 打率は0.25で, 観測された差の大半を捨てることになります.

どの指標も, 重み1 (観測値をそのまま使う) は最適から外れています. 序盤の成績をそのまま信じるのは, どの指標でも損だということです.

注意

  • 「安定しはじめる打席数」の r = 0.5 という基準に理論的な根拠はありません. 慣習的な目安です. どこで線を引くかで数字は変わります.
  • 半分割は同じシーズンの中で行っています. 対戦相手・球場・気候が 前半と後半で違うので, 完全に同条件ではありません.
  • 打席数の多い打者ほど良い打者です. 350打席以上に絞ると 集団が偏り, ばらつきが小さくなって相関は低めに出ます (レンジの制限). 必要打席数はそのぶん長めに出ている可能性があります.
  • 縮小の重みはこの集団・この打席数での最適値です. 打席数が変われば変わります. 一般則としては「打席数が少ないほど強く縮める」だけが言えます.
  • 対象は2009-2013年です. 三振率が上がり続けている時代なので, 今の値とは一致しません.