統計應用|單元 6:假設檢定——用資料回答「是不是真的有差?」

SuhsiungAI學習教室 授課教師:蘇志雄
適用對象:大學部・統計初學者 實作工具:Python(numpy、scipy.stats、pandas、matplotlib)
預估學習時間:講義閱讀 90 分鐘+實作 90 分鐘
← 上一單元:單元 5:信賴區間——用一個區間說出「我們有多確定」


本單元學習目標

完成本單元後,你將能夠:

  1. 寫出虛無假設 H0 與對立假設 H1,並分辨雙尾與單尾檢定。
  2. 說明檢定統計量、p 值與顯著水準 α 的意義,並依 p 值做出結論。
  3. 區分型一錯誤、型二錯誤與檢定力,知道樣本數與效果大小如何影響檢定力。
  4. 用 scipy.stats 進行單一樣本 t 檢定、兩獨立樣本 Welch t 檢定、成對 t 檢定與比例 z 檢定。
  5. 說明假設檢定與信賴區間的對應關係。
  6. 了解多重檢定為什麼會讓「假陽性」暴增,以及 Bonferroni 校正。
  7. 用 numpy 模擬型一錯誤率與檢定力,親眼驗證理論。

一、為什麼需要假設檢定?

單元 5 的信賴區間回答「μ 大概在哪裡?」。但很多問題是是非題:

  • 😴 睡眠:大學生平均睡眠是否少於建議的 7 小時?
  • 👫 性別差異:男生和女生的平均睡眠時數真的不同,還是只是抽樣的偶然?
  • 📚 考試週:同一批同學在考試週是否睡得比較少?

假設檢定的邏輯像法庭的「無罪推定」:先假設「沒有效果、沒有差異」(被告無罪),除非資料提供的證據夠強,強到「如果真的沒差,這種結果很難出現」,才推翻這個假設。


二、假設檢定的五個步驟

步驟 內容 睡眠例子
1. 建立假設 H0:沒有差異(含等號);H1:研究想證明的主張 H0:μ = 7;H1:μ ≠ 7
2. 選定 α 可接受的型一錯誤機率,常用 0.05 α = 0.05
3. 計算檢定統計量 (估計值 − 假設值)/標準誤 t = (x̄ − 7)/(s/√n)
4. 計算 p 值 假設 H0 為真,得到「和觀察值一樣極端或更極端」結果的機率 查 t 分配(df = 29)
5. 下結論 p < α → 拒絕 H0;p ≥ α → 無法拒絕 H0 用文字回到原問題

💡 重點觀念:檢定統計量和信賴區間用的是同一個零件——標準誤。「差距是幾個標準誤?」差距越多個 SE,就越不像偶然。

雙尾與單尾

類型 H1 何時使用
雙尾 μ ≠ μ0 不預設方向,最常用、也最保守
左尾 μ < μ0 研究問題本來就只關心「比較小」
右尾 μ > μ0 研究問題本來就只關心「比較大」

⚠️ 單尾或雙尾必須在看資料之前決定。看到雙尾 p = 0.06 才改成單尾、讓 p 變成 0.03,是不誠實的作法。


三、p 值與顯著水準

p 值:在 H0 為真的前提下,得到目前這麼極端(或更極端)資料的機率。

p 值的常見誤解

錯誤說法 正確理解
「p = 0.03 代表 H0 為真的機率是 3%。」 p 值是假設 H0 為真時資料的機率,不是 H0 本身的機率。
「p > 0.05,所以證明了沒有差異。」 只是證據不足,「無法拒絕 H0」不等於「接受 H0」。樣本小、檢定力低時特別常見。
「p 越小,效果越大。」 p 值同時受效果大小與樣本數影響;樣本極大時,微不足道的差異也會顯著。請同時報告效果量與信賴區間。
「p = 0.049 和 p = 0.051 是完全不同的結論。」 證據強度幾乎一樣;0.05 只是慣例,不是魔法界線。

四、型一錯誤、型二錯誤與檢定力

