跳轉到

8.4 敘述統計#

本節目標#

  • 計算資料集的平均數、變異數、標準差與中位數
  • 用 SymPy 對 Python list 資料求統計量(符號精確值)
  • 理解樣本變異數為何除以 \(n-1\)
  • 實驗:離群值對各統計量的影響
  • 認識中央極限定理,並用骰子平均的小模擬驗證

平均數、變異數、標準差#

給定資料 \(x_1, x_2, \ldots, x_n\) ,最基本的敘述統計量:

平均數(mean):

\[ \bar{x} = \frac{1}{n} \sum_{i=1}^{n} x_i \]

變異數(variance)衡量資料對平均的離散程度:

\[ s^2 = \frac{1}{n - 1} \sum_{i=1}^{n} (x_i - \bar{x})^2 \]

標準差(standard deviation)是變異數的平方根,與資料同單位:

\[ s = \sqrt{s^2} \]

中位數(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#

母體變異數的定義是

\[ \sigma^2 = \frac{1}{N} \sum_{i=1}^{N} (x_i - \mu)^2 \]

但實務上我們只有樣本,且用樣本平均 \(\bar{x}\) 取代未知的母體平均 \(\mu\) 。問題來了: \(\bar{x}\) 是「最貼近這批資料」的中心——資料到 \(\bar{x}\) 的距離平方和,必然不大於到任何其他值(包括真實的 \(\mu\) )的距離平方和。因此用 \(\bar{x}\) 計算的離散度系統性地低估了真實變異數。

修正方法:分母從 \(n\) 改成 \(n - 1\) ,讓期望值剛好等於 \(\sigma^2\) (稱為不偏估計量):

\[ E[s^2] = \sigma^2, \quad \text{其中 } s^2 = \frac{1}{n-1} \sum (x_i - \bar{x})^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\) 夠大時,樣本平均

\[ \bar{X} = \frac{X_1 + \cdots + X_n}{n} \;\approx\; N\!\left(\mu, \frac{\sigma^2}{n}\right) \]

無論原本的分布長什麼樣(即使是偏斜、跳躍的骰子分布),平均的分布都會趨近常態,且標準差以 \(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}")

試著修改實驗:

  1. 把資料中最大的值改成它的 3 倍,重新計算,觀察 mean、median、std 各變化多少。
  2. 把你的資料畫成直方圖,再計算落在一個標準差內的比例,與 68% 比較(資料接近常態時才會相近)。
  3. 把 CLT 模擬中的 \(n\) 改成 \(2\) 、 \(5\) 、 \(100\) ,觀察鐘形出現的快慢。
  4. 對 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}\) 縮小

練習

  1. 資料 \([3, 5, 7, 7, 8]\) :手算 \(\bar{x}\) 、 \(s^2\) 、 \(s\) ,再用 SymPy 驗證。
  2. 證明:對任意常數 \(c\) , \(\sum (x_i - \bar{x})^2 \le \sum (x_i - c)^2\) (這是「低估」的根源)。提示:對 \(c\) 微分。
  3. 用 8.2 節的 Die('X') 與本節的 variance(),驗證「除以 \(n\) 的樣本變異數」其期望值是 \(\frac{n-1}{n} \sigma^2\) 。
  4. 蒐集兩份資料(例如你與家人的一週步數),分別計算 mean 與 std,用「平均 ± 2 個標準差」區間比較兩組的集中與離散。
  5. 模擬「擲 100 顆骰子取平均」10000 次,估計 \(P(3.4 < \bar{X} < 3.6)\) ,並與 CLT 常態近似 \(N(3.5, 35/1200)\) 的理論值對照。