预测建模实战指南¶
从原始数据到可部署模型——端到端 ML 项目的完整流程
🧭 标准工作流¶
80/20 法则:特征工程占 80% 的时间,模型选择占 20%。
第一步:问题定义¶
在写任何代码之前,先回答:
- 业务目标是什么?(提升转化率?减少流失?预测库存?)
- 这是一个什么类型的问题?(分类 / 回归 / 聚类 / 排序)
- 成功的标准是什么?(准确率 > 85%?AUC > 0.8?MAE < 10?)
- 用什么指标评估业务价值?(不是准确率——是 ROI、节省的成本、提升的收入)
问题类型速查¶
| 你想要什么输出? | 问题类型 | 示例 | 典型算法 |
|---|---|---|---|
| 是/否 | 二分类 | 用户会流失吗? | XGBoost、LightGBM |
| 多选一 | 多分类 | 用户属于哪个群体? | LightGBM、CatBoost |
| 一个数值 | 回归 | 用户会花多少钱? | XGBoost、线性回归 |
| 分组 | 聚类 | 哪些用户行为相似? | K-Means、DBSCAN |
| 排序 | Learning to Rank | 推荐前 10 个商品 | LambdaMART |
| 概率 | 概率预测 | 用户明天回来的概率? | XGBoost + 校准 |
第二步:探索性数据分析(EDA)¶
建模前最重要的一步——了解你的数据。
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# 1. 快速概览
print(f"形状: {df.shape}")
print(f"缺失值:\n{df.isnull().sum()[df.isnull().sum() > 0]}")
print(f"数据类型:\n{df.dtypes.value_counts()}")
# 2. 目标变量分布
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
df['target'].hist(bins=50, ax=axes[0])
axes[0].set_title('目标变量分布')
# 分类问题 → 检查类别平衡
df['target'].value_counts().plot(kind='bar', ax=axes[1])
axes[1].set_title('类别分布')
# 3. 相关性热力图
plt.figure(figsize=(12, 10))
sns.heatmap(df.corr(), annot=False, cmap='RdBu_r', center=0)
plt.title('特征相关性矩阵')
# 4. 特征 vs 目标的关系
# 分类问题 → 箱线图
for col in num_cols[:5]:
df.boxplot(column=col, by='target', figsize=(6, 4))
plt.title(f'{col} vs Target')
plt.show()
EDA 检查清单¶
- 数据量是否足够?(经验:特征数 × 50 是最小样本量)
- 缺失值比例 > 50% 的列 → 考虑删除
- 目标变量是否严重不平衡?(< 5% 的正样本 → 需要特殊处理)
- 是否存在数据泄露?(如:用"未来"信息预测"过去")
- 训练集和测试集的分布是否一致?
第三步:数据预处理¶
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, LabelEncoder
from sklearn.impute import SimpleImputer
# 1. 划分训练/测试集(在一切预处理之前!)
X = df.drop('target', axis=1)
y = df['target']
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y # 分层抽样
)
# 2. 缺失值处理
num_cols = X_train.select_dtypes(include=np.number).columns
cat_cols = X_train.select_dtypes(include='object').columns
# 数值特征 → 中位数填充
num_imputer = SimpleImputer(strategy='median')
X_train[num_cols] = num_imputer.fit_transform(X_train[num_cols])
X_test[num_cols] = num_imputer.transform(X_test[num_cols])
# 分类特征 → 众数填充
cat_imputer = SimpleImputer(strategy='most_frequent')
X_train[cat_cols] = cat_imputer.fit_transform(X_train[cat_cols])
X_test[cat_cols] = cat_imputer.transform(X_test[cat_cols])
# 3. 分类变量编码
for col in cat_cols:
le = LabelEncoder()
X_train[col] = le.fit_transform(X_train[col])
X_test[col] = le.transform(X_test[col])
# 4. 标准化(用于线性模型、神经网络;树模型不需要!)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
第四步:特征工程¶
特征工程比算法选择更能决定模型上限。
常用特征变换¶
# 1. 数学变换(偏态分布 → 正态化)
df['amount_log'] = np.log1p(df['amount'])
df['amount_sqrt'] = np.sqrt(df['amount'])
# 2. 分箱 → 捕捉非线性关系
df['age_bin'] = pd.cut(df['age'], bins=[0, 25, 35, 50, 100],
labels=['青年', '壮年', '中年', '老年'])
# 3. 交互特征
df['income_per_age'] = df['income'] / (df['age'] + 1)
# 4. 时间特征
df['day_of_week'] = df['timestamp'].dt.dayofweek
df['hour'] = df['timestamp'].dt.hour
df['is_weekend'] = df['day_of_week'].isin([5, 6]).astype(int)
# 5. 聚合特征(RFM 分析的基础)
user_agg = df.groupby('user_id').agg(
recency=('timestamp', lambda x: (df['timestamp'].max() - x.max()).days),
frequency=('order_id', 'count'),
monetary=('amount', 'sum')
)
特征选择¶
from sklearn.feature_selection import mutual_info_classif, SelectKBest
# 互信息 → 捕捉非线性关系
mi_scores = mutual_info_classif(X_train, y_train, random_state=42)
mi_df = pd.DataFrame({'feature': X_train.columns, 'mi_score': mi_scores})
mi_df = mi_df.sort_values('mi_score', ascending=False)
# 选择 Top 20 特征
selector = SelectKBest(mutual_info_classif, k=20)
X_train_selected = selector.fit_transform(X_train, y_train)
print("Top 10 特征:")
print(mi_df.head(10))
第五步:建模与调参¶
基线模型 → 快速试错¶
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from xgboost import XGBClassifier
from lightgbm import LGBMClassifier
from sklearn.model_selection import cross_val_score
models = {
'Logistic Regression': LogisticRegression(max_iter=1000),
'Random Forest': RandomForestClassifier(n_estimators=100, random_state=42),
'XGBoost': XGBClassifier(n_estimators=100, random_state=42, eval_metric='logloss'),
'LightGBM': LGBMClassifier(n_estimators=100, random_state=42, verbose=-1),
}
for name, model in models.items():
scores = cross_val_score(model, X_train_scaled, y_train,
cv=5, scoring='roc_auc')
print(f"{name}: AUC = {scores.mean():.4f} (+/- {scores.std()*2:.4f})")
超参数调优¶
from sklearn.model_selection import RandomizedSearchCV
# LightGBM 超参数搜索
param_dist = {
'num_leaves': [31, 63, 127],
'max_depth': [-1, 5, 10, 15],
'learning_rate': [0.01, 0.05, 0.1],
'min_child_samples': [20, 50, 100],
'subsample': [0.6, 0.8, 1.0],
'colsample_bytree': [0.6, 0.8, 1.0],
}
lgbm = LGBMClassifier(n_estimators=500, random_state=42, verbose=-1)
search = RandomizedSearchCV(
lgbm, param_dist, n_iter=30, cv=5,
scoring='roc_auc', random_state=42, n_jobs=-1
)
search.fit(X_train, y_train)
print(f"最佳参数: {search.best_params_}")
print(f"最佳 AUC: {search.best_score_:.4f}")
第六步:模型评估¶
分类问题¶
from sklearn.metrics import (
accuracy_score, precision_score, recall_score,
f1_score, roc_auc_score, confusion_matrix,
classification_report
)
y_pred = search.best_estimator_.predict(X_test)
y_proba = search.best_estimator_.predict_proba(X_test)[:, 1]
print("=== 分类报告 ===")
print(classification_report(y_test, y_pred))
print(f"AUC: {roc_auc_score(y_test, y_proba):.4f}")
# 混淆矩阵
cm = confusion_matrix(y_test, y_pred)
print(f"混淆矩阵:\n{cm}")
# 关键业务指标
tn, fp, fn, tp = cm.ravel()
print(f"精确率 (Precision): {tp/(tp+fp):.4f} — 预测为正的里面有多少真的正")
print(f"召回率 (Recall): {tp/(tp+fn):.4f} — 真正的正例被找出了多少")
回归问题¶
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
y_pred = model.predict(X_test)
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)
print(f"MAE: {mae:.2f} — 平均绝对误差")
print(f"RMSE: {rmse:.2f} — 均方根误差(对大误差更敏感)")
print(f"R²: {r2:.4f} — 解释的方差比例")
特征重要性¶
# LightGBM / XGBoost 特征重要性
importance = search.best_estimator_.feature_importances_
feat_imp = pd.DataFrame({
'feature': X_train.columns,
'importance': importance
}).sort_values('importance', ascending=False)
plt.figure(figsize=(10, 6))
plt.barh(feat_imp['feature'].head(20)[::-1],
feat_imp['importance'].head(20)[::-1])
plt.xlabel('Feature Importance')
plt.title('Top 20 特征重要性')
plt.tight_layout()
🚫 常见陷阱¶
陷阱 1:数据泄露¶
# ❌ 错误:在划分前做标准化
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X) # 用到了测试集信息!
X_train, X_test = train_test_split(X_scaled, y)
# ✅ 正确
X_train, X_test, y_train, y_test = train_test_split(X, y)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test) # 只用训练集的参数
陷阱 2:用准确率衡量不平衡数据¶
99% 不流失,1% 流失 → 全部预测"不流失"就能达到 99% 准确率,但完全没用。
正确做法:用 AUC、F1、Precision-Recall 曲线。
陷阱 3:过拟合¶
训练集 AUC 0.99,测试集 AUC 0.75 → 过拟合。
防御: - 交叉验证 - 正则化(L1/L2) - Early stopping - 减少特征数量
🔗 相关资源¶
- 机器学习资源合集 — 书单 + 算法速查
- NumPy 实战指南 — 数据处理基础
- Pandas 实战指南 — 数据操作核心
- 相关案例:信用评分卡 · 欺诈检测