H0 實際為真 H0 實際為假
拒絕 H0 ❌ 型一錯誤(機率 α,假陽性) ✅ 正確(機率 1 − β,即檢定力)
無法拒絕 H0 ✅ 正確(機率 1 − α) ❌ 型二錯誤(機率 β,假陰性)
  • α 由研究者事先設定;把 α 調小,型一錯誤變少,但型二錯誤會變多。
  • 檢定力 1 − β 越高越好,通常希望 ≥ 80%。提高檢定力的方法:增加樣本數、真實差距較大、資料變異較小、使用成對設計、α 較大。

五、常用的檢定方法

問題 方法 scipy 函式 檢定統計量
一組平均數 vs. 定值 單一樣本 t stats.ttest_1samp(x, mu0) t = (x̄ − μ0)/(s/√n),df = n − 1
兩組獨立樣本平均數 Welch t(不假設變異數相等) stats.ttest_ind(a, b, equal_var=False) t = (x̄1 − x̄2)/√(s1²/n1 + s2²/n2)
同一批人前後比較 成對 t stats.ttest_rel(before, after) 對差值 d 做單一樣本 t
一組比例 vs. 定值 比例 z 手算或 stats.binomtest z = (p̂ − p0)/√(p0(1 − p0)/n)

⚠️ 成對資料一定要用成對 t。同一個人前後兩次測量並不獨立,把它當成兩組獨立樣本會忽略「個人差異」,檢定力大幅下降(儲存格 6 有示範)。

💡 Welch t 檢定在兩組變異數相等時表現也很好,因此建議預設使用 Welch(equal_var=False)。


六、假設檢定與信賴區間:一體兩面

在相同的 α 下,雙尾檢定與信賴區間的結論一定一致:

μ0 落在 95% 信賴區間之外 ⇔ 雙尾檢定 p < 0.05

信賴區間的資訊更多:它不只告訴你「顯不顯著」,還告訴你差距大約有多大。報告時建議兩者並列。


七、多重檢定的陷阱

每個檢定在 H0 為真時都有 5% 機會誤判。如果同時做 20 個彼此獨立、其實都沒有差異的檢定,至少一個「顯著」的機率是:

1 − 0.95²⁰ ≈ 64%

這就是為什麼「把所有變數兩兩比一遍,挑顯著的寫成報告」(俗稱 p-hacking)會得到大量假陽性。最簡單的校正是 Bonferroni:做 m 個檢定時,每個檢定改用 α/m 當門檻。


八、Python 實作:檢定、模擬型一錯誤與檢定力

範例資料說明

本單元繼續使用單元 1~5 的 students.csv:30 位學生的「虛構」問卷資料(由程式隨機產生,非真實個資,人物與數值皆為虛構)。我們把這 30 人當成從全校抽出的樣本,檢定睡眠時數 sleep_hours、體溫 body_temp_c、運動次數 exercise_per_week 等。成對 t 檢定的「考試週睡眠時數」是以程式模擬的教學假設資料。

📥 下載範例資料:students.csv | 📥 完整程式碼:unit06.py

環境準備

本單元使用的套件和單元 5 相同。Google Colab 已內建;若在自己電腦執行,請先安裝:

pip install numpy scipy pandas matplotlib

把 students.csv 和程式放在同一個資料夾(使用 Colab 的同學,請先點左側📁圖示上傳 students.csv)。下面每一個程式區塊可以當成 Notebook 的一個儲存格(cell),請依序執行——因為所有模擬共用同一個亂數產生器,跳著執行會得到不同的數字。

儲存格 1:匯入套件與資料

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import stats

# 讓圖表可以顯示繁體中文(依序嘗試 Windows / macOS / Linux 的字型)
plt.rcParams["font.sans-serif"] = [
    "Microsoft JhengHei", "PingFang TC", "Noto Sans CJK TC",
    "Noto Serif CJK TC", "Arial Unicode MS", "DejaVu Sans",
]
plt.rcParams["axes.unicode_minus"] = False  # 讓負號正常顯示

pd.set_option("display.unicode.east_asian_width", True)
pd.set_option("display.width", 160)

# 固定種子,讓每次執行的模擬結果都一樣
rng = np.random.default_rng(2026)

