跳轉到

常見 ML Case Study:分類、回歸、時間序列與推薦

同樣的錯,在每種任務類型裡有固定的長相。本頁把四種最常見的 ML 任務各自的「解題思路 + 典型陷阱 + 程式碼骨架」整理在一起。


任務類型對照表

任務 輸出 典型業務問題 首選評估指標
分類 離散類別 違約預測、流失預測、疾病診斷 PR-AUC(不平衡)/ F1
回歸 連續數值 房價預測、需求預測、定價 RMSE / MAE / R²
時間序列 連續數值(有時序) 銷售預測、流量預測、股價 MAE / SMAPE
推薦 排序列表 / 評分 商品推薦、內容推薦、廣告 Precision@K / NDCG

Case 1:不平衡分類(信用違約預測)

問題特徵

  • 正例(違約)通常只佔 1–5%,直接訓練模型會全猜多數類
  • 目標:抓到儘可能多的違約者(高 Recall),同時不誤傷太多正常客戶(Precision)
  • 業務成本不對稱:漏報違約(FN)成本 >> 誤報正常(FP)成本

解題思路

  1. 先確認類別比例,決定要不要處理不平衡
  2. 評估指標選 PR-AUC 或 F1,不用 Accuracy
  3. 建模策略:優先用樹模型(GBM / XGBoost),配合 scale_pos_weightclass_weight
  4. Threshold 調整:默認 0.5 不一定最佳,用 PR 曲線找業務成本最小的閾值

程式碼骨架

import pandas as pd
import numpy as np
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from xgboost import XGBClassifier
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.metrics import (average_precision_score, roc_auc_score,
                              precision_recall_curve, classification_report)

# 前處理 Pipeline(見 sklearn Pipeline 模板)
preprocessor = ColumnTransformer([
    ('num', Pipeline([('imp', SimpleImputer(strategy='median')),
                      ('sc', StandardScaler())]), num_cols),
    ('cat', Pipeline([('imp', SimpleImputer(strategy='most_frequent')),
                      ('enc', OneHotEncoder(handle_unknown='ignore'))]), cat_cols),
])

# 不平衡處理:用 scale_pos_weight
neg, pos = np.bincount(y_train)
scale_pos_weight = neg / pos  # 約 19:1 → scale_pos_weight=19

pipe = Pipeline([
    ('pre', preprocessor),
    ('clf', XGBClassifier(
        scale_pos_weight=scale_pos_weight,
        n_estimators=300,
        learning_rate=0.05,
        max_depth=5,
        eval_metric='aucpr',   # 用 PR-AUC 作為 early stopping 指標
        random_state=42,
    )),
])

pipe.fit(X_train, y_train)

# 評估
y_prob = pipe.predict_proba(X_test)[:, 1]
print(f"PR-AUC: {average_precision_score(y_test, y_prob):.4f}")
print(f"AUC-ROC: {roc_auc_score(y_test, y_prob):.4f}")

# 找最佳閾值(最大化 F1)
prec, rec, thresholds = precision_recall_curve(y_test, y_prob)
f1_scores = 2 * prec * rec / (prec + rec + 1e-9)
best_thresh = thresholds[np.argmax(f1_scores)]
print(f"Best threshold: {best_thresh:.3f}")
print(classification_report(y_test, (y_prob >= best_thresh).astype(int)))

常見陷阱

陷阱 說明
SMOTE 在 CV 外做 讓合成資料跨折,造成 leakage;要在 Pipeline 裡或每折前做
只看 Accuracy 全猜多數類也有 95% 準確率
忘記調閾值 0.5 在不平衡資料上幾乎都不是最佳閾值
只報單一指標 Precision 和 Recall 要一起看,業務方通常更在乎某一個

Case 2:回歸(房價 / 需求預測)

問題特徵

  • 目標值通常是右偏分布(少數大房子拉高均值)
  • 模型常會對離群高值預測不足(underestimate)
  • 不同量級的誤差「感覺」不一樣:1000 元的誤差在 10 萬的房子和 1 千萬的房子意義完全不同

解題思路

  1. EDA 先看目標值分布——如果右偏,考慮 log 轉換
  2. 評估指標:
  3. RMSE:對大誤差敏感,原始尺度有意義時用
  4. MAPE:對比較不同尺度的預測有用,但 y 接近 0 時會爆
  5. R²:解釋基線的相對提升,快速溝通
  6. 特徵工程比換模型更有效(面積、地段交互項、鄰近設施距離)
  7. 樹模型(LightGBM)通常是 tabular 回歸首選

