J. C. Bradbury and D. J. Drinen (2007). Crime and Punishment in Major League Baseball: The Case of the Designated Hitter and Hit Batters. Economic Inquiry 45(1), 131-144.
投手が打席に立たなくて済むなら, 相手打者にぶつけても自分が報復される心配がない. だから DH 制のリーグでは死球が増える — これが論文の仮説 (モラルハザード) です. DH が AL にだけ入ったので, 自然実験になっています.
論文の主張は3つです.
論文は retrosheet の1973-2003年を使いました. ここでは手元にある1938-2013年で追試します. DH 導入 (1973年) の 35年前から40年後までを一続きに見られるので, 論文より広い窓になります.
library(data.table)
library(dplyr)
library(ggplot2)
suppressMessages(library(Lahman))
col_names <- fread("../../data/reference/retrosheet-columns.csv", header = FALSE)[[1]]
KEEP <- c("GAME_ID", "AWAY_TEAM_ID", "INN_CT", "BAT_HOME_ID", "OUTS_CT",
"AWAY_SCORE_CT", "HOME_SCORE_CT", "BAT_ID", "PIT_ID", "BAT_FLD_CD",
"EVENT_CD", "BAT_EVENT_FL", "AB_FL",
"BASE1_RUN_ID", "BASE2_RUN_ID", "BASE3_RUN_ID")
idx <- match(KEEP, col_names)
files <- list.files("../../data", pattern = "^all[0-9]{4}\\.csv$", full.names = TRUE)
if (!length(files)) stop("../../data に all<年>.csv がありません")
seasons <- as.integer(sub(".*all([0-9]{4})\\.csv$", "\\1", files))
dat <- rbindlist(Map(function(f, y) {
x <- fread(f, select = idx, col.names = KEEP, showProgress = FALSE)
x[BAT_EVENT_FL == "T"][, year := y][]
}, files, seasons))
EVENT_CD の 16 が死球です. BAT_EVENT_FL
で打席として成立した行だけを見ます.
dat[, hbp := as.integer(EVENT_CD == 16L)]
# 投球していたチーム = 打撃側の反対
dat[, home_team := substr(GAME_ID, 1, 3)]
dat[, pitch_team := ifelse(BAT_HOME_ID == 1L, AWAY_TEAM_ID, home_team)]
dat[, bat_team := ifelse(BAT_HOME_ID == 1L, home_team, AWAY_TEAM_ID)]
# リーグは Lahman の teamIDretro で引く
lg <- unique(as.data.table(Lahman::Teams)[, .(year = yearID, team = teamIDretro,
lg = as.character(lgID))])
dat[lg, on = .(year, pitch_team = team), pitch_lg := i.lg]
dat[lg, on = .(year, bat_team = team), bat_lg := i.lg]
10,593,748打席のうち100,689打席は AL / NL 以外です (オールスター戦とニグロリーグ). これを落とします.
dat <- dat[!is.na(pitch_lg) & !is.na(bat_lg) & pitch_lg %in% c("AL", "NL")]
# DH を使った試合か: 守備位置10 (DH) の打者が居るか
dat[, dh := any(BAT_FLD_CD == 10L), by = GAME_ID]
# 打席に立ったのが投手か (守備位置1)
dat[, bat_pit := as.integer(BAT_FLD_CD == 1L)]
| DHあり | 投手が打席に立った | 試合 |
|---|---|---|
| TRUE | FALSE | 1212 |
| FALSE | TRUE | 1215 |
| TRUE | TRUE | 4 |
DH のある試合では投手が打席に立たない, という排他関係が 99.8%の試合で成り立っています. 残り4試合は, 試合中に DH を解除したものです.
yr <- dat[, .(PA = .N, HBP = sum(hbp)), by = .(year, pitch_lg)]
yr[, rate := 1000 * HBP / PA]
wide <- data.table::dcast(yr, year ~ pitch_lg, value.var = "rate")
wide[, gap := AL - NL]
excess <- function(rg) 100 * (wide[year %in% rg, mean(AL / NL)] - 1)
論文と同じ窓 (1973-2003年) で数えると, AL の死球率は NL を 15.4% 上回りました. 論文の「平均15%」がほぼそのまま出ます.
DH の無かった1938-1972年では-1.2%です. リーグ差は DH と一緒に現れたように見えます.
ggplot(yr, aes(x = year, y = rate, colour = pitch_lg)) +
geom_line() +
geom_vline(xintercept = 1973, linetype = "dashed") +
annotate("text", x = 1973.5, y = max(yr$rate), hjust = 0, size = 3,
label = "1973年 ALにDH導入") +
labs(x = NULL, y = "死球 (1000打席あたり)", colour = "投球側",
title = "リーグ別の死球率") +
theme_minimal()