df = pd.read_csv("students.csv", encoding="utf-8-sig")
sleep = df["sleep_hours"].to_numpy()
n = len(sleep)
print(f"n = {n},x̄ = {sleep.mean():.4f} 小時,s = {sleep.std(ddof=1):.4f} 小時")

執行結果:

n = 30,x̄ = 6.6867 小時,s = 0.8858 小時

說明:30 位同學平均睡 6.69 小時,比建議的 7 小時少約 0.31 小時。這個差距是真的,還是抽樣的偶然?

儲存格 2:單一樣本 t 檢定——平均睡眠是 7 小時嗎?

# 單一樣本 t 檢定:全校平均睡眠是否等於建議的 7 小時?
# H0: μ = 7  H1: μ ≠ 7(雙尾)
mu0 = 7
se = sleep.std(ddof=1) / np.sqrt(n)
t_stat = (sleep.mean() - mu0) / se
p_two = 2 * stats.t.sf(abs(t_stat), n - 1)
print(f"手算:t = ({sleep.mean():.4f} − {mu0}) / {se:.4f} = {t_stat:.4f},df = {n - 1},雙尾 p = {p_two:.4f}")
res = stats.ttest_1samp(sleep, popmean=mu0)
print(f"stats.ttest_1samp:t = {res.statistic:.4f},p = {res.pvalue:.4f}")
# 單尾:H1: μ < 7(「大學生睡不夠」)
res_less = stats.ttest_1samp(sleep, popmean=mu0, alternative="less")
print(f"單尾(less):p = {res_less.pvalue:.4f}")
alpha = 0.05
print("結論(α = 0.05,雙尾):", "拒絕 H0" if res.pvalue < alpha else "無法拒絕 H0")

執行結果:

手算:t = (6.6867 − 7) / 0.1617 = -1.9375,df = 29,雙尾 p = 0.0625
stats.ttest_1samp:t = -1.9375,p = 0.0625
單尾(less):p = 0.0312
結論(α = 0.05,雙尾): 無法拒絕 H0

說明:

  • t ≈ −1.94:樣本平均比 7 小時低了約 1.94 個標準誤。
  • 雙尾 p ≈ 0.0625 > 0.05,無法拒絕 H0:證據還不足以說全校平均不是 7 小時。
  • 若研究前就決定只關心「是否睡不夠」(左尾),p ≈ 0.031 < 0.05,可以拒絕 H0。這正說明單尾或雙尾要事先決定。

儲存格 3:檢定與信賴區間的對應

# 檢定與信賴區間是一體兩面:μ0 落在 95% CI 之外 ⇔ 雙尾 p < 0.05
lo, hi = res.confidence_interval(0.95)
print(f"95% CI = [{lo:.3f}, {hi:.3f}];μ0 = 7 {'在' if lo <= 7 <= hi else '不在'}區間內")
for m0 in [6.3, 6.5, 7.0, 7.1]:
    p = stats.ttest_1samp(sleep, m0).pvalue
    print(f"μ0 = {m0}:p = {p:.4f},{'在' if lo <= m0 <= hi else '不在'} CI 內")

執行結果:

95% CI = [6.356, 7.017];μ0 = 7 在區間內
μ0 = 6.3:p = 0.0235,不在 CI 內
μ0 = 6.5:p = 0.2578,在 CI 內
μ0 = 7.0:p = 0.0625,在 CI 內
μ0 = 7.1:p = 0.0161,不在 CI 內

說明:95% CI 為 [6.356, 7.017]。μ0 = 7.0、6.5 在區間內,p 都 > 0.05;μ0 = 6.3、7.1 在區間外,p 都 < 0.05——兩種方法的結論完全一致。

儲存格 4:單一樣本 t 檢定——平均體溫是 37.0 °C 嗎?

# 單一樣本 t 檢定:平均體溫是否等於 37.0 °C?
temp = df["body_temp_c"].to_numpy()
r = stats.ttest_1samp(temp, 37.0)
print(f"x̄ = {temp.mean():.3f} °C,t = {r.statistic:.3f},p = {r.pvalue:.2e}")

