8.4 敘述統計#
本節目標#
- 計算資料集的平均數、變異數、標準差與中位數
- 用 SymPy 對 Python list 資料求統計量(符號精確值)
- 理解樣本變異數為何除以 \(n-1\)
- 實驗:離群值對各統計量的影響
- 認識中央極限定理,並用骰子平均的小模擬驗證
平均數、變異數、標準差#
給定資料 \(x_1, x_2, \ldots, x_n\) ,最基本的敘述統計量:
平均數(mean):
變異數(variance)衡量資料對平均的離散程度:
標準差(standard deviation)是變異數的平方根,與資料同單位:
中位數(median)是把資料排序後位於正中間的值( \(n\) 為偶數時取中間兩數的平均),它對極端值遠比平均數穩健。
用 Python list 資料計算統計量#
SymPy 的價值在精確的有理數與根號:把手寫公式配上 Rational,統計量就是精確值而非浮點近似:
from sympy import Rational, sqrt
data = [2, 4, 4, 4, 5, 5, 7, 9]
n = len(data)
xbar = Rational(sum(data), n) # 平均數
s2 = Rational(sum((x - xbar)**2 for x in data), n - 1) # 樣本變異數
s = sqrt(s2) # 標準差
print(n) # 8
print(xbar) # 5
print(s2) # 32/7
print(s) # 4*sqrt(14)/7
# 中位數:排序後取中間
sdata = sorted(data)
med = (Rational(sdata[n//2 - 1]) + Rational(sdata[n//2])) / 2 \
if n % 2 == 0 else Rational(sdata[n//2])
print(med) # 9/2
與 Python 標準庫 statistics 對照(注意 statistics.stdev 同樣除以 \(n-1\) ):
import statistics
data = [2, 4, 4, 4, 5, 5, 7, 9]
print(statistics.mean(data)) # 5
print(statistics.median(data)) # 4.5
print(statistics.stdev(data)) # 2.1380899...(浮點近似)
print(statistics.pvariance(data)) # 4.0(除以 n 的母體變異數,注意差別!)
對照可見:SymPy 回傳精確形式 \(4\sqrt{14}/7\) (約 \(2.1381\) ),而 statistics.stdev 是浮點近似——這正是第 1.1 節「符號計算 vs 數值計算」的差別。另外 statistics.pvariance 除以 \(n\) 、statistics.variance 除以 \(n-1\) ,兩者不同,下一小節解釋為什麼。
變異數為何除以 n-1#
母體變異數的定義是
但實務上我們只有樣本,且用樣本平均 \(\bar{x}\) 取代未知的母體平均 \(\mu\) 。問題來了: \(\bar{x}\) 是「最貼近這批資料」的中心——資料到 \(\bar{x}\) 的距離平方和,必然不大於到任何其他值(包括真實的 \(\mu\) )的距離平方和。因此用 \(\bar{x}\) 計算的離散度系統性地低估了真實變異數。
修正方法:分母從 \(n\) 改成 \(n - 1\) ,讓期望值剛好等於 \(\sigma^2\) (稱為不偏估計量):
用模擬直接驗證這件事(母體取自第 8.2 節的骰子, \(\mathrm{Var} = 35/12 \approx 2.917\) ):
import random
from sympy import Rational
random.seed(1)
N, n = 200_000, 5 # 重複 20 萬次、每次抽 5 顆骰子
pop_var = Rational(35, 12)
sum_n, sum_nm1 = 0, 0
for _ in range(N):
xs = [random.randint(1, 6) for _ in range(n)]
xb = sum(xs) / n
ss = sum((x - xb)**2 for x in xs)
sum_n += ss / n # 除以 n 的版本
sum_nm1 += ss / (n - 1) # 除以 n-1 的版本
print("母體變異數 :", pop_var.evalf(5)) # 2.9167
print("模擬:除以 n :", sum_n / N) # ≈ 2.333(偏低!)
print("模擬:除以 n-1 :", sum_nm1 / N) # ≈ 2.917(吻合)
除以 \(n\) 的版本平均只有 \(\frac{n-1}{n} \sigma^2\) (此處 \(\frac{4}{5} \times 2.917 \approx 2.333\) ),明顯偏低;除以 \(n-1\) 才對準母體變異數。
動手實驗:離群值效應#
極端值(outlier)對平均數與中位數的影響天差地遠。做一個對照實驗:
from sympy.stats import Normal, E, variance
import statistics
data = [68, 70, 71, 72, 73, 74, 76] # 一組正常資料
mean = statistics.mean
median = statistics.median
std = statistics.stdev
print("原始:")
print(" mean =", mean(data)) # 72
print(" median =", median(data)) # 72
print(" std =", round(std(data), 4)) # 2.6458
data_out = data + [200] # 加入一個離群值
print("加入 200 後:")
print(" mean =", mean(data_out)) # 88(被拉高 16!)
print(" median =", median(data_out)) # 72.5(幾乎不動)
print(" std =", round(std(data_out), 4)) # 45.32(暴增 17 倍)
# 觀察:mean 與 std 對離群值極敏感;median 穩健(robust)。
可以看到:一個 200 就把平均數從 72 拉到 88、標準差暴增 17 倍,但中位數幾乎紋風不動。這也是為什麼房價報導常用「中位數」而非「平均數」。
中央極限定理的概念與小模擬#
中央極限定理(Central Limit Theorem, CLT):若 \(X_1, \ldots, X_n\) 獨立同分布,平均數為 \(\mu\) 、變異數為 \(\sigma^2\) ,則當 \(n\) 夠大時,樣本平均
無論原本的分布長什麼樣(即使是偏斜、跳躍的骰子分布),平均的分布都會趨近常態,且標準差以 \(1/\sqrt{n}\) 縮小——這就是「多測量取平均更準」的數學根據。
用骰子做小模擬:擲 30 顆骰子取平均,重複 10000 次,畫出這些平均的直方圖:
import random
import matplotlib.pyplot as plt
from sympy.stats import Die, E, variance, Normal, density
from sympy import lambdify, Symbol
import numpy as np
random.seed(7)
X = Die('X')
mu, sig2 = float(E(X)), float(variance(X)) # 3.5, 35/12 ≈ 2.917
n, N = 30, 10_000
means = [sum(random.randint(1, 6) for _ in range(n)) / n
for _ in range(N)]
plt.hist(means, bins=40, density=True, alpha=0.6, label='模擬平均')
# CLT 預測的常態曲線:N(mu, sigma^2/n)
z = Symbol('x', real=True)
f = lambdify(z, density(Normal('Zc', mu, (sig2 / n) ** 0.5))(z), 'numpy')
xs = np.linspace(min(means), max(means), 200)
plt.plot(xs, f(xs), 'r-', lw=2, label='CLT 常態曲線')
plt.xlabel('30 顆骰子的平均')
plt.ylabel('density')
plt.legend()
plt.show()
print("模擬平均的平均:", sum(means) / N) # ≈ 3.5
print("模擬平均的標準差:", np.std(means)) # ≈ sqrt(2.917/30) ≈ 0.312
print("CLT 預測標準差 :", (sig2 / n) ** 0.5) # 0.3118
直方圖呈鐘形、與紅色常態曲線幾乎重合——即使單顆骰子是「六根等高柱子」的分布,30 顆的平均已經非常常態。
動手實驗:用自己蒐集的資料算統計量#
找一份自己感興趣的資料(例如:一週每天的通勤時間、班上同學的身高、每天使用手機的分鐘數),填進下面的程式計算完整統計量:
import statistics
from sympy import sqrt, Rational
# 例:一週通勤時間(分鐘),請換成你自己的資料
data = [25, 32, 28, 41, 35, 0, 0]
n = len(data)
xbar = statistics.mean(data)
med = statistics.median(data)
s2 = statistics.variance(data) # 除以 n-1
s = statistics.stdev(data)
print("筆數 n :", n)
print("平均數 :", xbar)
print("中位數 :", med)
print("變異數 :", s2)
print("標準差 :", round(s, 4))
print("最小值 :", min(data), " 最大值:", max(data))
print("全距 :", max(data) - min(data))
# 用 SymPy 給出精確的標準差
xbar_exact = Rational(sum(data), n)
s_exact = sqrt(Rational(sum((x - xbar_exact)**2 for x in data), n - 1))
print("標準差(精確):", s_exact, "≈", s_exact.evalf(4))
# 哪些資料落在「平均 ± 2 個標準差」內?
lo, hi = xbar - 2 * s, xbar + 2 * s
in_range = [x for x in data if lo <= x <= hi]
print(f"平均 ± 2s 區間: [{lo:.3f}, {hi:.3f}]")
print(f"區間內筆數: {len(in_range)} / {n}")
試著修改實驗:
- 把資料中最大的值改成它的 3 倍,重新計算,觀察 mean、median、std 各變化多少。
- 把你的資料畫成直方圖,再計算落在一個標準差內的比例,與 68% 比較(資料接近常態時才會相近)。
- 把 CLT 模擬中的 \(n\) 改成 \(2\) 、 \(5\) 、 \(100\) ,觀察鐘形出現的快慢。
- 對 CLT 模擬改用「兩顆骰子之和」當單位(原本就接近三角分布),看平均的分布多久變鐘形。
小結與練習#
- 敘述統計量: \(\bar{x}\) (中心)、 \(s^2\) 與 \(s\) (離散)、median(穩健的中心)
- SymPy 搭配
Rational回傳精確統計量;樣本變異數使用 \(n-1\) 分母(statistics.variance、statistics.stdev同) - 除以 \(n-1\) 是為了修正「用 \(\bar{x}\) 取代 \(\mu\) 」造成的低估,使 \(E[s^2] = \sigma^2\)
- 離群值強烈影響 mean 與 std,幾乎不影響 median
- 中央極限定理:樣本平均 \(\bar{X} \approx N(\mu, \sigma^2/n)\) ,標準差隨 \(1/\sqrt{n}\) 縮小
練習
- 資料 \([3, 5, 7, 7, 8]\) :手算 \(\bar{x}\) 、 \(s^2\) 、 \(s\) ,再用 SymPy 驗證。
- 證明:對任意常數 \(c\) , \(\sum (x_i - \bar{x})^2 \le \sum (x_i - c)^2\) (這是「低估」的根源)。提示:對 \(c\) 微分。
- 用 8.2 節的
Die('X')與本節的variance(),驗證「除以 \(n\) 的樣本變異數」其期望值是 \(\frac{n-1}{n} \sigma^2\) 。 - 蒐集兩份資料(例如你與家人的一週步數),分別計算 mean 與 std,用「平均 ± 2 個標準差」區間比較兩組的集中與離散。
- 模擬「擲 100 顆骰子取平均」10000 次,估計 \(P(3.4 < \bar{X} < 3.6)\) ,並與 CLT 常態近似 \(N(3.5, 35/1200)\) 的理論值對照。