図を見ると, 論文が書かなかった話が見えます. 1990年代にリーグ差が消えています.
| 期間 | ALの超過 |
|---|---|
| 1938-1972 (DH前) | -1.2% |
| 1973-1993 | +21.9% |
| 1994-2013 | +1.3% |
| 1973-2003 (論文と同じ窓) | +15.4% |
1973-1993年は+21.9%ですが, 1994-2013年は+1.3%しかありません. 論文の15%は, 論文が観測した窓の前半に集中していたことになります.
ここが追試の山場です. まず, 生のリーグ差には構成の効果が混ざっています.
dat[, .(打席 = .N, 死球 = sum(hbp), `1000打席あたり` = round(1000 * sum(hbp) / .N, 2)),
by = .(`打席に立ったのは` = ifelse(bat_pit == 1L, "投手", "野手"))] %>%
knitr::kable(caption = "投手は死球を受けにくい", format.args = list(big.mark = ","))
| 打席に立ったのは | 打席 | 死球 | 1000打席あたり |
|---|---|---|---|
| 野手 | 9,994,229 | 64,735 | 6.48 |
| 投手 | 498,830 | 1,247 | 2.50 |
投手の打席の死球率は2.50, 野手は6.48で, 投手は2.6倍ぶつけられにくい. これは論文も報告しています (ぶつけても損が小さい打者が狙われる, という別仮説の証拠).
すると DH の無い試合は, 打席の約1/9が「ぶつけられにくい投手」で埋まります. DH のある試合との差の一部は, 投手の意思決定ではなく打順の中身です.
inter <- dat[year >= 1997 & pitch_lg != bat_lg] # 交流戦
cmp <- rbind(
inter[, .(対象 = "全打席", PA = .N, rate = 1000 * sum(hbp) / .N), by = .(pitch_lg, dh)],
inter[bat_pit == 0L, .(対象 = "野手の打席だけ", PA = .N, rate = 1000 * sum(hbp) / .N),
by = .(pitch_lg, dh)])
| 対象 | 投球側 | DHなし | DHあり | 差 |
|---|---|---|---|---|
| 全打席 | AL | 8.85 | 9.82 | 0.98 |
| 全打席 | NL | 8.15 | 8.77 | 0.62 |
| 野手の打席だけ | AL | 9.23 | 9.82 | 0.59 |
| 野手の打席だけ | NL | 8.60 | 8.77 | 0.17 |
AL の投手で見ると, 全打席では差が+0.98ですが, 野手の打席だけにすると+0.59まで縮みます. 生の差の39%は構成の効果でした.
論文 (と2006年の姉妹論文) は, 交流戦を使えば 「AL というリーグの癖」と「DH というルール」を分けられる, と論じました. 手元のデータで確かめます.
inter[, .(打席 = .N), by = .(投球側 = pitch_lg, DH = dh,
球場 = ifelse(BAT_HOME_ID == 0L, "ホーム", "ビジター"))] %>%
data.table::dcast(投球側 + 球場 ~ DH, value.var = "打席") %>%
knitr::kable(caption = "交流戦: DHの有無と, 投球側から見た球場",
format.args = list(big.mark = ","))
| 投球側 | 球場 | FALSE | TRUE |
|---|---|---|---|
| AL | ビジター | 79,931 | 121 |
| AL | ホーム | 107 | 83,139 |
| NL | ビジター | 115 | 81,089 |
| NL | ホーム | 83,691 | 107 |
表のFALSE/TRUEは同じ球場を裏表から見たものです.
交流戦の DH はAL の球場かどうかで決まるので, AL
の投手にとっては 「DH = 自軍球場」, NL の投手にとっては「DH =
敵地」になります.
一致率は AL で99.9%, NL で99.9%, 合わせて99.9%です.
つまり交流戦で「DH の効果」として測ったものは, 「ホームで投げているかどうか」の効果と分けられません. 投球側のリーグを固定すると, DH の有無は球場の裏返しでしかないからです.
dat[bat_pit == 0L & year >= 1973,
.(打席 = .N, `1000打席あたり` = round(1000 * sum(hbp) / .N, 2)),
by = .(投球側 = ifelse(BAT_HOME_ID == 0L, "ホーム", "ビジター"))] %>%
knitr::kable(caption = "ホームとビジターで死球率は違う (野手の打席, 1973-2013)",
format.args = list(big.mark = ","))
| 投球側 | 打席 | 1000打席あたり |
|---|---|---|
| ホーム | 3,403,942 | 6.96 |
| ビジター | 3,287,439 | 7.31 |
無視できる差ではありません.
打席単位のロジスティック回帰で, 論文が挙げた要因を統制します. 説明変数がすべてカテゴリなので, 同じ組み合わせのセルに畳んでから当てます (推定結果は打席単位で当てるのと同じで, ずっと速い).
fit_dh <- function(d) {
d <- copy(d)
# 点差は投球側から見たリード。スコアはプレー前の値なので, そのまま「その時点の状況」
d[, lead := ifelse(BAT_HOME_ID == 1L, AWAY_SCORE_CT - HOME_SCORE_CT,
HOME_SCORE_CT - AWAY_SCORE_CT)]
d[, `:=`(lead_g = cut(lead, c(-Inf, -4, -2, 0, 2, 4, Inf), labels = FALSE),
inn_g = pmin(INN_CT, 10L),
base_g = (!is.na(BASE1_RUN_ID) & nzchar(BASE1_RUN_ID)) +
2L * (!is.na(BASE2_RUN_ID) & nzchar(BASE2_RUN_ID)) +
4L * (!is.na(BASE3_RUN_ID) & nzchar(BASE3_RUN_ID)),
home_pitch = as.integer(BAT_HOME_ID == 0L),
AL = as.integer(pitch_lg == "AL"), DH = as.integer(dh))]
# 打者の質 (シーズンOPS) と投手の乱調 (シーズン与四球率) を年内4分位で
d[, ops_g := 0L]
d[bat_pa >= 50, ops_g := cut(bat_ops, quantile(bat_ops, 0:4 / 4, na.rm = TRUE),
labels = FALSE, include.lowest = TRUE), by = year]
d[, bb_g := 0L]
d[pit_bf >= 50, bb_g := cut(pit_bb, quantile(pit_bb, 0:4 / 4, na.rm = TRUE),
labels = FALSE, include.lowest = TRUE), by = year]
K <- c("year", "inn_g", "OUTS_CT", "base_g", "lead_g", "bat_pit",
"home_pitch", "ops_g", "bb_g", "AL", "DH")
a <- d[, .(PA = .N, HBP = sum(hbp)), by = K]
for (v in c("year", "inn_g", "OUTS_CT", "base_g", "lead_g", "ops_g", "bb_g"))
set(a, j = v, value = factor(a[[v]]))
glm(cbind(HBP, PA - HBP) ~ AL + DH + year + inn_g + OUTS_CT + base_g +
lead_g + bat_pit + home_pitch + ops_g + bb_g, family = binomial, data = a)
}
打者と投手のシーズン成績を作ります.
dat[, `:=`(h1 = as.integer(EVENT_CD == 20L), h2 = as.integer(EVENT_CD == 21L),
h3 = as.integer(EVENT_CD == 22L), hr = as.integer(EVENT_CD == 23L),
bb = as.integer(EVENT_CD %in% c(14L, 15L)),
ubb = as.integer(EVENT_CD == 14L), ab = as.integer(AB_FL == "T"))]
bq <- dat[, .(PA = .N, AB = sum(ab), H = sum(h1 + h2 + h3 + hr),
TB = sum(h1 + 2 * h2 + 3 * h3 + 4 * hr),
BB = sum(bb), HBP = sum(hbp)), by = .(year, BAT_ID)]
bq[, ops := ifelse(AB > 0, TB / AB, 0) + (H + BB + HBP) / PA]
# 乱調は与四球率で測る。死球そのものを入れると同義反復になるので使わない
pw <- dat[, .(BF = .N, UBB = sum(ubb)), by = .(year, PIT_ID)]
pw[, bbrate := UBB / BF]
dat[bq, on = .(year, BAT_ID), `:=`(bat_ops = i.ops, bat_pa = i.PA)]
dat[pw, on = .(year, PIT_ID), `:=`(pit_bb = i.bbrate, pit_bf = i.BF)]
m_paper <- fit_dh(dat[year %in% 1997:2003]) # 論文が交流戦を使った窓
m_all <- fit_dh(dat[year >= 1997]) # 交流戦がある全期間
| 窓 | 変数 | 打席 | オッズ比 | 95%CI | p |
|---|---|---|---|---|---|
| 1997-2003 | AL | 1,304,814 | 1.033 | [0.950, 1.123] | 0.444 |
| 1997-2003 | DH | 1,304,814 | 0.964 | [0.887, 1.049] | 0.397 |
| 1997-2013 | AL | 3,170,899 | 1.020 | [0.968, 1.076] | 0.455 |
| 1997-2013 | DH | 3,170,899 | 0.959 | [0.909, 1.011] | 0.118 |
DH の効果は取り出せませんでした. 1997-2013年のオッズ比は0.959 (95%CI [0.909, 1.011]) で, 1をまたぎます. リーグそのものの効果も1.020 (95%CI [0.968, 1.076]) で1をまたぎます.
論文の「60-80%を説明する」は, 手元では再現できません. ただしこれを論文の誤りとするのは早いです. 理由は2つあります.
論文の3つめの主張です. 半イニングごとに畳んで, 直前の半イニング (相手の投手が投げた回) で死球があったかを見ます.
half <- dat[, .(hbp = as.integer(any(hbp == 1L)),
lead = ifelse(BAT_HOME_ID[1] == 1L,
AWAY_SCORE_CT[1] - HOME_SCORE_CT[1],
HOME_SCORE_CT[1] - AWAY_SCORE_CT[1]),
dh = dh[1], lg = pitch_lg[1], year = year[1]),
by = .(GAME_ID, INN_CT, BAT_HOME_ID)]
# 試合内の順番。ビジターが先攻なので 2*回 + 打撃側 で並ぶ
half[, ord := 2L * INN_CT + BAT_HOME_ID]
setorder(half, GAME_ID, ord)
half[, `:=`(prev_hbp = shift(hbp), prev_ord = shift(ord)), by = GAME_ID]
# 直前が本当に「ひとつ前の半イニング」である行だけを使う
half <- half[!is.na(prev_hbp) & prev_ord == ord - 1L]
half[, .(半イニング = .N, `死球あり` = sum(hbp),
割合 = sprintf("%.2f%%", 100 * mean(hbp))),
by = .(`直前の半イニングの死球` = ifelse(prev_hbp == 1L, "あり", "なし"))][
order(`直前の半イニングの死球`)] %>%
knitr::kable(caption = "報復はあるか", format.args = list(big.mark = ","))
| 直前の半イニングの死球 | 半イニング | 死球あり | 割合 |
|---|---|---|---|
| あり | 61,648 | 2,072 | 3.36% |
| なし | 2,256,373 | 58,668 | 2.60% |
ぶつけられた直後の半イニングでは, 死球の確率が 2.60%から3.36%に上がります. リスク比1.29です.
年と回と点差と DH とリーグを統制しても残るか見ます.
half[, `:=`(lead_g = cut(lead, c(-Inf, -4, -2, 0, 2, 4, Inf), labels = FALSE),
inn_g = pmin(INN_CT, 10L), DH = as.integer(dh), AL = as.integer(lg == "AL"))]
hc <- half[, .(N = .N, HBP = sum(hbp)), by = .(year, inn_g, lead_g, DH, AL, prev_hbp)]
for (v in c("year", "inn_g", "lead_g")) set(hc, j = v, value = factor(hc[[v]]))
m_ret <- glm(cbind(HBP, N - HBP) ~ prev_hbp + year + inn_g + lead_g + DH + AL,
family = binomial, data = hc)
残ります. オッズ比1.217 (95%CI [1.164, 1.273], p = 8.2e-18).
「全部の半イニングに同じ確率 (全体の死球率) を答える」より当たるのか確かめます. 比べるのは対数損失です.
# prev_hbp だけを落としたモデル (報復の項が効いているか)
m_no <- glm(cbind(HBP, N - HBP) ~ year + inn_g + lead_g + DH + AL,
family = binomial, data = hc)
# セル単位で数えているので, 対数損失も件数で重みを付けて足す
logloss <- function(p) {
-sum(hc$HBP * log(p) + (hc$N - hc$HBP) * log(1 - p)) / sum(hc$N)
}
ll_base <- logloss(rep(sum(hc$HBP) / sum(hc$N), nrow(hc)))
ll_no <- logloss(fitted(m_no))
ll_fit <- logloss(fitted(m_ret))
| 予測 | 対数損失 |
|---|---|
| 全部に同じ確率 (ベースライン) | 0.121286 |
| 統制のみ (報復の項なし) | 0.119802 |
| 統制 + 報復の項 | 0.119787 |
報復の項を足しても改善は1.5e-05しかありません. 効果は統計的には確実だが, 予測にはほとんど寄与しないということです. 死球はもともと稀 (2.6%) なので, オッズ比1.2は絶対確率にすると 0.76分ポイントの差にしかなりません.
m_int <- glm(cbind(HBP, N - HBP) ~ prev_hbp * DH + year + inn_g + lead_g + AL,
family = binomial,
data = hc[as.integer(as.character(year)) >= 1973])
| 変数 | オッズ比 | 95%CI | p |
|---|---|---|---|
| prev_hbp | 1.169 | [1.086, 1.258] | 0.000 |
| prev_hbp:DH | 1.104 | [0.999, 1.220] | 0.053 |
交互作用は1.104で, 有意ではありません (p = 0.05). 論文のモラルハザード仮説は「DH があると投手個人が報復されない」ですが, チーム単位の報復 (誰かにぶつけ返す) は DH の有無で はっきりとは変わりません.
| 論文の主張 | 手元の結果 | 判定 |
|---|---|---|
| DH導入後, ALの死球率はNLを約15%上回る | 15.4% (1973-2003年, 論文と同じ窓) | 再現した |
| 投手は他の打者よりぶつけられにくい | 2.6倍の差 (野手 6.48 対 投手 2.50) | 再現した |
| 「投手が打席に立つこと」がリーグ差の60-80%を説明する | DHのオッズ比 0.959, 95%CI [0.909, 1.011] | 再現できない |
| 直前の半イニングの死球が次の死球を増やす | オッズ比 1.217, 95%CI [1.164, 1.273] | 再現した |
論文の事実と機構は再現しましたが, 因果の大きさは再現できませんでした.
再現しなかった理由は, 論文が間違っていたからとは限りません. リーグ差そのものが1990年代に消えてしまったので, 交流戦が使える時代には説明すべき差が残っていないのです. そしてリーグ差が大きかった時代には交流戦が無く, DH とリーグを分離できません.
つまりこの自然実験は, 答えが欲しい時期と, 識別できる時期がずれています.