アメリカ野球殿堂の選出は記者投票です. 明文化された基準はありません. それでも「通算3000安打なら当確」のような相場観は語られます.
通算成績だけでどこまで当てられるかを見ます.
library(Lahman)
library(dplyr)
library(ggplot2)
# 選手としての選出のみ (監督・審判・功労者は除く)
hof <- HallOfFame %>%
filter(category == "Player") %>%
group_by(playerID) %>%
dplyr::summarise(inducted = any(inducted == "Y"), .groups = "drop")
# 投手か野手かは, 通算出場のうち投手としての出場が半分を超えるかで決める
role <- Appearances %>%
group_by(playerID) %>%
dplyr::summarise(seasons = n_distinct(yearID),
G_all = sum(G_all, na.rm = TRUE),
G_p = sum(G_p, na.rm = TRUE), .groups = "drop") %>%
mutate(投手 = G_p > 0.5 * G_all)
profile <- People %>%
mutate(final_year = as.integer(substr(as.character(finalGame), 1, 4)),
name = paste(nameFirst, nameLast)) %>%
dplyr::select(playerID, name, final_year)
allstar <- AllstarFull %>% group_by(playerID) %>%
dplyr::summarise(AS = n_distinct(yearID), .groups = "drop")
mvp <- AwardsPlayers %>% filter(grepl("Most Valuable Player", awardID)) %>%
count(playerID, name = "MVP")
cy <- AwardsPlayers %>% filter(grepl("Cy Young", awardID)) %>%
count(playerID, name = "CY")
対象は殿堂入りの資格を満たしうる選手に絞ります. 10シーズン以上出場し, 2019年までに引退した選手です (選出は引退5年後から, 記者投票は最長10年続きます).
LAST <- 2019 # これ以降の引退者はまだ投票が終わっていない
MINS <- 10 # 殿堂入りの資格は10シーズン以上
build <- function(is_pitcher, career) {
role %>%
filter(投手 == is_pitcher, seasons >= MINS) %>%
inner_join(profile, by = "playerID") %>%
filter(!is.na(final_year), final_year <= LAST) %>%
inner_join(career, by = "playerID") %>%
left_join(allstar, by = "playerID") %>%
left_join(mvp, by = "playerID") %>%
left_join(cy, by = "playerID") %>%
left_join(hof, by = "playerID") %>%
mutate(AS = ifelse(is.na(AS), 0, AS),
MVP = ifelse(is.na(MVP), 0, MVP),
CY = ifelse(is.na(CY), 0, CY),
y = ifelse(is.na(inducted), 0L, as.integer(inducted)))
}
bat_career <- Batting %>%
group_by(playerID) %>%
dplyr::summarise(AB = sum(AB, na.rm = TRUE), H = sum(H, na.rm = TRUE),
HR = sum(HR, na.rm = TRUE), RBI = sum(RBI, na.rm = TRUE),
.groups = "drop") %>%
filter(AB >= 1000) %>%
mutate(AVG = H / AB)
pit_career <- Pitching %>%
group_by(playerID) %>%
dplyr::summarise(W = sum(W, na.rm = TRUE), SO = sum(SO, na.rm = TRUE),
SV = sum(SV, na.rm = TRUE), IPouts = sum(IPouts, na.rm = TRUE),
ER = sum(ER, na.rm = TRUE), .groups = "drop") %>%
filter(IPouts >= 1500) %>%
mutate(ERA = 27 * ER / IPouts)
batters <- build(FALSE, bat_career)
pitchers <- build(TRUE, pit_career)
野手2082人 (うち殿堂入り192人, 9.2%), 投手1212人 (同81人, 6.7%) が対象です. 当たりはごく少数なので, 「全員に『入らない』と答える」だけで正解率は 91%を超えます. 正解率で評価してはいけない問題です.
同じ選手で当てても意味がありません. 1994年までに引退した選手でモデルを作り, 1995年以降に引退した選手で試します.
SPLIT <- 1994
# 順位付けの良さを見る. 殿堂入りした選手のほうが高いスコアを付けられた組の割合
auc <- function(score, y) {
r <- rank(score)
n1 <- sum(y == 1); n0 <- sum(y == 0)
(sum(r[y == 1]) - n1 * (n1 + 1) / 2) / (n1 * n0)
}
evaluate <- function(d, full, base) {
tr <- d %>% filter(final_year <= SPLIT)
te <- d %>% filter(final_year > SPLIT)
m_full <- glm(full, data = tr, family = binomial())
m_base <- glm(base, data = tr, family = binomial())
te$p_full <- predict(m_full, te, type = "response")
te$p_base <- predict(m_base, te, type = "response")
list(train = tr, test = te, model = m_full,
auc_full = auc(te$p_full, te$y), auc_base = auc(te$p_base, te$y))
}
b <- evaluate(batters,
y ~ H + HR + AVG + seasons + AS + MVP,
y ~ H)
p <- evaluate(pitchers,
y ~ W + SO + ERA + SV + seasons + AS + CY,
y ~ W)
data.frame(
対象 = c("野手", "投手"),
検証人数 = c(nrow(b$test), nrow(p$test)),
殿堂入り = c(sum(b$test$y), sum(p$test$y)),
ベースライン = sprintf("%.3f", c(b$auc_base, p$auc_base)),
フルモデル = sprintf("%.3f", c(b$auc_full, p$auc_full))) %>%
knitr::kable(caption = "検証データでの AUC (ベースラインは野手が通算安打のみ, 投手が通算勝利のみ)")
| 対象 | 検証人数 | 殿堂入り | ベースライン | フルモデル |
|---|---|---|---|---|
| 野手 | 662 | 36 | 0.978 | 0.990 |
| 投手 | 467 | 13 | 0.829 | 0.997 |
AUC は「殿堂入りした選手のほうに高い確率を付けられた組の割合」で, でたらめなら0.5です. 野手は通算安打だけで0.978, 説明変数を増やして0.990 (+0.012). 投手は通算勝利だけで0.829, フルモデルで0.997 (+0.168).
野手と投手で様子が違います. 野手は通算安打1本でほぼ説明がつきます. 上乗せは+0.012しかなく, 「3000安打なら当確」という相場観はよく当たっています.
投手は通算勝利だけでは足りません. 0.829は 野手のベースラインよりはっきり低く, 奪三振・防御率・セーブを足すと 0.997まで上がります (+0.168). 勝利数は味方の得点に左右されるうえ, 抑え投手は勝利がほとんど付きません. 投票する側も勝利数だけを見てはいない, ということになります.
AUC は順位の指標なので, 実務的な見え方も出します. 確率の高い順に並べ, 上位から実際の殿堂入りが何人含まれるかを数えます.
topn <- function(d, k = 40) {
o <- d %>% arrange(desc(p_full)) %>% head(k)
data.frame(上位 = seq_len(k), 的中 = cumsum(o$y))
}
rbind(topn(b$test) %>% mutate(対象 = "野手"),
topn(p$test) %>% mutate(対象 = "投手")) %>%
ggplot(aes(x = 上位, y = 的中, colour = 対象)) +
geom_step() +
geom_abline(slope = 1, intercept = 0, linetype = "dashed") +
xlab("予測確率の高い順") + ylab("実際に殿堂入りした人数") +
ggtitle("上位から数えた的中数 (破線は全員的中)")

