威威學長|數據分析動手做(高三)
📚 系列總覽:數據分析系列地圖|⬅️ 上一篇:用投影矩陣證明決定係數的幾何意義|➡️ 下一篇:係數說的是「其他變數固定時」的效果
到目前為止,這一系列裡的每個數字你都是手算的:$\bar x$、$\sum(x_i-\bar x)(y_i-\bar y)$、斜率、殘差。真實工作不會這樣做,一行程式就解決了。
但第一次寫迴歸的程式,最危險的地方不是寫錯,而是跑出數字就相信它。程式不會告訴你它有沒有搞錯係數順序、有沒有用錯分母;它只會很篤定地印出一個數字。
所以這篇的做法是:先挑一組小到你可以手算的資料,把電腦算出來的每一個數字都跟手算的答案對一次。對上了,你才有資格相信它在別的資料上也對。
先挑一組可以對帳的資料
$$x=1,\ 2,\ 3,\ 4,\ 5,\qquad y=2,\ 4,\ 5,\ 4,\ 5$$
這組資料的手算答案($\bar x=3$、$\bar y=4$,所以 $\sum(x_i-\bar x)(y_i-\bar y)=6$、$\sum(x_i-\bar x)^2=10$):
$$\hat y=2.2+0.6x,\qquad SSE=2.40,\qquad R^2=0.60,\qquad s=\sqrt{0.8}\approx0.894,\qquad r\approx0.7746$$
底下每一段程式跑完,都應該給出這些數字。對不上就是有問題,不要放過。
先用純 Python 算一遍
不用任何套件,把公式直接寫出來。這樣做的好處是:程式裡每一行你都能對回公式,出錯時知道去哪一行找。
x = [1, 2, 3, 4, 5]
y = [2, 4, 5, 4, 5]
n = len(x)
mx = sum(x) / n
my = sum(y) / n
sxy = sum((xi - mx) * (yi - my) for xi, yi in zip(x, y))
sxx = sum((xi - mx) ** 2 for xi in x)
a = sxy / sxx # 斜率
b = my - a * mx # 截距:直線一定會穿過 (x̄, ȳ)
fitted = [a * xi + b for xi in x]
resid = [yi - fi for yi, fi in zip(y, fitted)]
sse = sum(e * e for e in resid)
sst = sum((yi - my) ** 2 for yi in y)
r2 = 1 - sse / sst
s = (sse / (n - 2)) ** 0.5
兩個地方值得指一下。
b = my - a * mx 這一行不是隨便寫的:它來自「最適直線一定通過 $(\bar x,\bar y)$」這件事。所以只要先有斜率,截距就是把它代入那個點解出來的。你如果看過斜率與相關係數的關係,會發現同一件事也在那裡出現。
s = (sse / (n - 2)) ** 0.5 用的是 $n-2$,不是 $n$。原因跟殘差分析那篇講的一樣:你花了兩個係數去配這條線,自由度就少兩個。
它實際印出來的東西
把上面那些變數印出來,輸出是這樣(這是真的跑出來的,不是我在這裡編的):
x̄ = 3.0 ȳ = 4.0
Σ(x-x̄)(y-ȳ) = 6.0
Σ(x-x̄)² = 10.0
斜率 a = 0.6
截距 b = 2.2
x y ŷ e
1 2 2.80 -0.80
2 4 3.40 0.60
3 5 4.00 1.00
4 4 4.60 -0.60
5 5 5.20 -0.20
Σe = -0.0
SSE = 2.40
SST = 6.00
R² = 0.6000
s = 0.894427
r = 0.774597
跟手算的答案一項一項比:$a=0.6$ ✓、$b=2.2$ ✓、殘差 $-0.8,\ 0.6,\ 1.0,\ -0.6,\ -0.2$ ✓、$SSE=2.40$ ✓、$SST=6.00$ ✓、$R^2=0.6000$ ✓、$s\approx0.894$ ✓。
還有一行要特別看:Σe = -0.0。殘差加起來應該是 $0$,它印出「負零」是因為浮點數的關係:電腦用二進位表示小數,加總的順序不同就會留下極小的誤差。看到 -0.0 不是錯誤,但這也提醒你:電腦上的「等於」永遠要加上一個容差。
一行做完,但要看清楚它回傳什麼
有 numpy 的話,整件事可以縮成一行:
import numpy as np
slope, intercept = np.polyfit(x, y, 1)
跑出來是:
numpy polyfit -> slope=0.600000 intercept=2.200000
matches hand-rolled: True
matches ... True 是我在程式裡真的去比對兩者的差是否小於 $10^{-12}$,不是目測。
⚠️ 學長的警告:np.polyfit 回傳的順序是次方由高到低。配一次多項式時它剛好是「斜率、截距」,所以看起來很自然;但等你配二次式,它會變成「二次項、一次項、常數項」。如果你還是照「斜率、截距」去解讀,係數會整個對錯位置。這件事在多項式迴歸裡很容易踩到。
三個常見的錯誤
第一,把係數順序記反。 上面說過了,而且它不會報錯,只會給你一組看起來很合理的錯答案。自保的方法只有一個:拿一組你手算過的資料去對。 這就是這整篇為什麼要用那麼小的資料集。
第二,用錯分母算 $s$。 除 $n$ 會讓你低估誤差,於是後面所有的信心判斷都過度樂觀。記法很簡單:你有幾個係數,自由度就少幾個。
第三,把 $R^2$ 當成成績單。 換一個更複雜的模型 $R^2$ 一定會更高,所以它單獨看沒有篩選能力。這件事在多項式迴歸與曲線配適裡展開過,而它不是理論上的細節,是實務上最常見的誤判。
換你動手
練習 1:把上面的純 Python 版本改成用輸入的資料,然後拿這組 $x=1,2,3,4,5$、$y=2,4,5,4,5$ 跑,確認印出來是 $a=0.6$、$b=2.2$。
先確認你有把 sxx 跟 sxy 分清楚:$sxx$ 是 $\sum(x_i-\bar x)^2=10$,$sxy$ 是 $\sum(x_i-\bar x)(y_i-\bar y)=6$。兩個都很容易在打字時寫反,而寫反的結果是斜率變成 $0.6$ 的倒數,會錯得很明顯,這算是幸運的錯法。
練習 2:承上,把 s = (sse / (n - 2)) ** 0.5 改成除以 n,看答案變成多少。兩個值差幾倍?
除 $n-2=3$ 得 $\sqrt{0.8}\approx0.8944$;除 $n=5$ 得 $\sqrt{0.48}\approx0.6928$。差了將近三成。這不是「差不多」,而是會影響你判斷某個點算不算異常的分界。
練習 3:把五個殘差印出來之後,用肉眼判斷這組殘差是不是隨機散開。
依 $x$ 的順序:$-0.80,\ 0.60,\ 1.00,\ -0.60,\ -0.20$。中間兩個是正的、兩端是負的,排起來是倒 U。
所以它不像隨機。 但只有五個點,這只是一個值得追的線索,不是結論。殘差分析那篇花了一整段在講這個訊號要怎麼判讀、以及為什麼不能過度解讀。
挑一組你真的有興趣的資料(任何你追蹤過的數字都行),用上面的純 Python 版本跑一遍。跑完之後,做一件程式不會幫你做的事:把 R²、s、以及五個殘差印出來,然後回頭讀一遍這篇的「三個常見錯誤」,確認自己沒有踩到任何一個。
如果你跑出來的係數跟預期差很多,先把資料的前五筆跟 a、b 貼給我,我們一起看是資料順序、還是分母、還是係數順序出了問題。