多項式迴歸對 X 的整個取值範圍施加了全域性結構(一條曲線通吃)。階梯函數(Step Function)則放棄全域曲線,改為將 X 的範圍「切成好幾段」,每一段內擬合一個常數——本質上就是把連續變數轉換成有序類別變數(ordered categorical variable)。
📖 ISLP §7.2, p.300 · James, Witten, Hastie, Tibshirani (2023)
設定 K 個切點(cutpoints)c₁, c₂, …, cK,將 X 分割成 K+1 個區間,並為每個區間定義一個指示變數(indicator/dummy variable):
\[ C_0(X) = I(X < c_1),\quad C_1(X) = I(c_1 \leq X < c_2),\quad C_2(X) = I(c_2 \leq X < c_3),\quad\ldots,\quad C_K(X) = I(c_K \leq X) \]其中 I(·) 是指示函數:條件成立回傳 1,否則回傳 0。注意到對任意 X,C₀ + C₁ + … + CK = 1(因為 X 必定落在恰好一個區間內)。
接著用最小平方法擬合線性模型:
\[ y_i = \beta_0 + \beta_1 C_1(x_i) + \beta_2 C_2(x_i) + \cdots + \beta_K C_K(x_i) + \epsilon_i \qquad (7.5) \]注意:我們刻意排除了 C₀(X)——它與截距項 β₀ 冗餘(類似於三類別質性變數只需要兩個 dummy)。當 X < c₁ 時,所有 C₁…CK 都是 0,此時 β₀ 就是該區間的平均回應值。βⱼ 則代表從前一區間「跳」到第 j 個區間時,平均回應的變化量。
想像你在經營咖啡店,想根據戶外溫度預測銷量。多項式迴歸會畫一條光滑曲線:「溫度每升 1°C 銷量都不同」。但階梯函數的邏輯更簡單:「0-10°C 冷死人沒人出來 → 固定低銷量;10-20°C 舒服 → 中銷量;20-35°C 熱死人 → 冰咖啡爆單」。溫度不是連續變化了,而是分成「冷/涼/熱」三段,每段一個銷量水準。
# ISLP §7.2:階梯函數 — 在 Wage 資料上分段擬合常數
try:
from google.colab import drive
drive.mount('/content/drive')
DATA_PATH = '/content/drive/MyDrive/ISLP_data/'
except ImportError:
DATA_PATH = '/tmp/'
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import numpy as np
from ISLP import load_data
from sklearn.linear_model import LinearRegression
# 載入 Wage 資料
wage = load_data('Wage')
X = wage['age'].values.reshape(-1, 1)
y = wage['wage'].values
# ── 方法:手動建立階梯函數的指示變數 ──
# 將年齡切成 4 段(3 個切點 → 4 個區間 → 3 個 dummy + intercept)
cutpoints = [30, 45, 60] # 切點:<30, 30-45, 45-60, >60
C = np.zeros((len(X), len(cutpoints)))
for j, c in enumerate(cutpoints):
C[:, j] = (X.flatten() >= c).astype(int) # I(age >= c_j)
# 擬合模型:y = β₀ + β₁·I(age≥30) + β₂·I(age≥45) + β₃·I(age≥60) + ε
# 注意:這裡選用 C_k(X)=I(X≥c_k)(等價於課本的 I(c_k≤X)),同樣排除 C₀
model = LinearRegression()
model.fit(C, y)
# 係數解讀:
# β₀ = 年齡<30 的平均工資
# βⱼ = 從「= {c}): {model.coef_[j]:+.1f}K")
# ── 繪圖 ──
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 5))
# 左圖:階梯函數擬合
age_grid = np.linspace(X.min(), X.max(), 200).reshape(-1, 1)
C_grid = np.zeros((200, len(cutpoints)))
for j, c in enumerate(cutpoints):
C_grid[:, j] = (age_grid.flatten() >= c).astype(int)
pred = model.predict(C_grid)
ax1.scatter(X, y, s=4, alpha=0.3, color='#8b949e')
ax1.plot(age_grid, pred, color='#58a6ff', linewidth=2)
for c in cutpoints:
ax1.axvline(c, color='#d2991d', linestyle='--', alpha=0.5, label=f'c={c}')
ax1.set_xlabel('Age'); ax1.set_ylabel('Wage (K)')
ax1.set_title('Step Function Fit (3 cutpoints)')
# 右圖:比較不同切點數
ax2.scatter(X, y, s=4, alpha=0.3, color='#8b949e')
for n_cut, style in zip([2, 4, 7], ['--', '-.', ':']):
cpts = np.linspace(25, 65, n_cut + 1)[1:-1]
# Build and fit for each cut count
C_tr = np.zeros((len(X), n_cut))
for jj, cc in enumerate(cpts):
C_tr[:, jj] = (X.flatten() >= cc).astype(int)
m = LinearRegression()
m.fit(C_tr, y)
C_pr = np.zeros((200, n_cut))
for jj, cc in enumerate(cpts):
C_pr[:, jj] = (age_grid.flatten() >= cc).astype(int)
p2 = m.predict(C_pr)
ax2.plot(age_grid, p2, linestyle=style, linewidth=1.8 if n_cut == 4 else 1.2,
label=f'{n_cut} cuts')
ax2.set_xlabel('Age'); ax2.set_ylabel('Wage (K)')
ax2.set_title('More Cuts = More Flexibility')
ax2.legend(fontsize=8)
plt.tight_layout()
plt.show()
將 BMI 切為 <18.5(過輕)、18.5–24.9(正常)、25–29.9(過重)、≥30(肥胖),每層擬合一個固定風險值。醫師不需要記住「BMI 每增加 1 單位風險增 0.3%」的精確公式——記四個數字就夠了。
FICO 分數常被切成區間(300–579 Poor、580–669 Fair、670–739 Good、740–799 Very Good、800–850 Exceptional),銀行對每個區間給定一個違約機率,而不是用連續函數建模。
將年收入分為低/中/高三段,每段設定不同的行銷預算或折扣策略。階梯函數天然對應「分級定價」的商業邏輯,比多項式曲線更容易向主管解釋。
| 方法 | 核心想法 | 平滑度 | 可解釋性 | 適用情境 |
|---|---|---|---|---|
| 階梯函數 | 分區擬合常數 | 完全不連續(階梯狀) | ★★★★★ | 分級分類、快速分層 |
| 多項式迴歸 | 全域曲線 | 無限可微 | ★★☆☆☆ | 每單位 X 的邊際效應一致時 |
| 迴歸樣條(§7.4) | 分段多項式 + 接點平滑 | 可控(線性/二次/三次) | ★★★★☆ | 需要兼顧彈性與平滑時 |
| 局部迴歸(§7.6) | 只看鄰居擬合小段 | 可調(span 控制) | ★★☆☆☆ | 探索性資料分析 |
階梯函數的設計哲學——「放棄全域最優,切換成分段最優」——其實就是 Hermes 的 C-A-F(Context-Action-Feedback)路由器的核心。不同任務類型(程式開發 vs 統計教學 vs 內容生成)各自對應一個「區間」,每個區間指派最適合的子 agent 模型。β₀ 是基礎能力(DeepSeek),β₁ 是程式能力增量(Codex),β₂ 是教學能力增量(專門微調模型)。不是找一條萬能曲線,而是為每個任務類型定義最佳「階梯」。