2013年4月のMLB全打席結果を使って, 複数列にまとめて同じ集計をかける 書き方を確認します.
もともとはdplyr::summarise_eachとdplyr::mutate_eachの練習でした.
どちらもdplyr 0.7.0で非推奨になり, 今は削除されています.
同じことはacross()で書けます.
library(data.table)
library(dplyr)
library(ggplot2)
library(knitr)
dat <- fread("../../data/derived/events-2013-04.csv")
昔のfreadは"T"/"F"をlogicalとして読んでいたので,
打席フラグAB_FLは
そのままsum()で数えられました.
今はcharacterとして読むので変換が要ります.
source("../../R/retrosheet.R") # as_flag()
dat[, .(AB_FL, BAT_EVENT_FL)] |> head(3) |> kable()
| AB_FL | BAT_EVENT_FL |
|---|---|
| T | T |
| T | T |
| F | T |
PITCH_SEQ_TXには1打席の投球が1文字ずつ入っています.
ただし投球でない文字も混ざります.
seq_chars <- table(unlist(strsplit(paste(dat$PITCH_SEQ_TX, collapse = ""), "")))
sort(seq_chars, decreasing = TRUE) |>
as.data.frame() |>
setNames(c("文字", "回数")) |>
head(22) |>
kable()
| 文字 | 回数 |
|---|---|
| B | 42818 |
| C | 21125 |
| X | 21057 |
| F | 19294 |
| S | 10907 |
| . | 5190 |
| 1 | 3249 |
| * | 2383 |
| > | 2221 |
| T | 852 |
| I | 564 |
| L | 435 |
| H | 250 |
| + | 123 |
| 2 | 93 |
| M | 86 |
| N | 61 |
| P | 60 |
| O | 13 |
| 3 | 11 |
| V | 3 |
1 2 3 は塁への牽制,
+ は捕手からの牽制, > は走者のスタート,
* は次の投球がワンバウンドだった印, .
は打者に関係ないプレー, N は投球なしです.
nchar()をそのまま使うと, これらも1球と数えてしまいます.
NON_PITCH <- "[123>+*.N]"
count_pitches <- function(x) nchar(gsub(NON_PITCH, "", x))
4月ぶんの合計で比べると, nchar()が130,795球,
牽制などを除くと117,464球で, 11.3%の過大計上でした.
H_FLは0=安打なし, 1=単打, 2=二塁打, 3=三塁打,
4=本塁打なので, 合計すると塁打数になります.
打者ごとに, 打席数・打数・安打数・塁打数・投球数を数えます.
数え方が全部sumなのでacross()でまとめられます.
per_batter <-
dat |>
transmute(BAT_ID,
PA = as_flag(BAT_EVENT_FL),
AB = as_flag(AB_FL),
H = H_FL > 0,
TB = H_FL,
pitches = count_pitches(PITCH_SEQ_TX)) |>
summarise(across(c(PA, AB, H, TB, pitches), sum), .by = BAT_ID) |>
filter(AB > 50)
251人が残りました (4月に51打数以上).
率に直します. 投球数は打数ではなく打席で割ります. 四球で終わった打席も球は投げられているので, 打数で割ると 四球の多い打者が過大に出ます.
rates <-
per_batter |>
mutate(打率 = H / AB,
長打率 = TB / AB,
打席あたり投球数 = pitches / PA) |>
select(retroID = BAT_ID, 打率, 長打率, 打席あたり投球数, PA, AB)
head(rates) |> kable(digits = 3)
| retroID | 打率 | 長打率 | 打席あたり投球数 | PA | AB |
|---|---|---|---|---|---|
| crisc001 | 0.283 | 0.556 | 4.197 | 117 | 99 |
| younc004 | 0.172 | 0.391 | 4.079 | 101 | 87 |
| lowrj001 | 0.333 | 0.529 | 3.913 | 115 | 102 |
| cespy001 | 0.228 | 0.509 | 4.227 | 66 | 57 |
| donaj001 | 0.314 | 0.490 | 4.267 | 116 | 102 |
| mossb001 | 0.295 | 0.477 | 4.068 | 103 | 88 |
打数で割ると平均4.42球, 打席で割ると3.95球です. MLBの1打席あたりの投球数は3.9球前後なので, 後者が妥当な値です.
fullname <- fread("../../data/derived/player-names.csv")
named <- rates |> inner_join(fullname, by = "retroID")
show_top <- function(col, n = 10) {
named |>
arrange(desc(.data[[col]])) |>
transmute(選手 = name, !!col := .data[[col]], 打席 = PA) |>
head(n) |>
kable(digits = 3)
}
show_top("打率")
| 選手 | 打率 | 打席 |
|---|---|---|
| Carlos Santana | 0.389 | 84 |
| James Loney | 0.373 | 74 |
| Torii Hunter | 0.370 | 107 |
| Chris Johnson | 0.369 | 87 |
| Jean Segura | 0.367 | 98 |
| Miguel Cabrera | 0.363 | 117 |
| Carlos Gomez | 0.360 | 94 |
| Wilin Rosario | 0.350 | 83 |
| Chris Davis | 0.348 | 113 |
| Nate McLouth | 0.346 | 94 |
show_top("長打率")
| 選手 | 長打率 | 打席 |
|---|---|---|
| Justin Upton | 0.734 | 112 |
| Chris Davis | 0.728 | 113 |
| Carlos Santana | 0.722 | 84 |
| Bryce Harper | 0.720 | 107 |
| Travis Hafner | 0.667 | 80 |
| Mark Reynolds | 0.651 | 95 |
| Wilin Rosario | 0.650 | 83 |
| Dexter Fowler | 0.621 | 112 |
| Carlos Gomez | 0.616 | 94 |
| Troy Tulowitzki | 0.603 | 94 |
show_top("打席あたり投球数")
| 選手 | 打席あたり投球数 | 打席 |
|---|---|---|
| Will Venable | 4.727 | 77 |
| A. J. Ellis | 4.686 | 86 |
| Mark Ellis | 4.612 | 80 |
| Lucas Duda | 4.543 | 94 |
| Joe Mauer | 4.538 | 104 |
| Mike Napoli | 4.518 | 112 |
| Chris Carter | 4.495 | 105 |
| Travis Snider | 4.485 | 68 |
| Stephen Drew | 4.483 | 60 |
| Kyle Seager | 4.479 | 117 |
long <-
rates |>
tidyr::pivot_longer(c(打率, 長打率), names_to = "指標", values_to = "値")
ggplot(long, aes(x = 打席あたり投球数, y = 値)) +
geom_point(alpha = 0.6) +
stat_smooth(method = "lm", formula = y ~ x) +
facet_wrap(~ 指標, scales = "free_y") +
ggtitle("打席あたり投球数との関係")