執行結果:

x̄ = 36.637 °C,t = -6.439,p = 4.81e-07

說明:樣本平均 36.64 °C,t ≈ −6.44,p 遠小於 0.05,拒絕 H0。在這份虛構資料中,平均體溫明顯低於 37.0 °C。

儲存格 5:兩獨立樣本 Welch t 檢定——男女睡眠時數

# 兩獨立樣本 t 檢定(Welch,不假設變異數相等):男女平均睡眠時數是否不同?
male = df.loc[df["gender"] == "男", "sleep_hours"].to_numpy()
female = df.loc[df["gender"] == "女", "sleep_hours"].to_numpy()
print(f"男:n = {len(male)},x̄ = {male.mean():.3f},s = {male.std(ddof=1):.3f}")
print(f"女:n = {len(female)},x̄ = {female.mean():.3f},s = {female.std(ddof=1):.3f}")
w = stats.ttest_ind(male, female, equal_var=False)
print(f"Welch t = {w.statistic:.4f},df = {w.df:.2f},p = {w.pvalue:.4f}")
ci = w.confidence_interval(0.95)
print(f"μ男 − μ女 的 95% CI = [{ci.low:.3f}, {ci.high:.3f}]")
s_pooled = np.sqrt(((len(male) - 1) * male.var(ddof=1) + (len(female) - 1) * female.var(ddof=1)) / (n - 2))
print(f"效果量 Cohen's d = {(male.mean() - female.mean()) / s_pooled:.3f}")

執行結果:

男:n = 13,x̄ = 6.992,s = 0.682
女:n = 17,x̄ = 6.453,s = 0.969
Welch t = 1.7874,df = 27.86,p = 0.0848
μ男 − μ女 的 95% CI = [-0.079, 1.158]
效果量 Cohen's d = 0.629

說明:

  • 男生平均多睡約 0.54 小時,但 p ≈ 0.085 > 0.05,無法拒絕 H0;差值的 95% CI [−0.08, 1.16] 包含 0。
  • Cohen’s d ≈ 0.63 屬於「中等」效果量——差距可能是真的,只是每組只有 13、17 人,檢定力不夠。「不顯著」不代表「沒有差異」。

儲存格 6:成對 t 檢定——考試週前後的睡眠(教學假設)

# 成對 t 檢定(教學假設資料):同一批 30 人在期中考週前後的睡眠時數
before = sleep
after = np.round(sleep - rng.normal(0.4, 0.6, size=n), 1)   # 模擬:考試週平均少睡約 0.4 小時
d = before - after
pr = stats.ttest_rel(before, after)
print(f"差值 d = 前 − 後:平均 = {d.mean():.3f},s_d = {d.std(ddof=1):.3f}")
print(f"成對 t = {pr.statistic:.4f},p = {pr.pvalue:.6f}")
print(f"等同於對 d 做單一樣本 t:p = {stats.ttest_1samp(d, 0).pvalue:.6f}")
wrong = stats.ttest_ind(before, after, equal_var=False)
print(f"(錯誤示範)當成兩獨立樣本:p = {wrong.pvalue:.4f}")

執行結果:

差值 d = 前 − 後:平均 = 0.397,s_d = 0.477
成對 t = 4.5578,p = 0.000087
等同於對 d 做單一樣本 t:p = 0.000087
(錯誤示範)當成兩獨立樣本:p = 0.1087

說明:

  • 成對 t 檢定的 p 極小,強烈拒絕 H0:考試週睡得比較少。它和「對差值 d 做單一樣本 t 檢定」完全相同。
  • 錯誤示範:同一份資料當成兩獨立樣本,p ≈ 0.11,變成「不顯著」!因為個人之間睡眠時數差很多,遮蓋了前後的變化;成對設計把個人差異消掉了。

儲存格 7:比例的 z 檢定——運動比例是 50% 嗎?

