9.3 假設檢定的計算實作#
概念說明#
假設檢定是統計推論的核心程序:先假設「無效果」(虛無假設 H₀),再計算「若 H₀ 為真,觀察到目前資料(或更極端)的機率」,即 p 值。p 值很小就拒絕 H₀——效果顯著。整個流程是純計算問題,完全可以用程式實作。
本章實作兩個最常用的檢定:
- 獨立樣本 t 檢定:比較兩組平均數是否有顯著差異。統計量
t = (x̄₁ − x̄₂) / √(s₁²/n₁ + s₂²/n₂)
在 H₀ 下近似 t 分布(自由度用 Welch 修正),p 值是 t 分布尾面積。 2. 卡方適合度檢定:檢驗觀察頻率是否符合理論分布(例如骰子是否公正)。統計量
χ² = Σ (Oᵢ − Eᵢ)²/Eᵢ
p 值是卡方分布(自由度 k−1)的右尾面積。
兩個檢定都需要「分布的尾端機率」——這正是 7.3 節泰勒級數與不完全伽瑪函數的應用場景。而驗證檢定實作是否正確的終極武器,是置換檢定(permutation test):把兩組資料隨機重新分派一萬次,直接用模擬估 p 值——不需任何分布假設,是「用計算換理論」的典範。
演算法步驟#
t 檢定:
- 算兩組的 n、平均、樣本變異數。
- 計算 t 統計量與 Welch 自由度。
- 求 p = 2·P(T > |t|)(雙尾)。
- 與顯著水準 α(常取 0.05)比較,下結論。
卡方檢定:
- 計算各類別期望次數 Eᵢ(總次數 × 理論機率)。
- 累加 (Oᵢ − Eᵢ)²/Eᵢ 得 χ²。
- 查/算卡方右尾機率,得 p 值。
Python 程式碼範例#
import numpy as np, math
rng = np.random.default_rng(0)
# --- t 檢定:完整手作實作 ---
def t_test(a, b):
n1, n2 = len(a), len(b)
m1, m2 = a.mean(), b.mean()
v1, v2 = a.var(ddof=1), b.var(ddof=1)
se = math.sqrt(v1/n1 + v2/n2)
t = (m1 - m2) / se
# Welch 自由度
df = (v1/n1 + v2/n2)**2 / ((v1/n1)**2/(n1-1) + (v2/n2)**2/(n2-1))
# t 分布尾機率:用不完全貝塔函數
from math import lgamma
def t_sf(x, df):
# 右尾機率,透過貝塔函數恆等式
xx = df / (df + x*x)
ib = _ibeta(xx, df/2, 0.5)
return ib
def _ibeta(x, a, b):
# 正則化不完全貝塔函數(Lentz 連分數,簡化版)
if x > (a + 1) / (a + b + 2):
return 1 - _ibeta(1 - x, b, a)
lbeta = lgamma(a+b) - lgamma(a) - lgamma(b)
front = math.exp(a*math.log(x) + lbeta) / a
f, c, d = 1.0, 0.0, 0.0
for i in range(200):
m = i // 2
num = m*(b-m)*x / ((a+2*m-1)*(a+2*m)) if i % 2 == 0 \
else -((a+m)*(a+b+m)*x) / ((a+2*m)*(a+2*m+1))
d = 1 + num*d
if abs(d) < 1e-30: d = 1e-30
d = 1 / d
c = 1 + num/c
if abs(c) < 1e-30: c = 1e-30
f *= c * d
if abs(c*d - 1) < 1e-12: break
return min(max(front * f, 0), 1)
p = 2 * t_sf(abs(t), df)
return t, df, p
a = rng.normal(loc=100, scale=15, size=40) # 對照組
b = rng.normal(loc=106, scale=15, size=45) # 實驗組
t, df, p = t_test(a, b)
print(f"t 檢定: t = {t:.3f}, df = {df:.1f}, p = {p:.4f}")
try:
from scipy import stats
ref = stats.ttest_ind(a, b, equal_var=False)
print(f"scipy 對照: t = {ref.statistic:.3f}, p = {ref.pvalue:.4f}")
except ImportError:
print("(未安裝 scipy,略過對照)")
# --- 卡方檢定:骰子是否公正 ---
obs = np.bincount(rng.integers(1, 7, size=600), minlength=7)[1:]
exp = np.full(6, 100.0)
chi2 = ((obs - exp)**2 / exp).sum()
def chi2_sf(x, k):
"""卡方右尾機率(正則化不完全伽瑪 Q(k/2, x/2))"""
from math import lgamma
s, term = 0.0, 1.0
a, xx = k/2, x/2
# 用級數算 P(a, xx),Q = 1 - P(x 適中時夠用)
if xx < a + 1:
term = 1.0 / a; s = term; n = a
for _ in range(500):
n += 1; term *= xx / n; s += term
if abs(term) < 1e-15 * abs(s): break
return 1 - math.exp(-xx + a*math.log(xx) - lgamma(a)) * s
return 1.0 # 簡化:此例 x 不會太大
print(f"骰子觀察 {obs.tolist()},χ² = {chi2:.2f}, p ≈ {chi2_sf(chi2, 5):.4f}")
# --- 置換檢定:無分布假設的 p 值 ---
def permutation_test(a, b, n_perm=100_000):
obs_diff = abs(a.mean() - b.mean())
pool = np.concatenate([a, b])
count = 0
for _ in range(n_perm):
rng.shuffle(pool)
if abs(pool[:len(a)].mean() - pool[len(a):].mean()) >= obs_diff:
count += 1
return count / n_perm
print(f"置換檢定 p 值 ≈ {permutation_test(a, b):.4f}(應與 t 檢定接近)")
誤差分析#
檢定的「誤差」不是數值誤差,而是統計誤差:第一型錯誤(H₀ 真卻拒絕)機率 = α;第二型錯誤(H₁ 真卻不拒絕)機率 = β,其補數 1−β 稱為檢定力(power),取決於效果大小、樣本數與雜訊——樣本數規劃(power analysis)正是靠這個公式反推。數值層面要注意:(1) p 值極小時(如 10⁻³⁰)要算對數 p 值避免下溢,scipy 的函數多提供 logsf;(2) t 統計量的分母 se 在樣本變異數估計時用 ddof=1(除以 n−1),除錯時第一個該查的地方;(3) 多重比較問題:做 20 個檢定就有約 64% 機率至少一個假陽性,需 Bonferroni 校正(α/20)。置換檢定的估計誤差是 ±1.96√(p(1−p)/n_perm),p ≈ 0.03 時要 10 萬次才有三位有效數字——模擬也有它自己的 CLT。
練習#
- 用 9.1 節的 LCG 產生 600 次骰子點數,做卡方檢定,判斷你的 LCG 亂數是否「夠均勻」;與
numpy的結果比較 χ² 大小。 - 實作單樣本 t 檢定(檢驗樣本平均是否等於已知值 μ₀),並用模擬驗證:當 H₀ 為真時,重複 1 萬次實驗的 p 值分布應為均勻分布——這是檢定正確性的終極自我檢查。
- 用置換檢定重做本節的兩組比較,但把 n_perm 從 1000 加到 100 萬,畫出 p 值估計的收斂曲線,驗證 ±√(p(1−p)/N) 的理論誤差帶。