8  Chapter 8: 假設檢定:兩樣本推論 (hypothesis testing: two-sample inference)

兩組資料的對話:差異、配對與效果大小

8.1 本章學習目標

上一章我們處理一個樣本與一個基準值的比較。臨床與公衛研究更常見的問題是兩組比較:新治療與標準治療哪個好?介入前後是否改善?兩種照護模式的再住院率是否不同?這些都屬於兩樣本推論 (two-sample inference)。

讀完本章後,你應該能夠:

  1. 區分獨立樣本 (independent samples) 與配對樣本 (paired samples)。
  2. 執行兩獨立樣本 t 檢定 (two-sample t-test) 與 Welch t 檢定 (Welch's t-test)。
  3. 執行配對 t 檢定 (paired t-test)。
  4. 比較兩個母群體比例 (two population proportions)。
  5. 建立平均差 (mean difference)、比例差 (risk difference) 與相對風險 (relative risk) 的估計與信賴區間。
  6. 從效果大小、信賴區間與臨床意義一起解讀兩組比較。

8.2 兩組比較先問:獨立還是配對?

兩組資料最重要的第一個問題不是「要按哪個軟體按鈕」,而是資料結構。若兩組來自不同個體,例如標準照護組與介入照護組各自收案不同病人,這通常是獨立樣本。若兩個測量值來自同一個人,例如治療前與治療後血壓,這是配對樣本。

獨立樣本與配對樣本的分析方法不同。配對資料的核心是每個人的前後差異,而不是把治療前與治療後當成兩群陌生人。若忽略配對,會浪費資訊,甚至得到錯誤結論。統計方法很重視資料關係;資料之間若本來有牽手,請不要硬把它們拆散。

8.3 兩獨立樣本平均值比較

兩獨立樣本平均值比較常用於比較兩組病人的連續結果,例如收縮壓、LDL-C、住院天數或疼痛分數。令 \(\mu_1\)\(\mu_2\) 為兩組母群體平均值,常見雙尾假設為:

\[ H_0: \mu_1 - \mu_2 = 0 \]

\[ H_1: \mu_1 - \mu_2 \ne 0 \]

若兩組變異數可視為相等,可使用 pooled two-sample t-test。但醫療資料中兩組變異數常不同,Welch t 檢定較穩健,通常是很好的預設選擇。

8.4 範例 1:兩組照護模式的收縮壓

假設某研究比較標準照護與介入照護在 12 週後的收縮壓。兩組病人互相獨立。

standard = np.array([148, 142, 151, 139, 145, 150, 136, 144, 147, 153, 141, 149, 146, 138, 152, 143])
intervention = np.array([136, 132, 141, 129, 135, 138, 131, 134, 137, 140, 133, 139, 130, 128, 142, 136])

bp_groups = pd.DataFrame(
    {
        "sbp": np.r_[standard, intervention],
        "group": ["標準照護"] * len(standard) + ["介入照護"] * len(intervention),
    }
)

bp_groups.groupby("group")["sbp"].agg(["count", "mean", "std"]).round(2)
count mean std
group
介入照護 16 135.06 4.33
標準照護 16 145.25 5.16
plt.figure(figsize=(7, 4.5))
sns.boxplot(data=bp_groups, x="group", y="sbp", hue="group", palette="Set2", legend=False)
sns.stripplot(data=bp_groups, x="group", y="sbp", color="#333333", alpha=0.7, jitter=0.12)
plt.xlabel("照護組別")
plt.ylabel("12 週後收縮壓 (mmHg)")
plt.title("兩獨立樣本:12 週後收縮壓")
plt.tight_layout()
plt.savefig(FIG_DIR / "ch08_independent_sbp_boxplot.png", dpi=300)
plt.show()
Figure 8.1: 兩獨立樣本:標準照護與介入照護在 12 週後的收縮壓分布。
welch_result = ttest_ind(intervention, standard, equal_var=False)
mean_diff = intervention.mean() - standard.mean()
se_welch = np.sqrt(intervention.var(ddof=1) / len(intervention) + standard.var(ddof=1) / len(standard))
df_welch = (intervention.var(ddof=1) / len(intervention) + standard.var(ddof=1) / len(standard)) ** 2 / (
    (intervention.var(ddof=1) / len(intervention)) ** 2 / (len(intervention) - 1)
    + (standard.var(ddof=1) / len(standard)) ** 2 / (len(standard) - 1)
)
ci_mean = t.interval(0.95, df=df_welch, loc=mean_diff, scale=se_welch)

pd.DataFrame(
    {
        "quantity": ["平均差:介入-標準", "Welch t 統計量", "自由度", "p 值", "95% CI 下限", "95% CI 上限"],
        "value": [mean_diff, welch_result.statistic, df_welch, welch_result.pvalue, ci_mean[0], ci_mean[1]],
    }
).round(4)
quantity value
0 平均差:介入-標準 -10.1875
1 Welch t 統計量 -6.0526
2 自由度 29.1220
3 p 值 0.0000
4 95% CI 下限 -13.6294
5 95% CI 上限 -6.7456

平均差為負值,代表介入組平均收縮壓較低。p 值與信賴區間都提供證據支持兩組平均值不同。不過,接著仍要問:下降幅度是否達到臨床上重要?是否有副作用、成本或可行性問題?統計顯著是門票,不是整場演出。

8.5 Pooled t 檢定與 Welch t 檢定

Pooled t 檢定假設兩組母群體變異數相同,會把兩組變異合併估計共同變異。Welch t 檢定不要求兩組變異數相等,並使用調整後自由度。實務上,若不確定變異數是否相等,Welch t 檢定通常較安全。

假設變異數相等與否不應只靠「看起來差不多」。更重要的是研究設計與資料來源是否支持這個假設。醫療資料裡,不同病情嚴重度、不同治療路徑常讓變異不一樣。

pooled_result = ttest_ind(intervention, standard, equal_var=True)

pd.DataFrame(
    {
        "method": ["Pooled t-test", "Welch t-test"],
        "t_statistic": [pooled_result.statistic, welch_result.statistic],
        "p_value": [pooled_result.pvalue, welch_result.pvalue],
    }
).round(4)
method t_statistic p_value
0 Pooled t-test -6.0526 0.0
1 Welch t-test -6.0526 0.0

8.6 配對樣本平均值比較

配對 t 檢定適用於同一個體的前後測量,或自然配對的資料,例如左右眼、同一病人兩種檢驗方法、同一社區介入前後。配對 t 檢定的關鍵不是比較兩組原始平均值,而是比較「差值」的平均是否為 0。

\(D_i = X_{after,i} - X_{before,i}\),假設為:

\[ H_0: \mu_D = 0 \]

\[ H_1: \mu_D \ne 0 \]

若差值平均顯著小於 0,代表治療後數值下降。

8.7 範例 2:同一批病人治療前後收縮壓

before = np.array([152, 148, 155, 146, 150, 158, 149, 153, 147, 156, 151, 154])
after = np.array([140, 138, 144, 139, 141, 146, 137, 142, 136, 145, 139, 143])
paired_df = pd.DataFrame({"id": np.arange(1, len(before) + 1), "before": before, "after": after})
paired_df["difference"] = paired_df["after"] - paired_df["before"]

paired_df[["before", "after", "difference"]].agg(["mean", "std"]).round(2)
before after difference
mean 151.58 140.83 -10.75
std 3.75 3.21 1.48
paired_long = paired_df.melt(id_vars="id", value_vars=["before", "after"], var_name="time", value_name="sbp")
paired_long["time"] = paired_long["time"].map({"before": "治療前", "after": "治療後"})

plt.figure(figsize=(7, 4.5))
for _, row in paired_df.iterrows():
    plt.plot(["治療前", "治療後"], [row["before"], row["after"]], color="#999999", alpha=0.65)
sns.pointplot(data=paired_long, x="time", y="sbp", errorbar=("ci", 95), color="#d62828", markers="o")
plt.xlabel("時間")
plt.ylabel("收縮壓 (mmHg)")
plt.title("配對樣本:同一批病人治療前後收縮壓")
plt.tight_layout()
plt.savefig(FIG_DIR / "ch08_paired_sbp_lines.png", dpi=300)
plt.show()
Figure 8.2: 配對樣本:同一批病人治療前後收縮壓。灰線連接同一位病人。
paired_result = ttest_rel(after, before)
diff_mean = paired_df["difference"].mean()
diff_se = paired_df["difference"].std(ddof=1) / np.sqrt(len(paired_df))
diff_ci = t.interval(0.95, df=len(paired_df) - 1, loc=diff_mean, scale=diff_se)

pd.DataFrame(
    {
        "quantity": ["平均差:後-前", "配對 t 統計量", "p 值", "95% CI 下限", "95% CI 上限"],
        "value": [diff_mean, paired_result.statistic, paired_result.pvalue, diff_ci[0], diff_ci[1]],
    }
).round(4)
quantity value
0 平均差:後-前 -10.7500
1 配對 t 統計量 -25.0807
2 p 值 0.0000
3 95% CI 下限 -11.6934
4 95% CI 上限 -9.8066

配對分析通常比把前後資料當獨立樣本更有效率,因為每位病人都作為自己的對照。個體間差異被抵消後,治療前後差異更容易被看見。

8.8 兩比例比較

若結果是二元事件,例如再住院、有無感染、是否達標,常比較兩組比例。常見效果量包括比例差、相對風險與勝算比。本章先聚焦比例差與相對風險。

比例差又稱風險差 (risk difference):

\[ \hat{p}_2 - \hat{p}_1 \]

相對風險 (relative risk, RR) 為:

\[ RR = \frac{\hat{p}_2}{\hat{p}_1} \]

比例差回答「絕對差多少」,相對風險回答「相對於原本是多少倍」。臨床與公衛決策常需要兩者一起看。

8.9 範例 3:兩組 30 天再住院率

prop_df = pd.DataFrame(
    {
        "group": ["標準照護", "介入照護"],
        "n": [180, 175],
        "readmit": [42, 26],
    }
)
prop_df["rate"] = prop_df["readmit"] / prop_df["n"]

prop_df
group n readmit rate
0 標準照護 180 42 0.233333
1 介入照護 175 26 0.148571
plt.figure(figsize=(7, 4.5))
sns.barplot(data=prop_df, x="group", y="rate", color="#8ab17d")
plt.ylim(0, 0.32)
plt.xlabel("照護組別")
plt.ylabel("30 天再住院率")
plt.title("兩組 30 天再住院率")
for index, row in prop_df.iterrows():
    plt.text(index, row["rate"] + 0.015, f"{row['rate']:.1%}", ha="center")
plt.tight_layout()
plt.savefig(FIG_DIR / "ch08_two_proportion_readmission.png", dpi=300)
plt.show()
Figure 8.3: 兩組 30 天再住院率。
x1, n1 = prop_df.loc[0, ["readmit", "n"]]
x2, n2 = prop_df.loc[1, ["readmit", "n"]]
p1 = x1 / n1
p2 = x2 / n2
pooled_p = (x1 + x2) / (n1 + n2)
se_null = np.sqrt(pooled_p * (1 - pooled_p) * (1 / n1 + 1 / n2))
z_stat = (p2 - p1) / se_null
p_value = 2 * min(norm.cdf(z_stat), 1 - norm.cdf(z_stat))

diff_prop = p2 - p1
se_diff_prop = np.sqrt(p1 * (1 - p1) / n1 + p2 * (1 - p2) / n2)
ci_prop = (diff_prop - norm.ppf(0.975) * se_diff_prop, diff_prop + norm.ppf(0.975) * se_diff_prop)
relative_risk = p2 / p1

pd.DataFrame(
    {
        "quantity": ["比例差:介入-標準", "比例差 CI 下限", "比例差 CI 上限", "z 統計量", "p 值", "相對風險"],
        "value": [diff_prop, ci_prop[0], ci_prop[1], z_stat, p_value, relative_risk],
    }
).round(4)
quantity value
0 比例差:介入-標準 -0.0848
1 比例差 CI 下限 -0.1660
2 比例差 CI 上限 -0.0036
3 z 統計量 -2.0290
4 p 值 0.0425
5 相對風險 0.6367

比例差為負值,表示介入照護組再住院率較低。相對風險小於 1,也支持介入組風險較低。實務上,比例差常更直觀,因為它可以轉成每 100 位病人少幾位再住院。

8.10 效果量與信賴區間

兩樣本推論的重點不只是 p 值,而是效果大小與不確定性。平均差、比例差、相對風險都可以搭配信賴區間呈現。

effect_df = pd.DataFrame(
    {
        "outcome": ["收縮壓平均差\n介入-標準", "再住院率差\n介入-標準"],
        "estimate": [mean_diff, diff_prop],
        "lower": [ci_mean[0], ci_prop[0]],
        "upper": [ci_mean[1], ci_prop[1]],
    }
)

plt.figure(figsize=(7, 4.5))
for index, row in effect_df.iterrows():
    plt.plot([row["lower"], row["upper"]], [index, index], color="#264653", linewidth=2)
    plt.scatter(row["estimate"], index, color="#d62828", s=60)
plt.axvline(0, color="#666666", linestyle="--")
plt.yticks(range(len(effect_df)), effect_df["outcome"])
plt.xlabel("效果估計與 95% 信賴區間")
plt.title("兩樣本推論的效果量")
plt.tight_layout()
plt.savefig(FIG_DIR / "ch08_effect_estimates_ci.png", dpi=300)
plt.show()
Figure 8.4: 兩樣本推論的效果量與 95% 信賴區間。垂直虛線為沒有差異。

這張圖的尺度不同,主要用來示範概念:效果量最好搭配信賴區間一起報告。若信賴區間很寬,代表不確定性大;若信賴區間跨過 0,代表資料仍與沒有差異相容。

8.11 常見陷阱

  1. 把配對資料當獨立資料分析:會浪費資訊,甚至錯估標準誤。
  2. 只報 p 值不報平均差或比例差:讀者不知道差異大小。
  3. 忽略基準風險:相同相對風險在不同基準風險下,絕對影響不同。
  4. 未檢查變異數與分布形狀:極端偏態或離群值會影響 t 檢定。
  5. 把統計顯著當成治療值得採用:仍需考慮副作用、成本、可行性與病人偏好。

8.12 本章重點整理

  • 兩組比較前先判斷資料是獨立樣本或配對樣本。
  • Welch t 檢定適合兩獨立樣本平均值比較,且不要求兩組變異數相等。
  • 配對 t 檢定分析的是同一個體的差值。
  • 兩比例比較可用比例差、相對風險與 p 值描述。
  • 效果大小與信賴區間比單獨 p 值提供更多臨床資訊。
  • 統計顯著不等於臨床顯著,兩者應分開討論。

8.13 小練習

  1. 說明獨立樣本與配對樣本的差異,並各舉一個醫學例子。
  2. 某研究兩組平均 LDL-C 分別為 108 與 121 mg/dL,你會如何定義平均差方向?
  3. 若兩組再住院率分別為 12% 與 18%,計算比例差與相對風險。
  4. 說明為何配對 t 檢定通常比未配對分析更有效率。
  5. 解釋「p 值顯著但比例差只有 1%」在臨床上可能代表什麼。
p_standard = 0.18
p_intervention = 0.12
practice_risk_difference = p_intervention - p_standard
practice_relative_risk = p_intervention / p_standard

pd.DataFrame(
    {
        "quantity": ["比例差:介入-標準", "相對風險:介入/標準"],
        "value": [practice_risk_difference, practice_relative_risk],
    }
).round(4)
quantity value
0 比例差:介入-標準 -0.0600
1 相對風險:介入/標準 0.6667

8.14 Glossary

中文術語 English term 說明
兩樣本推論 two-sample inference 比較兩組母群體參數的推論方法。
獨立樣本 independent samples 兩組觀察值來自不同個體且彼此獨立。
配對樣本 paired samples 兩個觀察值具有自然配對關係,例如同一人前後測。
兩獨立樣本 t 檢定 two-sample t-test 比較兩獨立樣本平均值的 t 檢定。
Welch t 檢定 Welch's t-test 不假設兩組變異數相等的兩樣本 t 檢定。
Pooled t 檢定 pooled t-test 假設兩組變異數相等並合併變異估計的 t 檢定。
配對 t 檢定 paired t-test 比較配對差值平均是否為 0 的 t 檢定。
平均差 mean difference 兩組平均值之差。
比例差 risk difference 兩組事件比例之差,也稱風險差。
相對風險 relative risk 兩組事件比例的比值。
效果量 effect size 差異或關聯大小的量化指標。
信賴區間 confidence interval 量化估計不確定性的區間。
統計顯著性 statistical significance 結果達到預設顯著水準的狀態。
臨床顯著性 clinical significance 結果是否具有實際臨床重要性。