# 比例的 z 檢定:每週運動 ≥ 3 次的比例是否等於 50%?
k = int((df["exercise_per_week"] >= 3).sum())
p0 = 0.5
p_hat = k / n
z = (p_hat - p0) / np.sqrt(p0 * (1 - p0) / n)   # 注意:檢定的 SE 用 p0,不是 p̂
p_z = 2 * stats.norm.sf(abs(z))
print(f"p̂ = {k}/{n} = {p_hat:.4f},條件 n·p0 = {n * p0:.0f}、n(1−p0) = {n * (1 - p0):.0f}")
print(f"z = {z:.4f},雙尾 p = {p_z:.4f}")
print(f"精確二項檢定 stats.binomtest:p = {stats.binomtest(k, n, p0).pvalue:.4f}")

執行結果:

p̂ = 10/30 = 0.3333,條件 n·p0 = 15、n(1−p0) = 15
z = -1.8257,雙尾 p = 0.0679
精確二項檢定 stats.binomtest:p = 0.0987

說明:

  • 檢定時的標準誤用假設值 p0 計算(不是 p̂),條件也用 n·p0 ≥ 10 檢查。
  • z ≈ −1.83,p ≈ 0.068,無法拒絕 H0。小樣本時建議參考精確二項檢定(p ≈ 0.099)。

儲存格 8:模擬型一錯誤率

# 模擬型一錯誤率:H0 為真(μ = 7)時,重複 10,000 次,p < 0.05 的比例
reps = 10_000
mu_true, sd_true, n_sim = 7.0, 0.9, 30
sims = rng.normal(mu_true, sd_true, size=(reps, n_sim))
pvals = stats.ttest_1samp(sims, 7.0, axis=1).pvalue
print(f"H0 為真時,拒絕 H0 的比例(型一錯誤率)= {(pvals < 0.05).mean():.4f}(理論 α = 0.05)")
print(f"p 值分配大致均勻:p < 0.1 的比例 = {(pvals < 0.1).mean():.4f},p < 0.5 的比例 = {(pvals < 0.5).mean():.4f}")

執行結果:

H0 為真時,拒絕 H0 的比例(型一錯誤率)= 0.0514(理論 α = 0.05)
p 值分配大致均勻:p < 0.1 的比例 = 0.0977,p < 0.5 的比例 = 0.4957

說明:H0 為真時,10,000 次檢定中約 5.1% 誤判為顯著,和 α = 0.05 吻合。而且 H0 為真時 p 值近似均勻分配:p < 0.1 的比例約 10%,p < 0.5 約 50%。

儲存格 9:模擬檢定力

# 模擬檢定力:真實 μ = 6.7(比 7 少 0.3 小時)時,不同 n 拒絕 H0 的比例
rows = []
for nn in [10, 20, 30, 50, 100]:
    smp = rng.normal(6.7, sd_true, size=(5_000, nn))
    pw = (stats.ttest_1samp(smp, 7.0, axis=1).pvalue < 0.05).mean()
    rows.append({"n": nn, "檢定力(模擬)": pw, "型二錯誤率 β": 1 - pw})
print(pd.DataFrame(rows).round(4).to_string(index=False))

執行結果:

  n  檢定力(模擬)  型二錯誤率 β
 10          0.1558        0.8442
 20          0.3022        0.6978
 30          0.4156        0.5844
 50          0.6400        0.3600
100          0.9120        0.0880

說明:真實平均其實是 6.7 小時(比 7 少 0.3 小時)。n = 30 時,只有約 42% 的機會偵測到差異,型二錯誤率高達約 58%;n = 100 時檢定力才超過 90%。這解釋了儲存格 2 為什麼「不顯著」。

儲存格 10:多重檢定的假陽性

# 多重檢定:同時檢定 20 個「其實沒有差異」的假設,至少一個 p < 0.05 的機率
m_tests = 20
print(f"理論:1 − 0.95^{m_tests} = {1 - 0.95 ** m_tests:.4f}")
sim = rng.normal(0, 1, size=(2_000, m_tests, 15)), rng.normal(0, 1, size=(2_000, m_tests, 15))
pv = stats.ttest_ind(sim[0], sim[1], axis=2).pvalue
print(f"模擬:至少一個 p < 0.05 的比例 = {(pv < 0.05).any(axis=1).mean():.4f}")
print(f"Bonferroni(門檻 0.05/{m_tests} = {0.05 / m_tests}):比例 = {(pv < 0.05 / m_tests).any(axis=1).mean():.4f}")

