EPV 與 Riley 樣本數
「每個變項至少十個事件」是 1990 年代模擬得到的粗略經驗法則,現在已經被 Riley 等人的樣本數計算取代。這一頁在同一份乳癌世代上把兩種規則各算一次,再把模型真的在那兩個樣本量下各配三百次——EPV 十的樣本量交出來的校準斜率遠低於 1,絕對預測誤差是 Riley 版本的近兩倍。樣本數不足的後果是過度配適,不是檢定力不足。
EPV ≥ 10 是從哪裡來的
EPV(events per variable,每個變項的事件數)= 事件數 ÷ 模型參數個數。 「至少 10」這個門檻來自 1990 年代前後的一系列模擬研究:他們在各種情境下配邏輯迴歸與 Cox 迴歸, 觀察係數的偏誤、標準誤的準確度與信賴區間的覆蓋率,發現 EPV 掉到 10 以下時這些性質開始明顯變差。
這條規則有兩個優點,也正是它活這麼久的原因:好記,而且只需要一個數字就能算。
但它也因此有一個結構性的問題:它只看事件數與參數數量,其他什麼都不看。
現在的做法:先講清楚你要模型達到什麼
Riley 等人的樣本數計算把問題倒過來問:你希望這個模型好到什麼程度,才回推需要幾個人。
「好到什麼程度」被拆成幾個可以寫成數學式的準則。在連續型結果的完整形式裡有四個準則;
pmsampsize 對二元或存活結果只報三個,因為第四個(殘差標準差要估得準)只在連續結果下才有意義:
| 準則 | 它在防什麼 | 預設目標 |
|---|---|---|
| 一、過度配適要小 | 係數太極端、校準斜率掉到 1 以下 | 期望的一致收縮因子 ≥ 0.9 |
| 二、表面與校正後的 R² 差距要小 | 模型看起來解釋了很多,其實大半是雜訊 | 兩者相差 ≤ 0.05 |
| 三、整體風險要估得準 | 存活模型的基準風險(等同於截距)本身有誤差,會整條刻度平移 | 估計的邊際誤差 ≤ 0.05 |
| 四、殘差標準差要估得準 | 連續結果的預測區間會太窄 | 僅適用連續型結果 |
要跑這個計算,必須事先把輸入備齊,而存活版與二元版要的東西不一樣: 存活版要參數個數、預期的 Cox-Snell R²(或等價的 C-index)、 事件發生率、平均追蹤時間與時間視野; 二元版把時間軸拿掉,改要參數個數、預期的 C-index(或 R²)與盛行率 (在時間視野上有事件的人所佔比例)。
兩種規則,同一個模型
模型:8 個參數的 Cox 迴歸,時間視野
5 年。從 survival::rotterdam 讀出算式需要的輸入:
| 輸入 | 值 | 用於 | 怎麼來的 |
|---|---|---|---|
| 參數個數 | 8 | 兩者 | 事前指定的候選變項 |
| Cox-Snell R² | 0.151 | 兩者 | 由模型的概似比卡方值換算 |
| (同一個模型的 Nagelkerke R²) | 0.182 | 兩者 | Cox-Snell 除以它的理論上限 0.832 |
| 事件發生率 | 0.100 / 人年 | 存活版 | 總事件數 ÷ 總追蹤人年 |
| 平均追蹤 | 5.74 年 | 存活版 | 世代平均 |
| 時間視野 | 5 年 | 存活版 | 事前指定的預測時點 |
| 時間視野的累積風險 | 43.6% | 僅二元版:prevalence | Kaplan-Meier |
EPV 規則:8 個參數 × 10 = 80 個事件, 換算成人數約 140 人。
Riley 的計算(pmsampsize,存活版本):
| 準則 | 需要的人數 | 對應的期望收縮因子 | 每個參數分到幾個事件 |
|---|---|---|---|
| 準則一:過度配適要小(期望收縮因子 0.9) | 436 | 0.900 | 31.3 |
| 準則二:表面與校正後的 R² 差距要小 | 174 | 0.784 | 12.5 |
| 準則三:整體風險要估得準 | 436 | 0.900 | 31.3 |
| 最終採用(三個準則裡最大的那一個) | 436 | 0.900 | 31.3 |
figures/scripts/B5-03-sample-size.RRiley 的計算要求 436 人,是 EPV 規則的 3.11 倍。決定最終數字的是準則一(過度配適), 而在那個樣本量下每個參數分到 31.3 個事件—— 遠高於 10。
真的在那兩個樣本量下配一次模型
上面都是算式。rotterdam 夠大,可以直接把兩種樣本量各抽 300 次, 每次配完模型都丟到沒被抽到的那些人身上量表現。那些人來自同一個世代, 所以差距只可能是樣本量造成的:
| 規則 | 人數 | 事件數 | EPV | 保留樣本校準斜率 | C-index 樂觀偏誤 | 五年風險的平均絕對誤差 |
|---|---|---|---|---|---|---|
| EPV = 10 | 140 | 80 | 10.0 | 0.688(0.487–0.927) | 0.038 | 8.4 個百分點 |
| Riley (pmsampsize) | 436 | 250 | 31.3 | 0.874(0.712–1.036) | 0.017 | 4.5 個百分點 |
| whole cohort | 2982 | 1713 | 214.1 | — | — | —(參照模型) |
全世代列用來定義 truth5,因此不提供保留樣本 MAPE;它不是零誤差的效能結果。
樣本數不足的後果不是「檢定力不足」
這是這一頁最容易被搬錯的一句話。
在假設檢定的世界裡,樣本不足=檢定力不足=該顯著的沒顯著。那是一個保守的失敗: 你會少宣稱一些東西,但你宣稱的東西不會因此變錯。
預測模型不是這樣。 樣本不足時模型照樣配得出來、C-index 照樣算得出來、 而且表面上看起來還特別好(上表的樂觀偏誤那一欄)。失敗的方式是:
- 係數太極端 → 預測值太極端 → 校準斜率小於 1
- 表面表現虛高 → 論文報出來的數字比實際能達到的高
- 換一批人重跑 → 得到相當不同的模型
動手跑一次
library(survival); library(pmsampsize)
data(cancer, package = "survival")
rot <- rotterdam
rot$time_y <- pmin(rot$rtime, rot$dtime) / 365.25
rot$rfs_event <- as.integer(rot$recur == 1 | rot$death == 1)
rot$size_mm <- c("<=20" = 15, "20-50" = 35, ">50" = 60)[as.character(rot$size)]
rot$grade3 <- as.integer(rot$grade >= 3)
rot$log_pgr <- log1p(rot$pgr); rot$log_er <- log1p(rot$er)
fit <- coxph(Surv(time_y, rfs_event) ~ age + meno + size_mm + grade3 +
nodes + log_pgr + log_er + hormon, data = rot)
# 三個輸入,全部從模型與世代讀出來
lr <- 2 * diff(fit$loglik)
r2cs <- 1 - exp(-lr / nrow(rot)) # Cox-Snell R^2
rate <- sum(rot$rfs_event) / sum(rot$time_y) # 每人年的事件率
mfu <- mean(rot$time_y)
pmsampsize(type = "s", csrsquared = r2cs, parameters = 8,
rate = rate, timepoint = 5, meanfup = mfu)
# 舊規則,一行就算完:10 個事件 / 參數
ceiling(10 * 8 / mean(rot$rfs_event))
# 同一個模型的二元版本(結果換成「五年內有沒有事件」)
km <- survfit(Surv(time_y, rfs_event) ~ 1, data = rot)
p5 <- 1 - summary(km, times = 5)$surv
pmsampsize(type = "b", cstatistic = summary(fit)$concordance[1],
parameters = 8, prevalence = p5)驗證環境:R 4.6.0 + survival 3.8.6 + pmsampsize 1.1.3。pmsampsize 的 csrsquared 與 nagrsquared 只能擇一;type = "s" 還需要 rate、timepoint、meanfup 三個參數。
import numpy as np
import pandas as pd
from lifelines import CoxPHFitter
RD = "https://vincentarelbundock.github.io/Rdatasets/csv/"
d = pd.read_csv(RD + "survival/rotterdam.csv")
# 與上面 R 端相同的衍生欄位。rotterdam 原始欄位沒有這些,少了這一段
# 下面每一行都會 KeyError。
d["time_y"] = d[["rtime", "dtime"]].min(axis=1) / 365.25
d["rfs_event"] = ((d["recur"] == 1) | (d["death"] == 1)).astype(int)
d["size_mm"] = d["size"].map({"<=20": 15, "20-50": 35, ">50": 60})
d["grade3"] = (d["grade"] >= 3).astype(int)
d["log_pgr"] = np.log1p(d["pgr"]); d["log_er"] = np.log1p(d["er"])
cand = ["age", "meno", "size_mm", "grade3", "nodes", "log_pgr", "log_er", "hormon"]
fit = CoxPHFitter().fit(d[cand + ["time_y", "rfs_event"]], "time_y", "rfs_event")
null = CoxPHFitter().fit(d[["time_y", "rfs_event"]], "time_y", "rfs_event")
# 概似比卡方 -> Cox-Snell R^2,這一步是所有樣本數公式的入口
lr = 2 * (fit.log_likelihood_ - null.log_likelihood_)
r2cs = 1 - np.exp(-lr / len(d))
print(lr, r2cs)
# 事件率與平均追蹤,pmsampsize 的另外兩個輸入
print(d["rfs_event"].sum() / d["time_y"].sum(), d["time_y"].mean())Python 目前沒有等價的成熟套件;實務上多半是直接呼叫 R 的 pmsampsize,或依原始論文的公式自行實作。下面示範的是最容易寫錯、也最值得自己算一次的那一段:Cox-Snell R² 的換算。
讀論文時要問的三件事
- 有沒有樣本數計算? 預測模型研究常常完全不提,或只寫一句「樣本量符合 EPV ≥ 10 的建議」。
- 參數個數怎麼算的? 一個三類的類別變項是兩個參數;一個樣條是好幾個參數; 被考慮過但沒進最終模型的候選變項也要算,因為篩選本身用掉了自由度。
- 樣本不足時作者做了什麼? 誠實的做法是縮減候選變項、改用懲罰、或明說模型是探索性的。 什麼都沒做而直接報告表面表現的,數字要打折看。
常見誤用
| 誤用 | 為什麼錯 |
|---|---|
| 「EPV 超過 10,樣本量足夠」 | 10 是 1990 年代針對係數準確度的粗略門檻,與預測值準不準不是同一件事 |
| 用最終模型的參數個數算 EPV | 要用候選變項的個數,篩選過程也消耗自由度 |
| 樣本不足時說「檢定力不夠,結果偏保守」 | 預測模型的失敗方向相反:表面表現虛高,模型看起來更好 |
| 把樣本數公式的 R² 用手上這份資料算出來當作事前輸入 | 那是事後的;事前必須借用已發表模型或 pilot |
| 只報最終樣本數,不報三個準則各自要求多少 | 讀者無法知道是哪一個準則在決定結果 |
| 二元與存活結果套用同一個數字 | 丟掉時間資訊後訊息量下降,需要的人數不同 |
| 類別變項只算一個參數 | k 類要算 k − 1 個 |
| 樣本不夠就靠懲罰迴歸補 | 懲罰改善校準,不會變出資料裡沒有的訊號 |
重跑本頁的所有數字
/opt/homebrew/bin/Rscript figures/scripts/B5-03-sample-size.R讀讀看這張圖
答案取自產生本頁圖表的同一份統計輸出,不是另外打上去的。
同一個八參數的 Cox 模型,EPV 等於 10 的規則要求 140 人,Riley 的計算要求 436 人。這個差距的來源是什麼?
看答案與解析
正確答案: Riley 的樣本量下每個參數分到 31 個事件,遠高於 EPV 的門檻——兩套規則瞄準的東西不同
31 個事件配一個參數,遠高於那條經驗法則的門檻,所以 Riley 的計算在這個例子裡並沒有比較寬鬆;174 只是三個準則中最低的一個要求,最終採用的一律是最大的那個,因為三個準則要同時滿足。二元版的 795 也不是「編碼方式決定人數」:把時間資訊丟掉之後同一批人能提供的訊息變少,所以需要更多人,那仍然是同一組準則算出來的。兩套規則真正的差別在於它們在防什麼——EPV 來自上個世紀關於係數偏誤與覆蓋率的模擬,那是病因學研究的驗收標準;預測模型的驗收標準是預測值準不準,而那取決於預期鑑別力、事件發生率、時間軸長度與參數個數,EPV 一個都看不到。
把 EPV 剛好等於 10 的樣本量真的抽出來配模型、重複三百次,保留樣本上的校準斜率是 0.688。這個數字說明什麼?
看答案與解析
正確答案: 0.688 明顯小於 1,代表模型算出來的風險差異要打三成左右的折才對;樣本不足在預測模型裡的後果是過度配適,不是檢定力不足
0.688 的意思是:模型說某人的風險比另一人高多少,實際上只有那個差距的七成左右。這正是樣本不足在預測模型裡的表現方式——它不是「該顯著的沒顯著」那種保守的失敗,模型照樣配得出來,而且表面上看起來還特別好。0.038 的樂觀偏誤看起來小,是因為 C-index 這個尺度本來就壓縮;同一列的校準斜率把同一個問題放大成看得見的樣子,這正是驗收不能只看鑑別力的理由。0.874 確實也還沒到 1,但它接近多了,而且準則一瞄準的是「期望的一致收縮因子」,這裡量的是保留樣本上實際跑出來的校準斜率,後者還額外吃到保留樣本本身的抽樣誤差,兩者本來就不會完全對齊。
EPV 樣本量配出來的模型,給每個人的五年風險平均偏離參照模型 0.084(以比例表示)。要判斷這個誤差算不算大,最該對照的是什麼?
看答案與解析
正確答案: 對照這個世代五年風險本身的分布——中位數 0.396,中央八成落在 0.273 到 0.677,平均偏離 0.084 約是那段區間的五分之一
判斷一個絕對誤差大不大,要看它跟被預測的量本身的散布比。這個世代的五年風險中位數是 0.396,中央八成的人落在 0.273 到 0.677 之間,那段區間寬約四成,而平均偏離 0.084 大約是它的五分之一,足以把一個人從門檻的一邊移到另一邊。拿 Riley 樣本量的 0.045 來對照只能說「另一個比較小」,那不是判斷這個大不大的參照物,何況 0.045 也不小,兩者差了將近兩倍正是這一節要示範的事。用最大值 0.999 當分母更糟:那是分布的極端,不是典型的人。順帶提醒,「每個人的預測風險平均偏多少」不是三個準則裡任何一個瞄準的量,它是本頁額外加的檢查。
pmsampsize 的存活版本對這個模型印出三個準則各自的人數要求:436、174、436,最終採用 436。為什麼是這個數字?
看答案與解析
正確答案: 三個準則要同時滿足,採用的是其中最大的那一個;436 由準則一(收縮因子 0.9)與準則三一起決定
436 是三個要求裡最大的一個,而三個準則的意思是「三件事都要做到」,所以最終樣本數一律取最大值;174 只說明準則二(表面與校正後 R² 的差距)在這個模型上比較容易滿足,它不是折衷點,也不是可以拿來當標準的那一個。31 這個每參數事件數是結果不是輸入:算出 436 人之後,那些人在這個事件率與時間視野下大約會提供多少事件,再除以參數個數才得到它。順序反過來讀,就會退回成另一條 EPV 規則,而那正是這套計算要取代的東西。至於準則一瞄準的 0.9,那是期望的一致收縮因子,也就是「係數平均只需要縮一成」這個目標。
同一個模型、同一批人,Riley 的存活版對三個準則分別要求 436、174、436 人,最終採用 436;換成二元的「五年內有沒有事件」,最終則要求 795 人。為什麼二元版要更多人?
看答案與解析
正確答案: 795 比存活版多,是因為二元化把時間資訊丟掉了:同一批人只剩「五年內有沒有事件」這一個是非題,能提供的訊息變少,要達到同樣的收縮因子就需要更多人
795 與 436 的差距來自資訊量。存活版用得到每個人「什麼時候」發生事件,以及被設限的人「至少活到什麼時候」;二元化之後這些全部塌成一個是非題。43 這個每參數事件數比存活版高,不是因為事件變多——二元版的事件定義並沒有比較寬鬆,是因為需要的人數多,事件數才跟著多。201 那一列同樣不能拿來反推:三個準則各自算各自的,最終一律取最大值,所以最終數字由最嚴格的那一個決定,不代表其餘準則寬鬆到可以忽略——而且二元版的準則二 201 其實高於存活版對應的 174,連「二元版每一條準則都比較寬鬆」都不成立。這也順帶說明了為什麼 EPV 這個只數事件的量回答不了樣本數的問題——它看不見時間軸。
在假設檢定的世界裡,樣本不足等於檢定力不足,那是一種保守的失敗。預測模型不是這樣。表上哪一個數字最直接說明了這件事?
看答案與解析
正確答案: EPV 樣本量的 C-index 樂觀偏誤是 0.038:樣本不足時模型不但配得出來,在自己的資料上還特別好看
0.038 是表面表現高出保留樣本表現的量。反保守就在這裡:樣本不足不會讓你什麼都得不到,它會讓你得到一個看起來很好、實際上不能用的模型,而論文報出來的正是那個看起來很好的數字。0.017 只說明樣本充足時這個偏誤會縮小,它不能推論成「失敗方向是保守的」——兩列的偏誤都是正的,方向從來沒有翻過面。0.487 那個百分位確實顯示重跑之間的變異也很大,但不確定性大是另一個問題:即使只跑一次、只看點估計,那個點估計本身就已經偏樂觀了。這就是為什麼預測模型的樣本數計算不是為了「有效的話看得出來」,而是為了「算出來的風險值可以拿給病人看」。
用到這個方法的章節
延伸觀看
Sample size calculations for clinical prediction model research
RSS Seminar with Richard Riley素材來源與授權
本頁為原創內容