上位20人のうち, 野手は16人, 投手は13人が実際に殿堂入りしています.
面白いのは外れたほうです. 数字は揃っているのに選ばれていない選手を並べます.
b$test %>% filter(y == 0) %>% arrange(desc(p_full)) %>% head(10) %>%
mutate(確率 = sprintf("%.2f", p_full), 打率 = sprintf("%.3f", AVG)) %>%
dplyr::select(name, 確率, 安打 = H, 本塁打 = HR, 打率, オールスター = AS) %>%
knitr::kable(caption = "野手: モデルは高く評価したが選ばれていない")
| name | 確率 | 安打 | 本塁打 | 打率 | オールスター |
|---|---|---|---|---|---|
| Barry Bonds | 1.00 | 2935 | 762 | 0.298 | 14 |
| Alex Rodriguez | 1.00 | 3115 | 696 | 0.295 | 14 |
| Manny Ramirez | 0.97 | 2574 | 555 | 0.312 | 12 |
| Gary Sheffield | 0.91 | 2689 | 509 | 0.292 | 9 |
| Rafael Palmeiro | 0.77 | 3020 | 569 | 0.288 | 4 |
| Carlos Beltran | 0.76 | 2725 | 435 | 0.279 | 9 |
| Julio Franco | 0.74 | 2586 | 173 | 0.298 | 3 |
| Don Mattingly | 0.70 | 2153 | 222 | 0.307 | 6 |
| Juan Gonzalez | 0.69 | 1936 | 434 | 0.295 | 3 |
| Sammy Sosa | 0.64 | 2408 | 609 | 0.273 | 7 |
p$test %>% filter(y == 0) %>% arrange(desc(p_full)) %>% head(10) %>%
mutate(確率 = sprintf("%.2f", p_full), 防御率 = sprintf("%.2f", ERA)) %>%
dplyr::select(name, 確率, 勝利 = W, 奪三振 = SO, 防御率, オールスター = AS) %>%
knitr::kable(caption = "投手: モデルは高く評価したが選ばれていない")
| name | 確率 | 勝利 | 奪三振 | 防御率 | オールスター |
|---|---|---|---|---|---|
| Roger Clemens | 1.00 | 354 | 4672 | 3.12 | 11 |
| John Franco | 0.78 | 90 | 975 | 2.89 | 4 |
| Kevin Brown | 0.56 | 211 | 2397 | 3.28 | 6 |
| Dennis Martinez | 0.54 | 245 | 2149 | 3.70 | 4 |
| Curt Schilling | 0.53 | 216 | 3116 | 3.46 | 6 |
| Francisco Rodriguez | 0.52 | 52 | 1142 | 2.86 | 6 |
| Joe Nathan | 0.48 | 64 | 976 | 2.87 | 6 |
| Jonathan Papelbon | 0.41 | 41 | 808 | 2.44 | 6 |
| Bartolo Colon | 0.30 | 247 | 2535 | 4.12 | 4 |
| David Cone | 0.28 | 194 | 2668 | 3.46 | 5 |
逆に, 数字が地味なのに選ばれた選手です.
b$test %>% filter(y == 1) %>% arrange(p_full) %>% head(8) %>%
mutate(確率 = sprintf("%.2f", p_full), 打率 = sprintf("%.3f", AVG)) %>%
dplyr::select(name, 確率, 安打 = H, 本塁打 = HR, 打率, オールスター = AS) %>%
knitr::kable(caption = "野手: モデルは低く見たが選ばれた")
| name | 確率 | 安打 | 本塁打 | 打率 | オールスター |
|---|---|---|---|---|---|
| Scott Rolen | 0.35 | 2077 | 316 | 0.281 | 7 |
| Alan Trammell | 0.53 | 2365 | 185 | 0.285 | 6 |
| Ozzie Smith | 0.57 | 2460 | 28 | 0.262 | 15 |
| Fred McGriff | 0.57 | 2490 | 493 | 0.284 | 5 |
| Jim Thome | 0.58 | 2328 | 612 | 0.276 | 5 |
| Jeff Bagwell | 0.63 | 2314 | 449 | 0.297 | 4 |
| Jeff Kent | 0.67 | 2461 | 377 | 0.290 | 5 |
| Joe Mauer | 0.70 | 2123 | 143 | 0.306 | 6 |