cors <-
tibble::tibble(
指標 = c("打率", "長打率"),
r = c(cor(rates$打席あたり投球数, rates$打率),
cor(rates$打席あたり投球数, rates$長打率))
) |>
mutate(`95%信頼区間の下` = NA_real_, `95%信頼区間の上` = NA_real_)
ci <- function(x, y) cor.test(x, y)$conf.int
cors[1, 3:4] <- as.list(ci(rates$打席あたり投球数, rates$打率))
cors[2, 3:4] <- as.list(ci(rates$打席あたり投球数, rates$長打率))
kable(cors, digits = 3)
| 指標 | r | 95%信頼区間の下 | 95%信頼区間の上 |
|---|---|---|---|
| 打率 | -0.003 | -0.127 | 0.121 |
| 長打率 | 0.152 | 0.028 | 0.270 |
打率との相関は-0.003 (95%信頼区間 -0.13 〜 0.12), 長打率との相関は0.152 (0.03 〜 0.27)です.
打率との関係は信頼区間が0をまたいでおり, あるとも無いとも言えません. 長打率のほうは信頼区間が0を含まず, 弱いながら正の関係があります. 長打を狙う打者ほど追い込まれても粘る, といった説明が考えられますが, この分析では区別できません.
いずれにせよ1か月251人ぶんで, 幅も0.24 あります. 順位や大小をここから断定するのは無理です.
rates |>
ggplot(aes(x = 打率, y = 長打率)) +
geom_point(alpha = 0.6) +
stat_smooth(method = "lm", formula = y ~ x) +
ggtitle("打率と長打率")

打率と長打率は当然ながら強く相関します (r = 0.73). 長打率は安打を塁打で重み付けしたものなので, 定義上そうなります.