執行結果:

理論:1 − 0.95^20 = 0.6415
模擬:至少一個 p < 0.05 的比例 = 0.6435
Bonferroni(門檻 0.05/20 = 0.0025):比例 = 0.0600

說明:20 個其實都沒有差異的檢定,至少一個 p < 0.05 的機率約 64%,模擬結果相符。用 Bonferroni 門檻 0.0025 後,降到約 6%,接近原本的 5%。

儲存格 11:畫出假設檢定

fig, axes = plt.subplots(2, 2, figsize=(13, 9))

# 左上:單一樣本 t 檢定的拒絕域與觀察到的 t
ax = axes[0, 0]
ts = np.linspace(-4, 4, 400)
ax.plot(ts, stats.t.pdf(ts, n - 1), color="black", lw=2, label=f"H0 下的 t 分配(df = {n - 1})")
tc = stats.t.ppf(0.975, n - 1)
for side in [ts <= -tc, ts >= tc]:
    ax.fill_between(ts[side], stats.t.pdf(ts[side], n - 1), color="#C44E52", alpha=0.5)
ax.fill_between(ts[ts <= -abs(t_stat)], stats.t.pdf(ts[ts <= -abs(t_stat)], n - 1), color="#4C72B0", alpha=0.4, label="p 值面積(雙尾)")
ax.fill_between(ts[ts >= abs(t_stat)], stats.t.pdf(ts[ts >= abs(t_stat)], n - 1), color="#4C72B0", alpha=0.4)
ax.axvline(t_stat, color="#4C72B0", linestyle="--", label=f"觀察 t = {t_stat:.2f}")
ax.set_title(f"睡眠 μ0 = 7:紅色為拒絕域(|t| > {tc:.3f}),p = {res.pvalue:.3f}")
ax.set_xlabel("t")
ax.set_ylabel("密度")
ax.legend(fontsize=9)

# 右上:H0 為真時 p 值的分配(均勻)
ax = axes[0, 1]
ax.hist(pvals, bins=20, range=(0, 1), color="#8DA0CB", edgecolor="white")
ax.hist(pvals[pvals < 0.05], bins=1, range=(0, 0.05), color="#C44E52", label=f"p < 0.05:{(pvals < 0.05).mean():.1%}")
ax.set_title("H0 為真時,10,000 個 p 值近似均勻分配")
ax.set_xlabel("p 值")
ax.set_ylabel("次數")
ax.legend(fontsize=9)

# 左下:檢定力曲線
ax = axes[1, 0]
ns = np.arange(5, 151, 5)
for diff, color in [(0.2, "#8DA0CB"), (0.3, "#4C72B0"), (0.5, "#1A4E80")]:
    dd = diff / sd_true
    pw = [stats.nct.sf(stats.t.ppf(0.975, k_ - 1), k_ - 1, dd * np.sqrt(k_)) + stats.nct.cdf(-stats.t.ppf(0.975, k_ - 1), k_ - 1, dd * np.sqrt(k_)) for k_ in ns]
    ax.plot(ns, pw, color=color, lw=2, label=f"真實差距 {diff} 小時")
ax.axhline(0.8, color="gray", linestyle=":", label="常用目標 80%")
ax.axhline(0.05, color="#C44E52", linestyle=":", label="α = 0.05")
ax.set_title("檢定力:差距越大、樣本越多,越容易偵測(σ = 0.9)")
ax.set_xlabel("樣本數 n")
ax.set_ylabel("檢定力 1 − β")
ax.legend(fontsize=9)

# 右下:男女睡眠時數與成對差值
ax = axes[1, 1]
ax.boxplot([male, female, d], tick_labels=["男 睡眠", "女 睡眠", "成對差值 前 − 後"])
ax.axhline(0, color="gray", lw=0.8)
ax.set_title(f"Welch p = {w.pvalue:.3f};成對 p = {pr.pvalue:.1e}")
ax.set_ylabel("小時")

