UPDATE: 2022-07-16 02:06:18
このノートは、下記の書籍の「p値関数」に関する各章各節の部分について、個人的な判断で重要だと感じ点をまとめたものです。また、自分の理解をすすめるために、t検定での例を自分で追加しています。書籍のまとめにおいては、できる限り原著の説明通りに翻訳して記載しているつもりではありますが、不適切な表現がある場合、原著をあたって確認いただけますと幸いです。
仮説検定のp値からのみ、仮説を判断することについてのデメリットおよび信頼区間から形成されるp値関数のメリットについて、具体的な研究事例をもとに非常にわかりやすく記述されており、学びが非常に多い内容でした。この章を読むだけでも、仮説検定を p値だけから判断することの恐ろしさ、そして、誤った判断に繋がりやすいことが理解できました。
※書籍ではORが計算されてRRとして表示されている。
\[ RD_{L}, RD_{U} = RD \pm 1.645 \times SD \]
\[ ln(RD_{L}), ln(RD_{U}) = ln(RR) \pm 1.645 \times SD(ln(RR)) \]
\[ RD_{L}, RD_{U} = e^ {(ln(RR) \pm 1.645 \times SD(ln(RR)))} \]
ここでは、平均値の差に関するp値関数をもとにもう少し自分の理解を進めていきます。サンプルデータとしては、sleep
データを利用します。このデータは、2種類の鎮静剤の効果(コントロールと比較しての睡眠時間の増加)を10名の患者さんで示したデータです。
データの説明を探したけれど見つからなかったので、あっているかどうかわからないですが、このデータにはコントロール群は含まれておらず、2種類の薬に関するデータが入っていて、これにt検定を行うということは、コントロール群と比較して、薬がどのような効果があるのかを見ているのではなく、薬1と薬2の効果の平均の差を見ることになります。
# [, 1] extra numeric increase in hours of sleep
# [, 2] group factor drug given
# [, 3] ID factor patient ID
data(sleep)
$group <- factor(sleep$group, levels = c(2,1))
sleepstr(sleep)
## 'data.frame': 20 obs. of 3 variables:
## $ extra: num 0.7 -1.6 -0.2 -1.2 -0.1 3.4 3.7 0.8 0 2 ...
## $ group: Factor w/ 2 levels "2","1": 2 2 2 2 2 2 2 2 2 2 ...
## $ ID : Factor w/ 10 levels "1","2","3","4",..: 1 2 3 4 5 6 7 8 9 10 ...
デフォルトではt.test(mu = 0)
となっている引数の値を調整することで、あらゆる帰無仮説に対する両側p値を計算していきます。イメージとしては、得られた観測データをもとに、「true
difference in means is equal to 0」「true difference in means is equal
to 0.1」「true difference in means is equal to
0.2」…というイメージで計算します。
p値をおさらいしておくと、p値は「特定の統計モデルのもとで(帰無仮説が真であると仮定することも含む)、観察されたデータの統計的要約が観察された値と同じか、それよりも極端である場合の確率」のこと。
<- seq(-2, 5,0.01)
x_vec <- vector(mode = "numeric", length = length(x_vec))
pval for (i in seq_along(x_vec)) {
<- t.test(extra ~ group, data = sleep, mu = x_vec[[i]], conf.level = 0.95, var.equal = TRUE)$p.val
pval[[i]]
}
# 母平均の差の計算内容確認用の自作p値関数
# このパターンでも同じものが計算されるはず
# 等分散を仮定する
# mean_diff_PvaluePlotFunction <- function(x, y) {
# cp <- seq(0.01, 0.999, 0.01)
# cpx <- c(cp, 1, rev(cp))
# cpy <- c(cp/2, 0.5, 0.5 + cp/2)
#
# nx <- length(x)
# mx <- mean(x)
# vx <- var(x)
#
# ny <- length(y)
# my <- mean(y)
# vy <- var(y)
#
# df <- nx + ny - 2
# d <- mx - my
#
# s2 <- ((nx - 1)*vx + (ny - 1)*vy) / df
# s <- sqrt(s2 * (1/nx + 1/ny))
# ci <- d + qt(cpy, df) * s
#
# return(data.frame(x_ci = ci, y_pval = cpx))
# }
# a <- f(sleep[1:10,1],sleep[11:20,1])
# plot(a$x_ci, a$y_pval, type = "l")
ここではグラフに信頼区間を99%、95%、90%、80%で書き込むための準備を行い、p値関数を可視化します。
<- 0.20; alpha010 <- 0.10; alpha005 <- 0.05; alpha001 <- 0.01
alpha020 <- t.test(extra ~ group, data = sleep, mu = 0, conf.level = 1 - alpha020)
ci80 <- t.test(extra ~ group, data = sleep, mu = 0, conf.level = 1 - alpha010)
ci90 <- t.test(extra ~ group, data = sleep, mu = 0, conf.level = 1 - alpha005)
ci95 <- t.test(extra ~ group, data = sleep, mu = 0, conf.level = 1 - alpha001)
ci99
<- function(){
plt plot(x_vec, pval, type = "l", xaxt = "n", yaxt = "n", xlab = "PointEstimation", ylab = 'p-value')
axis(side = 1, at = seq(-2, 5, 0.1))
axis(side = 2, at = seq(0, 1, 0.05))
<- ci95$estimate[1] - ci95$estimate[2]
point_estimate abline(v = 0, lty = 2, col = 'red')
abline(h = ci95$p.value, lty = 2)
abline(v = point_estimate, lty = 2, col = 'gray')
text(0.2, ci95$p.value+0.01, sprintf("p-value=%3.2f", ci95$p.value))
arrows(ci80$conf.int[1], alpha020,
$conf.int[2], alpha020,
ci80code = 3, lty = 1, length = 0.1)
text(point_estimate, alpha020+0.01, "80%CI")
arrows(ci90$conf.int[1], alpha010,
$conf.int[2], alpha010,
ci90code = 3, lty = 1, length = 0.1)
text(point_estimate, alpha010+0.01, "90%CI")
arrows(ci95$conf.int[1], alpha005,
$conf.int[2], alpha005,
ci95code = 3, lty = 1, length = 0.1)
text(point_estimate, alpha005+0.01, "95%CI")
arrows(ci99$conf.int[1], alpha001,
$conf.int[2], alpha001,
ci99code = 3, lty = 1, length = 0.1)
text(point_estimate, alpha001+0.01, "99%CI")
}plt()
仮説検定から10%有意水準で判断すると、薬1と薬2の平均値の差は0であるという帰無仮説を棄却し、対立仮説を採択することになりますが、5%有意水準で判断すると、帰無仮説を棄却できず、帰無仮説を受容することになり、p値の閾値によって判断が変わってしまいます。
しかし、p値関数のグラフがあれば、上記の内容に加え、点推定値周りの円錐形の広がり、信頼区間の幅が広いことから研究の精度としてはあまり高くないものの、点推定は+1.6となっており、薬2は薬1に比べて数ポイント高い可能性が示唆されます。また、1.6程度の差が実質的に意味のある差なのかどうかは、信頼区間やp値関数、検定結果が有意かどうかであれ、何も教えてくれないので、その点は別途、分析担当者が検討する必要があります。
# この結果を使うべきなのかな…
# delta_power <- abs(ci95$estimate[1] - ci95$estimate[2]) / ci95$stderr
# sd_power <- ci95$stderr
power.t.test(n = NULL, delta = 0.5, power = 0.8, sig.level = 0.05, alternative = "two.sided", sd = ci95$stderr)
##
## Two-sample t test power calculation
##
## n = 46.2498
## delta = 0.5
## sd = 0.849091
## sig.level = 0.05
## power = 0.8
## alternative = two.sided
##
## NOTE: n is number in *each* group
ここでは、1.6程度の差は実質的に重要な差と判断し、追試を行ったとします。目安のサンプルサイズを得るために、検定力分析を行い、その結果をもとに各群のサンプルサイズを50に増やしました。
set.seed(1989)
<- sample(x = 1:10, size = 50, replace = TRUE)
index.n <- sample(x = 11:20, size = 50, replace = TRUE)
index.m <- sleep[c(index.n, index.m), ]
sleep_large
<- vector(mode = "numeric", length = length(x_vec))
pval_large for (i in seq_along(x_vec)) {
<- t.test(extra ~ group, data = sleep_large, mu = x_vec[[i]], conf.level = 0.95)$p.val
pval_large[[i]]
}
plt() # 先程の結果に追試のグラフを重ねるために再度呼び出し
lines(x_vec, pval_large, lty = 3, col = "#4D4D4D")
<- mean(sleep_large[sleep_large$group == 2, "extra"]) - mean(sleep_large[sleep_large$group == 1, "extra"])
point_estimate_large abline(v = point_estimate_large, lty = 2, col = "#4D4D4D")
追試の結果は薄い点線のグレーで可視化しています。サンプルサイズを増やしたことで円錐形の幅も狭くなり、研究の精度が上がっていることがわかります。また、今回の結果を見る限り、やはり薬2の効果の方が高そうです。1回目の結果から、5%水準の統計的有意性がないことをp値だけから判断して、薬の効果はないと結論づけてしまっていると、薬の効果はあったかもしれないにも関わらず、誤った判断に繋がりそうです。この例のように、統計的仮説検定を利用してp値のみで判断することは、望ましいとはいえないですね…。
ここでは、理解を深めるためにp値関数を手計算していましたが、p値関数を可視化するpvaluefunctions
というパッケージがあるようです。内容をあまり確認していないので、忘れないメモ程度の内容を記載しています。
例えば、さきほどのsleep
データの例は下記のように感じで再現できます。
library(pvaluefunctions)
<- t.test(extra ~ group, data = sleep, mu = 0, conf.level = 0.95)
ttest <- t.test(extra ~ group, data = sleep_large, mu = 0, conf.level = 0.95)
ttest_large
<- mean(sleep[sleep$group == 2, "extra"]) - mean(sleep[sleep$group == 1, "extra"])
point_estimate <- mean(sleep_large[sleep_large$group == 2, "extra"]) - mean(sleep_large[sleep_large$group == 1, "extra"])
point_estimate_large
<- pvaluefunctions::conf_dist(
p estimate = c(point_estimate, point_estimate_large),
df = c(ttest$parameter, ttest_large$parameter),
tstat = c(ttest$statistic, ttest_large$statistic),
type = "ttest",
plot_type = "p_val",
conf_level = c(0.90, 0.95, 0.99),
null_values = c(0,0),
alternative = "two_sided",
xlab = "Mean difference (group2 - group1)",
together = TRUE,
plot = TRUE
)
冒頭で参考にしている書籍のプロットの再現コードを神戸大学の中澤先生がアップされているので、内容をおさらいしつつ動かした際のメモ。
# # ------------------------------------------------------------------------
# # NOTE: pvalueplot {fmsb} Drawing p-value function plot by a cross table
# # The table should be given as the cross table for the exposure status being column and the health outcome status being row,
# # opposite from usual manner for cross tabulation.
# # (拙意訳)
# # 表は、通常のクロス集計とは逆に、曝露状態を列、健康アウトカムを行とするクロス表で与えます。
# # 通常のリスクテーブルは行に曝露状態、列にアウトカムがあるが、下記のように表の行と列を入れ替えて渡す必要がある
# # Outcome : Col 1 / Comparing : Row 1 vs. Row 2
# # Col 1 Col 2
# # Row 1 8 70
# # Row 2 41 181
# # ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
# # ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
# # Row 1 Row 2
# # Col 1 8 41
# # Col 2 70 181
# # ------------------------------------------------------------------------
# library(fmsb)
# # Figure 8-1
# # TAB1は通常のリスクテーブルのセル位置
# TAB1 <- matrix(c(4,4,386,1250), 2)
# # t()で行と列を入替える。オッズ比の場合、オッズ比の計算式上、入替えても結果は変わらないが、
# # リスク比計算の場合と統一するために転置しておく
# T8.1 <- fmsb::pvalueplot(t(TAB1), plot.OR=TRUE, plot.log=TRUE, xrange=c(0.1, 100), ylim = c(0, 1.1))
# # oddsratio()は入替は不要で通常のリスクテーブル形式で入力。
# res <- fmsb::oddsratio(TAB1)
# segments(1, 0, 1, 1, lty = 1)
# segments(0.1, res$p.value, 100, res$p.value, lty = 2, col = 'red')
# text(res$estimate+0.5, 1.02, sprintf("Point Estimate=%3.2f", res$estimate))
# text(1, 1.02, "Null Hypothesis")
# text(0.6, res$p.value+0.02, sprintf("p-value=%3.2f", res$p.value))
#
# # Figure 8-2
# # TAB2は通常のリスクテーブルのセル位置
# TAB2 <- matrix(c(1090, 1000, 14910, 15000), 2)
# # t()で行と列を入替える。オッズ比の場合、オッズ比の計算式上、入替えても結果は変わらないが、
# # 通常のリスク比計算の場合と統一するために転置しておく
# T8.2 <- fmsb::pvalueplot(t(TAB2), plot.OR = TRUE, plot.log = TRUE, xrange = c(0.1, 100))
# lines(T8.1$OR, T8.1$p.value)
# segments(1, 0, 1, 1, lwd = 2)
#
# # Figure 8-3
# CI95 <- fmsb::oddsratio(TAB1, conf.level = 0.95)$conf.int
# CI90 <- fmsb::oddsratio(TAB1, conf.level = 0.90)$conf.int
# CI80 <- fmsb::oddsratio(TAB1, conf.level = 0.80)$conf.int
# fmsb::pvalueplot(t(TAB1), plot.OR = TRUE, plot.log = TRUE, xrange = c(0.1, 100))
# segments(1, 0, 1, 1, lty = 1)
# arrows(CI95[1], 0.05, CI95[2], 0.05, code = 3, lty = 3)
# text(res$estimate, 0.05, "95% CI")
# arrows(CI90[1], 0.10, CI90[2], 0.10, code = 3, lty = 3)
# text(res$estimate, 0.10, "90% CI")
# arrows(CI80[1], 0.20, CI80[2], 0.20, code = 3, lty = 3)
# text(res$estimate, 0.20, "80% CI")
#
# # Figure 8-5
# # TAB4は通常のリスクテーブルのセル位置
# TAB4 <- matrix(c(12, 5, 47, 45), 2)
# # t()で行と列を入替える
# fmsb::pvalueplot(t(TAB4), plot.OR = FALSE, plot.log = TRUE, xrange = c(0.1, 10))
# res4 <- fmsb::riskratio(X = TAB4[1,1], m1 = sum(TAB4[1,]),
# Y = TAB4[2,1], m2 = sum(TAB4[2,]),
# conf.level = 0.9)
# segments(1, res4$p.value, 10, res4$p.value, lty=2)
# text(0.5, res4$p.value+0.02, sprintf("p-value=%3.2f", res4$p.value))
# segments(1, 0, 1, 1, lwd = 1)
# arrows(res4$conf.int[1], 0.1, res4$conf.int[2], 0.1, code = 3, lty = 1)
# text(res4$estimate, 0.1+0.02, paste("90%", sprintf("CI: %3.2f-%3.2f", res4$conf.int[1], res4$conf.int[2]), sep=""))
ここからはメモ。
# ------------------------------------------------------------------------
# # http://halbau.world.coocan.jp/lecture/2007/epi12.pdf
# # リスク比の信頼区間の計算方法が記載されている
# library(Epi)
# a <- 8 ; b <- 70
# c <- 41; d <- 181
# TAB1 <- matrix(c(a, b, c, d), nrow = 2, byrow = TRUE)
# Epi::twoby2(TAB1, alpha = 0.05)
# 2 by 2 table analysis:
# ------------------------------------------------------
# Outcome : Col 1
# Comparing : Row 1 vs. Row 2
#
# Col 1 Col 2 P(Col 1) 95% conf. interval
# Row 1 8 70 0.1026 0.0521 0.1919
# Row 2 41 181 0.1847 0.1390 0.2412
#
# 95% conf. interval
# Relative Risk: 0.5553 0.2724 1.1321
# Sample Odds Ratio: 0.5045 0.2253 1.1298
# Conditional MLE Odds Ratio: 0.5056 0.1948 1.1639
# Probability difference: -0.0821 -0.1572 0.0161
#
# Exact P-value: 0.1095
# Asymptotic P-value: 0.0963
# ------------------------------------------------------
# # fmsb::pvalueplot()のリスク比の計算内容を確認する用の自作関数
# RiskRatio_PvaluePlotFunction <- function(XTAB) {
# cp <- seq(0.001, 0.999, 0.001)
# cpx <- c(cp, 1, rev(cp))
# cpy <- c(cp/2, 0.5, 0.5 + cp/2)
#
# row1_a <- XTAB[1, 1]
# row1_b <- XTAB[1, 2]
# row1_x <- sum(XTAB[1,])
#
# row2_c <- XTAB[2, 1]
# row2_d <- XTAB[2, 2]
# row2_y <- sum(XTAB[2,])
#
# row1_r <- row1_a / row1_x
# row2_r <- row2_c / row2_y
#
# rr <- row1_r / row2_r
# # リスク比の信頼区間の計算式の証明
# # https://yoshida931.hatenablog.com/entry/2018/02/01/154410
# # http://halbau.world.coocan.jp/lecture/2007/epi12.pdf
# rr_sigma2 <- (1 / row1_a) - (1 / row1_x) + (1 / row2_c) - (1 / row2_y)
# rr_sigma <- sqrt(rr_sigma2)
# rr_ci <- rr * exp(qnorm(cpy) * rr_sigma)
# return(data.frame(x_ci = rr_ci, y_pval = cpx))
# }
# a <- RiskRatio_PvaluePlotFunction(data)
# plot(a$x_ci, a$y_pval, type = "l")
# ------------------------------------------------------------------------