← 記事一覧へ

追試する論文

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つです.

  1. DH 導入後, AL の死球率は NL を平均15%上回った
  2. 「投手が打席に立つこと」の抑止効果が, このリーグ差の60-80%を説明する
  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)]
2013年: DH判定の妥当性 (排他になるはず)
DHあり 投手が打席に立った 試合
TRUE FALSE 1212
FALSE TRUE 1215
TRUE TRUE 4

DH のある試合では投手が打席に立たない, という排他関係が 99.8%の試合で成り立っています. 残り4試合は, 試合中に DH を解除したものです.

主張1: リーグ差は15%か

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 の死球率が NL を上回る割合
期間 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%は, 論文が観測した窓の前半に集中していたことになります.

主張2: DHの効果は取り出せるか

ここが追試の山場です. まず, 生のリーグ差には構成の効果が混ざっています.

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)])
交流戦 1997-2013: DHの有無で比べる (1000打席あたり)
対象 投球側 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%は構成の効果でした.

交流戦では DH と球場が区別できない

論文 (と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 = ","))
交流戦: DHの有無と, 投球側から見た球場
投球側 球場 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 = ","))
ホームとビジターで死球率は違う (野手の打席, 1973-2013)
投球側 打席 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])          # 交流戦がある全期間
統制後のリーグ効果とDH効果
窓 変数 打席 オッズ比 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つあります.

  • 交流戦が始まった1997年以降は, そもそもリーグ差がほとんど無い (1994-2013年で+1.3%). 説明すべき差が残っていません
  • リーグ差が大きかった1973-1993年には交流戦が無く, DH とリーグが完全に一致します. この期間ではどんな統制を入れても両者を分離できません

主張3: 報復はあるか

論文の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分ポイントの差にしかなりません.

報復は DH のある試合で強いか

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])
報復とDHの交互作用 (1973-2013)
変数 オッズ比 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 の有無で はっきりとは変わりません.

追試のまとめ

Bradbury & Drinen (2007) の追試
論文の主張 手元の結果 判定
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 とリーグを分離できません.

つまりこの自然実験は, 答えが欲しい時期と, 識別できる時期がずれています.

注意

  • DH の判定は「守備位置10の打者が居るか」で行いました. 試合中に DH を解除した試合 (2013年で4試合) は DH ありに数えています.
  • 「報復」と呼んでいるのは直前の半イニングの死球との相関です. 乱闘や警告のような文脈は見ていません. 荒れた試合では 両チームとも死球が増えるので, 意図的な報復でなくても相関は出ます. 試合固定効果を入れればこの一部は落とせますが, ここではやっていません.
  • 投手の乱調はシーズン与四球率で測りました. その日の調子は見ていません.
  • 打者の質はシーズンOPSです. 打席時点までの成績ではないので, わずかに未来の情報が入っています.
  • 1994年の中断と2020年前後は対象外です (手元のデータは2013年まで).
  • 死球は稀な事象です. オッズ比が1から離れていても, 絶対確率の差は1分ポイント未満にしかなりません.