跳轉到

9.3 假設檢定的計算實作#

概念說明#

假設檢定是統計推論的核心程序:先假設「無效果」(虛無假設 H₀),再計算「若 H₀ 為真,觀察到目前資料(或更極端)的機率」,即 p 值。p 值很小就拒絕 H₀——效果顯著。整個流程是純計算問題,完全可以用程式實作。

本章實作兩個最常用的檢定:

  1. 獨立樣本 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 檢定:

  1. 算兩組的 n、平均、樣本變異數。
  2. 計算 t 統計量與 Welch 自由度。
  3. 求 p = 2·P(T > |t|)(雙尾)。
  4. 與顯著水準 α(常取 0.05)比較,下結論。

卡方檢定:

  1. 計算各類別期望次數 Eᵢ(總次數 × 理論機率)。
  2. 累加 (Oᵢ − Eᵢ)²/Eᵢ 得 χ²。
  3. 查/算卡方右尾機率,得 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。

練習#

  1. 用 9.1 節的 LCG 產生 600 次骰子點數,做卡方檢定,判斷你的 LCG 亂數是否「夠均勻」;與 numpy 的結果比較 χ² 大小。
  2. 實作單樣本 t 檢定(檢驗樣本平均是否等於已知值 μ₀),並用模擬驗證:當 H₀ 為真時,重複 1 萬次實驗的 p 值分布應為均勻分布——這是檢定正確性的終極自我檢查。
  3. 用置換檢定重做本節的兩組比較,但把 n_perm 從 1000 加到 100 萬,畫出 p 值估計的收斂曲線,驗證 ±√(p(1−p)/N) 的理論誤差帶。