「今日は当たっている」「絶不調だ」と言います. 打席結果に流れ (系列相関) はあるのでしょうか.
安打かどうかの並びがランダムかを, 連検定 (runs test) で調べます. ランダムなら安打と凡打の切り替わり回数は決まった分布に従うので, それより切り替わりが少なければ「固まっている」= 流れがある, となります.
library(data.table)
library(dplyr)
library(ggplot2)
library(tseries)
library(knitr)
source("../../R/retrosheet.R")
dat <- fread("../../data/derived/events-2013-04.csv")
seqs <-
dat |>
as_tibble() |>
filter(as_flag(AB_FL)) |> # 四死球などは除く
mutate(HIT = factor(if_else(H_FL > 0, "HIT", "NOHIT"))) |>
filter(n() >= 50, n_distinct(HIT) == 2, .by = BAT_ID)
2013年4月のデータで, 50打数以上の打者 255人が対象です.
res <-
seqs |>
summarise(打数 = n(),
z = runs.test(HIT)$statistic,
p = runs.test(HIT)$p.value,
.by = BAT_ID)
rejected <- res |> filter(p < 0.05)
p < 0.05 になったのは13人でした.
これを「13人には流れがある」と読んではいけません.
255人に検定をかけているので, 全員が完全にランダムでも 有意水準5%なら平均12.8人が p < 0.05 になります.
実際に出たのは13人なので, 期待される数と ほぼ変わりません.
binom.test(nrow(rejected), nrow(res), p = 0.05)
##
## Exact binomial test
##
## data: nrow(rejected) and nrow(res)
## number of successes = 13, number of trials = 255, p-value = 0.8856
## alternative hypothesis: true probability of success is not equal to 0.05
## 95 percent confidence interval:
## 0.02742073 0.08560036
## sample estimates:
## probability of success
## 0.05098039
もっと素直な見方があります. 全員がランダムなら, p値は0から1の一様分布に なります. 流れのある打者が混ざっていれば, 小さいほうに山ができます.
res |>
ggplot(aes(x = p)) +
geom_histogram(breaks = seq(0, 1, 0.05), fill = "grey70", colour = "white") +
geom_hline(yintercept = nrow(res) / 20, linetype = "dashed", colour = "tomato") +
xlab("p値") + ylab("人数") +
ggtitle("赤線は「全員ランダム」のときの期待人数")

ks.test(res$p, "punif")
##
## Asymptotic one-sample Kolmogorov-Smirnov test
##
## data: res$p
## D = 0.05136, p-value = 0.5117
## alternative hypothesis: two-sided
一様分布からのずれは検出されません.
念のため Benjamini-Hochberg 法で偽発見率を制御します.
res <- res |> mutate(q = p.adjust(p, method = "BH"))
sum(res$q < 0.05)
## [1] 0
0人です.
そもそも「どの打者に流れがあるか」を1人ずつ判定するのは無理があります. 1人あたり84打数しかなく, 検定の検出力がほとんど無いからです.
問いを変えます. 「打者全体として, 流れの傾向があるか」 なら, 255人ぶんをまとめられるので, ずっと感度が上がります.
連検定の統計量 z は, ランダムなら標準正規分布に従います. 255人の平均を取れば, 標準誤差は 0.063まで小さくなります.
z_mean <- mean(res$z)
z_se <- 1 / sqrt(nrow(res))
z_ci <- z_mean + c(-1.96, 1.96) * z_se
| 値 | |
|---|---|
| z の平均 | +0.1079 |
| 期待値 (流れが無い場合) | 0 |
| 95%信頼区間 | [-0.015, +0.231] |
| z の標準偏差 | 0.955 (期待値 1) |
res |>
ggplot(aes(x = z)) +
geom_histogram(bins = 25, fill = "grey70", colour = "white") +
stat_function(fun = \(x) dnorm(x) * nrow(res) * diff(range(res$z)) / 25,
colour = "tomato", linewidth = 1) +
geom_vline(xintercept = 0, linetype = "dashed") +
xlab("連検定の統計量 z") +
ggtitle("赤線は「流れなし」のときの分布 (標準正規)")

z が負なら「切り替わりが少ない = 固まっている = 流れがある」向きです.
平均は+0.1079で, 信頼区間は0を含みます. 標準偏差も0.955で, 期待値1とほぼ同じです.
打者ごとに流れの強さが違うなら, この標準偏差は1より大きくなるはずです. そうなっていません.
2013年4月のデータからは, 打席結果の流れは見つかりませんでした.
「見つからなかった」は「無い」ではありません.
1か月ぶんなので1人84打数程度です. 255人ぶんをまとめてなお, 検出できるのは z の平均で0.12以上のずれです. それより小さい流れがあっても, このデータでは見えません.
シーズン全体を使えば打数は6倍ほどになり, 感度も上がります.
data/all2013.csv を用意すれば同じコードで試せます.
dplyr::do() を使わなくても, 結果が1個で済むなら
summarise() で書けます. do()
の使い方そのものは dplyr::doを使う
を見てください.