plt.tight_layout()
plt.savefig("unit06_charts.png", dpi=120)
plt.show()

t 檢定的拒絕域與 p 值;H0 為真時 p 值的均勻分配;檢定力曲線;男女睡眠與成對差值盒形圖

圖表解讀:

  • 左上:紅色是 α = 0.05 的拒絕域;觀察到的 t = −1.94 沒有落入拒絕域,藍色的 p 值面積約 0.062。
  • 右上:H0 為真時,p 值平均散布在 0~1 之間,最左邊紅色那一格(約 5%)就是型一錯誤。
  • 左下:檢定力曲線,差距越大、樣本越多,越容易偵測;差距 0.3 小時要約 75 人才達 80% 檢定力。
  • 右下:男女睡眠時數分布重疊很多;成對差值則明顯在 0 以上。

九、本單元重點整理

主題 重點
假設 H0 含等號、代表「沒有差異」;H1 是想證明的主張;單尾雙尾要事先決定
p 值 H0 為真時,得到如此極端資料的機率;p < α 拒絕 H0
錯誤 型一=假陽性(α)、型二=假陰性(β);檢定力 = 1 − β
方法 單一樣本 t、Welch t、成對 t、比例 z
與 CI μ0 在 95% CI 外 ⇔ 雙尾 p < 0.05;報告時兩者並列
多重檢定 m 個檢定 → 假陽性暴增;Bonferroni 用 α/m
解讀 「無法拒絕」≠「證明相等」;顯著 ≠ 重要,要看效果量

十、練習題

第 1 題|身高的單一樣本 t 檢定
用 students.csv 的 height_cm,檢定全校平均身高是否為 165 cm(雙尾,α = 0.05),並寫出 H0、H1、t、p 與結論,再與 95% CI 對照。

第 2 題|生活費的 Welch t 檢定
男女的平均每月生活費 monthly_allowance 是否不同?請用 Welch t 檢定並下結論。

第 3 題|比例檢定
某手搖飲店宣稱「超過一半的大學生最愛珍奶」。隨機訪問 100 位大學生,58 人最愛珍奶(教學假設)。
(a) 計算 z 值。
(b) 分別計算雙尾與右尾 p 值。依店家的宣稱,該用哪一個?結論是什麼?

第 4 題|檢定力模擬
真實平均睡眠 6.7 小時、σ = 0.9(教學假設),用左尾檢定 H0:μ = 7、H1:μ < 7、α = 0.05。用種子 6 模擬 10,000 次,估計 n = 30 與 n = 80 的檢定力。

第 5 題|觀念題:判斷對錯
(a) p = 0.20,表示 H0 為真的機率是 20%。
(b) 無法拒絕 H0,就證明了兩組平均數相等。
(c) α 設得越小,型二錯誤的機率通常越大。
(d) 研究者比較 30 對變數,找到 2 個 p < 0.05,就應該把這 2 個當成重要發現發表。
(e) 同一批學生上課前後的測驗成績,應該用成對 t 檢定。


十一、練習題參考答案

📝 建議先自己作答,再對照答案喔!

第 1 題

import pandas as pd
from scipy import stats

df1 = pd.read_csv("students.csv", encoding="utf-8-sig")
h = df1["height_cm"]
r1 = stats.ttest_1samp(h, 165)
ci1 = r1.confidence_interval(0.95)
print(f"x̄ = {h.mean():.2f},t = {r1.statistic:.4f},df = {len(h) - 1},p = {r1.pvalue:.4f}")
print(f"95% CI = [{ci1.low:.2f}, {ci1.high:.2f}]")

執行結果:

x̄ = 164.21,t = -0.4834,df = 29,p = 0.6324
95% CI = [160.85, 167.56]

H0:μ = 165;H1:μ ≠ 165。t ≈ −0.48,p ≈ 0.63 > 0.05,無法拒絕 H0,沒有足夠證據說平均身高不是 165 cm。95% CI [160.85, 167.56] 包含 165,結論一致。

第 2 題

import pandas as pd
from scipy import stats

