回想一下我們用多項式迴歸建模的經驗:一條 15 次多項式雖然很有彈性,但在邊界會「發瘋」──像一根綁住兩端的橡皮筋,中間晃得很厲害。有沒有辦法只在特定區間增加彈性,其他地方保持穩定?答案就是迴歸樣條(Regression Splines)。
迴歸樣條的核心思想是一個生活化的比喻:分段畫線,但在接點處黏在一起。想像你把一張紙撕成幾段,每段獨立畫一條曲線,然後用膠水在撕裂點(節點)上把相鄰的曲線平滑地接起來——這就是樣條。
在 §7.2 我們用階梯函數把 X 軸切成幾個 bin,每個 bin 內擬合一個常數。現在升級:每個 bin 內擬合一條多項式。
具體做法:在 X 軸上選定 K 個節點(knots),例如 age=50。X 軸被切成 K+1 個區間。在每個區間內,各自擬合一條獨立的 d 次多項式。
以分段三次多項式(piecewise cubic)為例,每個區間需要估計 4 個參數(\(\beta_0, \beta_1, \beta_2, \beta_3\))。如果有 1 個節點,總共有 2 個區間 × 4 個參數 = 8 個自由度。
為了解決斷層問題,我們逐步加上約束(constraints),每次約束會「吃掉」一個自由度:
| 約束層級 | 描述 | 自由度 (1 knot, cubic) |
|---|---|---|
| 無約束 | 各區間獨立三次多項式 | 2×4 = 8 |
| 連續性 | 函數值在節點處相等(不可有跳躍) | 8−1 = 7 |
| + 一階導數連續 | 加上斜率連續(不可有尖角) | 7−1 = 6 |
| + 二階導數連續 | 加上曲率連續 = 三次樣條 (cubic spline) | 6−1 = 5 |
課本 Figure 7.3 用四張圖完美展示這個演化過程:從左上(完全不連續,8 df)→ 右上(強制連續,7 df)→ 左下(三次樣條,5 df,肉眼都看不出接縫)→ 右下(線性樣條,2 df)。
線性樣條(linear spline)是 d=1 的特例:每個區間擬合直線,只在節點處強制連續。自由度 = 2 + K(每個區間 2 個參數,K 個約束)。雖然簡單,但在實際應用中常作為基線比較。
上面說的「分段擬合 + 加約束」聽起來有點複雜——到底要怎麼用 OLS 來實作?關鍵在基底函數(basis function)的巧妙設計。
最直觀的基底是截斷冪基底(truncated power basis):先從三次多項式的基礎基底 \(x, x^2, x^3\) 開始,然後每個節點加一個「開關」函數,只在超過節點時啟動:
這個設計的精妙之處:加上 \(\beta_4 h(x, \xi)\) 這一項後,只會在 \(\xi\) 處造成第三階導數的不連續,函數本身以及一階、二階導數都保持連續——自動滿足了三次樣條的所有約束!
因此,擬合一個有 K 個節點的三次樣條,就等於用 OLS 對 K+4 個預測變數(\(X, X^2, X^3, h(X,\xi_1), \dots, h(X,\xi_K)\))做線性迴歸。樣條看起來很複雜,實作上就是線性迴歸!
節點放哪裡、放幾個?這和 §5.1 的模型選擇問題如出一轍。
位置:直覺上應該在有劇烈變化的區域放更多節點。實務上最常見的做法是按分位數均勻放置——指定自由度 df,讓軟體自動在 df−1 個分位數位置上放置內部節點。
數量:用交叉驗證(CV)來選。課本 Figure 7.6 展示了用 10-fold CV 選擇自然三次樣條和三次樣條自由度在 Wage 資料上的結果:
三次樣條在邊界(X 很小或很大時)有一個問題:變異數很大,信賴區間會爆炸(見課本 Figure 7.4)。想像你在資料稀疏的邊界區域硬要彎曲——沒有足夠資料支撐時,彎曲就變成亂猜。
自然三次樣條(natural cubic spline)的解法:在邊界區域(小於最小節點、大於最大節點)強制函數變成線性。這兩個額外約束讓邊界估計更穩定,信賴區間顯著變窄。
實務上,自然三次樣條幾乎是預設選擇——邊界穩定性勝過那一點點額外的彈性損失。
課本 Figure 7.7 做了一個漂亮的對照:15 df 的自然三次樣條 vs 15 次多項式,都在 Wage 資料上擬合。結果:
| 特性 | 多項式迴歸 | 迴歸樣條 |
|---|---|---|
| 彈性來源 | 提高多項式次數(X¹⁵) | 增加節點數(保持三次) |
| 影響範圍 | 全局——一處改動影響整條曲線 | 局部——只在節點附近產生變化 |
| 邊界行為 | 高次多項式在邊界極不穩定 | 可控(自然樣條在邊界強制線性) |
| 自由度 | 次數 d → d+1 df(含截距) | K 節點 → 4+K df(三次樣條) |
| 計算複雜度 | 低(僅需 X 的冪次) | 低(OLS on 轉換後預測變數) |
以下的程式碼展示如何使用 patsy 的 bs() 和 cr() 函數在 Wage 資料上擬合三次樣條和自然三次樣條。
# 7.4 三次樣條與自然三次樣條 — 模型擬合
try:
from google.colab import drive
drive.mount('/content/drive')
DATA_PATH = '/content/drive/MyDrive/ISLP_data/'
except ImportError:
DATA_PATH = '/tmp/'
import numpy as np
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from sklearn.preprocessing import SplineTransformer
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import make_pipeline
from ISLP import load_data
Wage = load_data('Wage')
age = Wage['age'].values.reshape(-1, 1)
wage = Wage['wage'].values
# 三次樣條:Cubic spline with 3 interior knots = 7 df (3 + 4 basis)
spline_cs = SplineTransformer(n_knots=3, degree=3, extrapolation='continue')
model_cs = make_pipeline(spline_cs, LinearRegression()).fit(age, wage)
pred_cs = model_cs.predict(np.linspace(18, 80, 200).reshape(-1, 1))
# 自然三次樣條:sklearn 不直接支援 Natural Spline
# 改用較少 knots 的 cubic spline 近似(4 df = 1 interior knot)
spline_ns = SplineTransformer(n_knots=2, degree=3, extrapolation='linear')
model_ns = make_pipeline(spline_ns, LinearRegression()).fit(age, wage)
pred_ns = model_ns.predict(np.linspace(18, 80, 200).reshape(-1, 1))
print('Cubic Spline (3 knots=7 df): R2=%.4f' % model_cs.score(age, wage))
print('Cubic Spline (2 knots=6 df): R2=%.4f' % model_ns.score(age, wage))
print('Fewer knots = less flexible but more stable at boundaries (Fig 7.4)')
# 繪圖
age_grid = np.linspace(18, 80, 200)
fig, ax = plt.subplots(figsize=(10, 5))
ax.scatter(age, wage, s=0.5, alpha=0.3, color='#8b949e')
ax.plot(age_grid, pred_cs, color='#58a6ff', lw=2, label='Cubic Spline (3 knots)')
ax.plot(age_grid, pred_ns, color='#3fb950', lw=2, label='Minimal Spline (2 knots)')
ax.set_xlabel('Age'); ax.set_ylabel('Wage')
ax.set_title('Cubic Spline on Wage Data (sklearn)')
ax.legend(); plt.tight_layout(); plt.show()
# 用 CV 選擇樣條節點數
try:
from google.colab import drive
drive.mount('/content/drive')
DATA_PATH = '/content/drive/MyDrive/ISLP_data/'
except ImportError:
DATA_PATH = '/tmp/'
import numpy as np
from sklearn.preprocessing import SplineTransformer
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import cross_val_score
from ISLP import load_data
Wage = load_data('Wage')
age = Wage['age'].values.reshape(-1, 1)
wage = Wage['wage'].values
# 嘗試不同節點數 (n_knots: 2-6 → df: 6-10 for cubic)
knots_range = range(2, 7)
cv_results = []
for nk in knots_range:
spline = SplineTransformer(n_knots=nk, degree=3, extrapolation='linear')
X = spline.fit_transform(age)
mse = -cross_val_score(LinearRegression(), X, wage,
cv=10, scoring='neg_mean_squared_error').mean()
cv_results.append((nk, round(mse, 1)))
print('Cubic Spline CV MSE by n_knots (cubic degree=3):')
for nk, mse in cv_results:
print(f' n_knots={nk} (df~{nk+4}): MSE={mse}')
best = min(cv_results, key=lambda x: x[1])
print(f'Best: n_knots={best[0]} (df~{best[0]+4}), CV MSE={best[1]}')
print('課本 Figure 7.6: MSE flattens quickly after df=3-4')
兒童身高隨年齡的變化在不同發育階段差異極大(嬰兒期快速成長 → 穩定期 → 青春期衝刺 → 趨緩)。在每個階段放入節點的三次樣條,比全局多項式更能準確捕捉各階段的生長速率變化。WHO 的生長標準曲線本質上就是一種平滑樣條。
不同到期日的債券殖利率形成一條曲線。短期(1 年內)變化劇烈、中期平緩、長期(20–30 年)再次變化。在 1 年、5 年、10 年處設節點的三次樣條,是業界建構殖利率曲線的標準工具(如 Nelson-Siegel 模型就是一種參數化的樣條)。
全球平均溫度在 20 世紀初平穩、1970 年代後加速上升。在 1970 年放一個節點的線性樣條,可以同時捕捉「之前緩慢暖化」和「之後快速暖化」兩個階段,避免單一趨勢線平滑掉關鍵的轉折點。
| 方法 | 彈性控制 | 自由度 | 邊界行為 | 適用情境 |
|---|---|---|---|---|
| 多項式迴歸 | 次數 d | d+1 | 高次發散 | 簡單趨勢、低次數 |
| 階梯函數 (§7.2) | 區間數 K | K | 常數延伸 | 離散化分析、分組比較 |
| 三次樣條 | 節點數 K | 4+K | 三次外推不穩定 | 需要邊界內靈活擬合 |
| 自然三次樣條 | 節點數 K | K | 線性外推,穩定 | 實務預設選擇 |
| 平滑樣條 (§7.5) | 懲罰參數 λ | 等效 df | 線性邊界 | 自動平滑、不需手選節點 |
| 局部迴歸 (§7.6) | 跨度 (span) | 靈活 | 最近鄰延伸 | 極局部模式 |