程式碼骨架

import numpy as np
import lightgbm as lgb
from sklearn.pipeline import Pipeline
from sklearn.model_selection import KFold, cross_val_score
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score

# 目標值做 log1p(右偏分布處理)
y_train_log = np.log1p(y_train)
y_test_log  = np.log1p(y_test)

pipe = Pipeline([
    ('pre', preprocessor),
    ('reg', lgb.LGBMRegressor(
        n_estimators=500,
        learning_rate=0.05,
        num_leaves=31,
        random_state=42,
        n_jobs=-1,
    )),
])

# CV 在 log 尺度
cv = KFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(pipe, X_train, y_train_log,
                         cv=cv, scoring='neg_root_mean_squared_error')
print(f"CV RMSE (log): {-scores.mean():.4f} ± {scores.std():.4f}")

pipe.fit(X_train, y_train_log)

# 還原成原始尺度再報指標
y_pred = np.expm1(pipe.predict(X_test))   # 反轉 log1p

mae  = mean_absolute_error(y_test, y_pred)
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
r2   = r2_score(y_test, y_pred)
mape = np.mean(np.abs((y_test - y_pred) / (y_test + 1e-9))) * 100

print(f"MAE: {mae:.0f}, RMSE: {rmse:.0f}, R²: {r2:.4f}, MAPE: {mape:.2f}%")

常見陷阱

陷阱 說明
在 log 尺度算 MAE/RMSE 後報告 數字沒有業務意義,要反轉回原始尺度
MAPE 用在含 0 的目標值 分母為 0,指標爆炸;改用 MAE 或 RMSE
訓練集沒有的地段 / 類別 上線時遇到新類別,OneHotEncoder 要設 handle_unknown='ignore'
特徵洩漏 使用了「只有在成交後才知道」的資訊(如實際成交週期)當輸入特徵

Case 3:時間序列預測(銷售 / 需求)

問題特徵

  • 資料有時間順序,不能隨機切分——測試集必須在訓練集之後
  • 可能有趨勢(長期上升/下降)、季節性(每年同期相似)、節假日效應
  • 評估時要「假裝不知道未來」,模擬真實預測情境

解題思路

  1. 時序切分:按時間切,最後 N 期為測試集
  2. 特徵工程是核心:lag 特徵、rolling 統計、時間特徵(weekday、month、holiday flag)
  3. 兩條路:
  4. 機器學習路(LightGBM + lag 特徵):靈活、可加入任意特徵
  5. 統計模型路(Prophet / SARIMA):直接建模趨勢/季節,但可解釋特徵受限

程式碼骨架(ML 路)

import pandas as pd
import numpy as np
import lightgbm as lgb
from sklearn.metrics import mean_absolute_error

df = pd.read_csv('sales.csv', parse_dates=['date'])
df = df.sort_values('date').reset_index(drop=True)

# ── 特徵工程 ─────────────────────────────────────────────
df['day_of_week'] = df['date'].dt.dayofweek
df['month']       = df['date'].dt.month
df['is_weekend']  = df['day_of_week'].isin([5, 6]).astype(int)

# Lag 特徵(要注意對齊:預測 t+1 只能用 t 之前的資料)
for lag in [1, 7, 14, 28]:
    df[f'lag_{lag}'] = df['sales'].shift(lag)

# Rolling 統計
df['rolling_mean_7']  = df['sales'].shift(1).rolling(7).mean()
df['rolling_mean_28'] = df['sales'].shift(1).rolling(28).mean()

df = df.dropna()  # 刪掉 lag 造成的 NaN

# ── 時序切分(不能隨機)─────────────────────────────────
cutoff = df['date'].quantile(0.8)  # 最後 20% 為測試
train = df[df['date'] <= cutoff]
test  = df[df['date'] >  cutoff]

feature_cols = [c for c in df.columns if c not in ['date', 'sales']]
X_train, y_train = train[feature_cols], train['sales']
X_test,  y_test  = test[feature_cols],  test['sales']

# ── 訓練 ─────────────────────────────────────────────────
model = lgb.LGBMRegressor(n_estimators=500, learning_rate=0.05,
                           num_leaves=31, random_state=42)
model.fit(X_train, y_train,
          eval_set=[(X_test, y_test)],
          callbacks=[lgb.early_stopping(50, verbose=False)])