df2 = pd.read_csv("students.csv", encoding="utf-8-sig")
g = df2.groupby("gender")["monthly_allowance"]
print(g.agg(["count", "mean", "std"]).round(1).to_string())
r2 = stats.ttest_ind(df2.loc[df2["gender"] == "男", "monthly_allowance"],
                     df2.loc[df2["gender"] == "女", "monthly_allowance"], equal_var=False)
print(f"Welch t = {r2.statistic:.4f},df = {r2.df:.2f},p = {r2.pvalue:.4f}")

執行結果:

        count     mean     std
gender                        
女         17  10205.9  3428.1
男         13  10615.4  3658.1
Welch t = 0.3122,df = 25.05,p = 0.7575

男生平均只多約 410 元,Welch t ≈ 0.31、p ≈ 0.76,無法拒絕 H0。資料沒有顯示男女生活費有差異(但也不能「證明」相等)。

第 3 題

import numpy as np
from scipy import stats

k3, n3, p0 = 58, 100, 0.5
z3 = (k3 / n3 - p0) / np.sqrt(p0 * (1 - p0) / n3)
print(f"z = {z3:.3f}")
print(f"雙尾 p = {2 * stats.norm.sf(abs(z3)):.4f},單尾(H1: p > 0.5)p = {stats.norm.sf(z3):.4f}")

執行結果:

z = 1.600
雙尾 p = 0.1096,單尾(H1: p > 0.5)p = 0.0548

(a) z = (0.58 − 0.5)/√(0.25/100) = 1.6。
(b) 店家宣稱「超過一半」,H1:p > 0.5,應事先選右尾:p ≈ 0.055 > 0.05,無法拒絕 H0,證據還不足以支持店家的宣稱(雙尾 p ≈ 0.11 也一樣)。

第 4 題

import numpy as np
from scipy import stats

rng4 = np.random.default_rng(6)
for nn in [30, 80]:
    smp = rng4.normal(6.7, 0.9, size=(10_000, nn))
    pw = (stats.ttest_1samp(smp, 7.0, axis=1, alternative="less").pvalue < 0.05).mean()
    print(f"n = {nn}:單尾檢定力 ≈ {pw:.3f}")

執行結果:

n = 30:單尾檢定力 ≈ 0.557
n = 80:單尾檢定力 ≈ 0.900

左尾檢定把 α 全部放在一側,比雙尾更容易偵測同方向的差異:n = 30 時檢定力約 56%(雙尾約 42%),n = 80 時約 90%。

第 5 題

(a) 錯。p 值是「假設 H0 為真時」資料的機率,不是 H0 的機率。
(b) 錯。只代表證據不足,可能是檢定力不夠。
(c) 對。α 越小越難拒絕 H0,在 H0 為假時就越容易犯型二錯誤。
(d) 錯。30 個檢定在全部 H0 為真時,平均也會有 1.5 個 p < 0.05;應做多重檢定校正(Bonferroni 門檻 0.05/30 ≈ 0.0017)或用新資料重新驗證。
(e) 對。同一批人的前後測量彼此相關,屬於成對資料。

💡 小技巧:設計研究前,先模擬檢定力(像儲存格 9 和第 4 題),決定需要多少樣本,比事後才發現「不顯著」更有效率。


下一單元預告

t 檢定一次只能比較兩組。如果想比較三個以上的系所、年級的平均睡眠時數呢?兩兩做 t 檢定會掉進多重檢定的陷阱。單元 7 將介紹變異數分析 ANOVA:用一個 F 檢定同時比較多組平均數,並學習事後比較(post-hoc)方法。

← 回到單元 5:信賴區間——用一個區間說出「我們有多確定」

👉 單元 7:變異數分析 ANOVA


© SuhsiungAI學習教室(suhsiung.com)・統計應用課程・授課教師:蘇志雄。本單元範例資料 students.csv 為教學用虛構資料,與任何真實人物無關;考試週睡眠時數、珍奶調查、模擬母體等例子中的數字,除另有說明外,皆為教學用假設或簡化,不代表任何真實店家、機構或研究結果。