y_pred = model.predict(X_test)
mae  = mean_absolute_error(y_test, y_pred)
smape = np.mean(2 * np.abs(y_pred - y_test) / (np.abs(y_pred) + np.abs(y_test) + 1e-9)) * 100
print(f"MAE: {mae:.2f}, SMAPE: {smape:.2f}%")

常見陷阱

陷阱 說明
隨機切 train/test 讓未來資料進訓練集,CV 分數虛高,上線後崩
Lag 特徵沒有 shift df['sales'].rolling(7).mean() 包含了當前值,等於知道了未來
忽略時序 CV(TimeSeriesSplit) 調參時也要按時間切,不能用隨機 KFold
節假日沒有特殊處理 春節、雙十一等特殊時期的規律和平日完全不同,要加 flag

Case 4:推薦系統(協同過濾 + 內容型)

問題特徵

  • 資料稀疏:一個用戶只互動過幾十個物品,對全體物品沒有偏好
  • 評估不能只看準確率:多樣性、新穎性、覆蓋率也重要
  • 冷啟動問題:新用戶 / 新物品沒有歷史互動資料

兩種主要方法

方法 原理 適用情境 工具
協同過濾(Collaborative Filtering) 相似用戶喜歡相似物品 有互動歷史、資料夠密 implicit、Surprise、LightFM
內容型(Content-Based) 推薦屬性相似的物品 新物品冷啟動、有物品特徵 向量相似度、embedding
兩段式(Two-Tower) 深度 embedding 學 user/item 表示 工業級系統 TensorFlow Recommenders

協同過濾程式碼骨架(Matrix Factorization)

import pandas as pd
import numpy as np
from scipy.sparse import csr_matrix
from implicit import als  # pip install implicit

# 建立 user-item 互動矩陣(隱式反饋)
df = pd.read_csv('interactions.csv')  # user_id, item_id, count
user_map = {u: i for i, u in enumerate(df['user_id'].unique())}
item_map = {v: i for i, v in enumerate(df['item_id'].unique())}

rows = df['user_id'].map(user_map)
cols = df['item_id'].map(item_map)
data = df['count'].values

user_item = csr_matrix((data, (rows, cols)),
                        shape=(len(user_map), len(item_map)))
item_user = user_item.T.tocsr()  # implicit 需要 item-user 矩陣

# 訓練 ALS(Alternating Least Squares)
model = als.AlternatingLeastSquares(
    factors=64, regularization=0.01, iterations=50
)
model.fit(item_user)

# 為某用戶推薦
user_id = 'user_001'
uid = user_map[user_id]
recommended_ids, scores = model.recommend(uid, user_item[uid], N=10,
                                           filter_already_liked_items=True)
recommended_items = [k for k, v in item_map.items() if v in recommended_ids]

評估指標

def precision_at_k(actual, predicted, k):
    predicted_k = set(predicted[:k])
    actual_set  = set(actual)
    return len(predicted_k & actual_set) / k

def recall_at_k(actual, predicted, k):
    predicted_k = set(predicted[:k])
    actual_set  = set(actual)
    return len(predicted_k & actual_set) / len(actual_set) if actual_set else 0

def ndcg_at_k(actual, predicted, k):
    actual_set = set(actual)
    dcg  = sum(1 / np.log2(i + 2) for i, item in enumerate(predicted[:k])
               if item in actual_set)
    idcg = sum(1 / np.log2(i + 2) for i in range(min(len(actual_set), k)))
    return dcg / idcg if idcg > 0 else 0

常見陷阱

陷阱 說明
隨機切 train/test(忽略時間) 用戶未來的互動跑進訓練集,評估虛高
只看 Precision@K 如果總是推熱門物品,Precision@K 可以很高,但沒有個人化
冷啟動沒有策略 新用戶 / 新物品沒有 fallback(如熱門推薦、內容型)
Item popularity bias 熱門商品得到更多曝光、更多互動,馬太效應強化
離線指標不等於線上效果 NDCG 高不代表 CTR / 購買率高,需要 A/B 驗證

跨任務通用決策框架

定義業務問題
確認任務類型(分類 / 回歸 / 時序 / 推薦)
選對評估指標(在 train 之前就定好,不能事後換)
確認資料切分方式(不平衡→Stratified;時序→TimeSeriesSplit;群組→GroupKFold)
建立基線(最簡單能用的模型,設定性能下限)
特徵工程 + 模型選擇(樹模型先試)
調參(CV 內部,不碰 test set)
最終評估(test set 只碰一次)

延伸閱讀(本站)

來源