跳到正文
Learn Everything
返回

从数据清洗到特征工程:传统机器学习标准流程与完整学习路径

编辑文章

先建立正确的全局观

传统机器学习项目并不是“把 CSV 丢给一个算法”。更可靠的顺序是:

  1. 把业务问题改写成可验证的预测问题;
  2. 明确样本、预测时点、特征窗口和标签窗口;
  3. 切分前只核验 schema、来源和预先确定的业务规则;
  4. 按真实使用方式留出测试集;
  5. 只在训练数据上探索分布,并学习插补值、缩放参数、编码词表和特征选择规则;
  6. 从简单基线开始,用验证集或交叉验证选择方案;
  7. 锁定方案后只评估一次测试集;
  8. 将原始数据到预测结果的处理链一起保存,并监控上线后的数据和效果。

防止数据泄漏的传统机器学习流程:从问题定义、限定为模式与固定规则的初审和切分,到仅在训练与交叉验证内完成清洗、特征工程、训练和调参,最后进行一次测试并部署监控

图 1:虚线是关键边界。切分前的 Initial Audit 只检查 schema 与固定规则;缺失率、分布、标签关联、离群阈值等探索应在留出测试集后,只对训练数据进行。任何需要从数据中学习的步骤都只能在训练折内 fit,最终测试集只使用一次。对延迟标签,还必须保证训练样本的标签在每次拟合时已经成熟。

scikit-learn 的常见陷阱指南建议先切分,再对训练集调用 fit/fit_transform,对验证集和测试集只调用 transform。这条边界通常比“选哪个模型”更重要。

第一步:先定义任务,而不是先选算法

开始建模前,至少写下一页“问题合同”:

二分类、多分类、多标签与回归

任务一个样本的目标例子常见输出
二分类两种互斥结果之一30 天内流失/不流失正类概率与 0/1 决策
多分类三种以上互斥类别之一流失原因:价格/服务/搬迁/其他各类别概率
多标签分类可同时具有多个标签工单同时属于“计费”和“网络”每个标签的概率或判断
回归连续数值房价、销售额、交付时长一个数值或分位数

多分类不等于多标签。 “猫、狗、鸟三选一”是多分类;“文章可同时属于科技、教育、商业”是多标签。二者的标签表示、损失函数和指标平均方式不同。

贯穿本文的任务是:每月形成客户快照,预测客户在接下来 30 天是否主动流失。若目标改为互斥的“流失原因”,它成为多分类;若改为未来 30 天消费金额,则成为回归。

贯穿案例:客户月度快照

下表是教学用结构,数值仅为示意:

customer_idsnapshot_datetenure_monthsmonthly_chargessupport_calls_90dcontract_typepayment_methodlast_login_datechurn_30d
C0012026-06-301889.00annualcard2026-06-280
C0022026-06-302129.05monthlye_wallet2026-06-021
C0032026-06-3037缺失1annualcard2026-06-290

这里已经确定:snapshot_date 是预测时点;特征只能来自该日及以前;churn_30d 来自之后 30 天;customer_id 用于追踪和切分,通常不直接作为特征。“取消日期”“退款结果”“挽回工单结果”等预测后字段不能进入模型。

30 天标签还有一个容易遗漏的时间:标签可用时间。快照日后的 30 天没有走完时,标签尚未成熟;实际仓库可能还会晚一天或数天落库。应优先记录真实的 label_available_at,而不是仅凭快照月份推断。它只用于构造训练集和回测,绝不能作为模型特征。

同一客户可以有多个月份的记录,因此行与行并不独立。本文的主要上线目标是:用过去月份预测未来月份中的现有或新客户,所以主示例采用时间回测,而不是随机分层切分。

切分前初审:只检查元数据与固定规则

测试集尚未留出时,可以核验:

此时不要根据全表的缺失率、分位数、类别频数、正类比例或字段与标签的关系决定删列、阈值和特征。那些结论会把未来测试期的信息带回开发过程。

# 教学示例:未在你的数据和环境中运行验证
import pandas as pd

raw = pd.read_csv("customer_churn.csv")
required = {
    "customer_id", "snapshot_date", "tenure_months", "monthly_charges",
    "support_calls_90d", "contract_type", "payment_method",
    "last_login_date", "churn_30d",
}
missing_columns = required - set(raw.columns)
if missing_columns:
    raise ValueError(f"缺少字段: {sorted(missing_columns)}")

# 这是 schema 校验,不是根据总体分布学习日期边界
snapshot = pd.to_datetime(raw["snapshot_date"], errors="raise")
if raw[["customer_id", "snapshot_date"]].isna().any().any():
    raise ValueError("主键字段不可为空")

先切分:模拟模型真正面对的未来

三种常见切法

scikit-learn 的交叉验证指南也强调:时间相关数据应采用时间感知切分,有组结构时应采用组间隔离切分。

本例预先用业务日历确定边界:2026 年 6 月为阈值验证期,2026 年 8 月起为最终测试期。7 月作为等待期,使 6 月快照的 30 天标签在 8 月定阈值和开始测试评分前成熟。开发模型模拟在 6 月 1 日拟合,因此训练行不仅要早于 6 月,其标签也必须在 6 月 1 日前可用。

# 教学示例:未运行验证;日期边界和标签落库规则需按项目替换
TARGET = "churn_30d"
work = raw.loc[raw[TARGET].isin([0, 1])].copy()
work["snapshot_date"] = pd.to_datetime(work["snapshot_date"], errors="raise")

# 生产项目应优先读取标签仓库记录的真实可用时间。
# 这里只用 snapshot + 30 天演示最简标签成熟规则。
work["label_available_at"] = work["snapshot_date"] + pd.Timedelta(days=30)
work = work.sort_values("snapshot_date").reset_index(drop=True)

FIT_CUTOFF = pd.Timestamp("2026-06-01")
THRESHOLD_START = pd.Timestamp("2026-06-01")
THRESHOLD_END = pd.Timestamp("2026-07-01")
THRESHOLD_LOCK_AT = pd.Timestamp("2026-08-01")
TEST_START = pd.Timestamp("2026-08-01")

dev_raw = work.loc[
    (work["snapshot_date"] < THRESHOLD_START)
    & (work["label_available_at"] <= FIT_CUTOFF)
].copy()
threshold_raw = work.loc[
    (work["snapshot_date"] >= THRESHOLD_START)
    & (work["snapshot_date"] < THRESHOLD_END)
].copy()
test_raw = work.loc[work["snapshot_date"] >= TEST_START].copy()

# 断言的是模拟时点真正能看到标签,而不只是月份先后。
assert (dev_raw["label_available_at"] <= FIT_CUTOFF).all()
assert (threshold_raw["label_available_at"] <= THRESHOLD_LOCK_AT).all()
assert dev_raw["snapshot_date"].max() < threshold_raw["snapshot_date"].min()
assert threshold_raw["snapshot_date"].max() < test_raw["snapshot_date"].min()

这里的 7 月记录不进入训练、阈值选择或测试,是显式的 purge/embargo 间隔。30 天只是本例标签窗口;若真实落库更慢,就应使用真实 label_available_at 并延长间隔。测试集的指标也只能在其标签成熟后离线计算,但线上预测仍发生在各自快照时点。

如果部署目标明确是“只预测从未见过的新客户”,时间边界之外还要隔离客户:例如从未来测试期取出客户集合,再从过去训练期排除这些客户;每个验证折也做同样处理。这回答的是比“预测未来月份”更严格、也不同的问题,会丢掉测试客户的历史记录。不要为了形式同时套用时间与分组切分,先明确要估计哪种泛化能力。

训练集用于拟合模型和所有预处理统计量;阈值验证期只选行动阈值;测试期在方案锁定后估计一次泛化表现。若反复依据测试结果调整,测试集实际上已经变成验证集。

切分后审计:只在开发训练集探索

现在才对 dev_raw 做探索性数据分析(EDA):

# 教学示例:未运行验证;不要把 threshold_raw/test_raw 拼回来做这些统计
print(dev_raw.shape)
print(dev_raw.dtypes)
print(dev_raw.isna().mean().sort_values(ascending=False).head(20))
print(dev_raw.nunique(dropna=False).sort_values().head(20))
print(dev_raw.duplicated().sum())
print(dev_raw[TARGET].value_counts(dropna=False, normalize=True))
print(dev_raw.describe(include="all").T)

重点检查:

由这些结果提出的缺失率删列阈值、截断边界、类别合并和特征假设,都只能在开发期形成,并在交叉验证的训练折内拟合。最终测试期可以在一次正式评估时描述,以解释适用范围,但不能再反向改方案。

数据库整数未必是数值特征。邮政编码、门店编号和产品编号虽然由数字组成,本质上通常是类别或 ID。describe 也只能发现表象;还要询问字段何时写入、缺失代表系统故障还是业务不适用。

异常数据如何清洗

“异常”取决于业务规则和数据生成过程。极端值可能是录入错误,也可能是最有价值的高风险客户。异常值不等于错误数据。

问题先确认什么可选策略主要边界
缺失值缺失机制与业务含义删除、常量/中位数/众数插补、模型插补、缺失指示插补器只在训练数据拟合
重复值技术重复还是多次合法事件删除技术重复、聚合事件、按时间保留有效版本不要误删复购或多次测量
格式/单位不一致原始单位、时区、币种统一格式、单位和时区,记录转换规则应来自数据字典
非法值明确业务约束回源修正、置缺失、删除年龄 999 可判非法,103 未必非法
离群点是否真实、是否影响模型保留、截断、变换、稳健缩放、分箱阈值只能从训练折学习
标签错误标签定义和生成延迟回源复核、修正、排除不确定样本不能因模型猜错就改标签
数据泄漏预测时是否可得删除字段、重建窗口、重做切分异常高分首先查泄漏

缺失值

缺失极少且近似随机时可删除少量行,但要检查是否系统性排除某类人群。数值变量可用训练集的中位数作为稳健基线;类别变量可填入 "missing";若“没有填写”本身有含义,可增加缺失指示变量。复杂插补不天然更好,应通过交叉验证比较并考虑维护成本。

scikit-learn 的缺失值插补指南介绍了 SimpleImputer、KNN 和迭代插补,也支持 add_indicator=True。IterativeImputer 的 API 状态可能随版本变化,请以项目安装版本文档为准。

离群点

可按三层判断:硬规则负责识别明确非法值;IQR、MAD、分位数和箱线图只负责提示;Isolation Forest、Local Outlier Factor 等可发现多变量异常,但仍需业务解释。scikit-learn 区分离群点检测与新颖点检测,不要直接把无监督检测器的输出当成删除名单。

明确录入错误应优先回源;合法长尾可保留并尝试 log1p、稳健缩放或分箱;传感器饱和时按设备上限截断可能合理;若代表另一业务群体,可增加群体特征或分群建模。

特征工程:把业务信息变成可学习表示

特征工程不是无止境制造列,而是在预测时可获得的前提下,让输入更稳定、更贴近问题。

数值特征

类别、日期与文本

One-hot 适合无序、基数不高的类别;序数编码只用于确有顺序的类别。高基数特征可考虑频数、哈希或目标编码,但目标编码必须在训练折内计算。客户 ID 通常只用于连接、分组和追踪。

日期可转为距上次登录天数、星期、月份,以及过去 7/30/90 天的次数与趋势。滚动统计必须在快照时点截断。短文本可从 TF-IDF 加线性模型开始;若文本是在问题解决后填写,它很可能泄漏结果。

领域特征常比复杂模型更值钱,例如“近 30 天使用量相对过去 6 个月均值的下降率”。必须明确观察窗口、最小分母和缺失含义。

特征选择与降维

过滤法、递归特征消除、L1 正则和树模型可用于选择特征。PCA 适合大量相关数值变量,但牺牲可解释性,且要先处理缺失与尺度。选择、降维、分箱和截断都要放进交叉验证 Pipeline;先在全数据上选特征再验证会产生乐观偏差。

用 Pipeline 固化规则:完整时间切分示例

下面代码是教学示例,未在你的数据、Python 或 scikit-learn 版本中运行验证。列名、日期边界和参数需按项目修改;易变 API 请以当前官方文档与本地版本为准。

1. 把确定性特征生成放进可序列化 Transformer

在实际项目中将下面类放在可导入模块 churn_features.py,而不是只定义在 Notebook 或 __main__。这样 joblib 加载时能找到同一类;模块版本也应随制品锁定。

# churn_features.py;教学示例,未运行验证
import numpy as np
import pandas as pd
from sklearn.base import BaseEstimator, TransformerMixin

class ChurnFeatureBuilder(BaseEstimator, TransformerMixin):
    REQUIRED = {
        "customer_id", "snapshot_date", "tenure_months", "monthly_charges",
        "support_calls_90d", "contract_type", "payment_method", "last_login_date",
    }

    def fit(self, X, y=None):
        return self  # 不从总体分布学习参数

    def transform(self, X):
        missing = self.REQUIRED - set(X.columns)
        if missing:
            raise ValueError(f"缺少原始字段: {sorted(missing)}")
        out = X.copy()
        for col in ["contract_type", "payment_method"]:
            out[col] = out[col].astype("string").str.strip().str.lower()
        snapshot = pd.to_datetime(out["snapshot_date"], errors="coerce")
        last_login = pd.to_datetime(out["last_login_date"], errors="coerce")
        out["days_since_last_login"] = (snapshot - last_login).dt.days

        out.loc[out["tenure_months"] < 0, "tenure_months"] = np.nan
        out.loc[out["monthly_charges"] < 0, "monthly_charges"] = np.nan
        out.loc[out["days_since_last_login"] < 0, "days_since_last_login"] = np.nan
        out["calls_per_tenure_month"] = (
            out["support_calls_90d"] / (out["tenure_months"] + 1)
        )
        return out

Transformer 不负责删行,因为 transform 改变样本数会让 X 与 y 错位。技术重复应由上游按预先登记的主键和来源规则处理;是否属于业务重复,只在开发数据上调查。

2. 从原始字段构建完整 Pipeline

# train.py;教学示例,未运行验证
from churn_features import ChurnFeatureBuilder
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler

num_cols = [
    "tenure_months", "monthly_charges", "support_calls_90d",
    "days_since_last_login", "calls_per_tenure_month",
]
cat_cols = ["contract_type", "payment_method"]

numeric_pipe = Pipeline([
    ("imputer", SimpleImputer(strategy="median", add_indicator=True)),
    ("scaler", StandardScaler()),
])
categorical_pipe = Pipeline([
    ("imputer", SimpleImputer(strategy="most_frequent")),
    ("onehot", OneHotEncoder(handle_unknown="ignore")),
])
preprocess = ColumnTransformer([
    ("num", numeric_pipe, num_cols),
    ("cat", categorical_pipe, cat_cols),
])
model = Pipeline([
    ("features", ChurnFeatureBuilder()),
    ("preprocess", preprocess),
    ("classifier", LogisticRegression(max_iter=1000, random_state=42)),
])

# 标签可用时间只控制样本资格,不进入模型。
DROP_FROM_X = [TARGET, "label_available_at"]
X_dev = dev_raw.drop(columns=DROP_FROM_X).reset_index(drop=True)
y_dev = dev_raw[TARGET].astype(int).reset_index(drop=True)
dev_label_available = dev_raw["label_available_at"].reset_index(drop=True)

X_threshold = threshold_raw.drop(columns=DROP_FROM_X).reset_index(drop=True)
y_threshold = threshold_raw[TARGET].astype(int).reset_index(drop=True)
X_test = test_raw.drop(columns=DROP_FROM_X).reset_index(drop=True)
y_test = test_raw[TARGET].astype(int).reset_index(drop=True)

ColumnTransformer 可对不同列应用不同转换,再与特征构建器和分类器装进一条 Pipeline。组合估计器指南说明,这可统一 fit/predict、联合搜索参数,并减少交叉验证中的泄漏。

3. 按月份扩展窗口验证和调参

每一折都把验证月月初视作该折的拟合时点。训练样本必须同时满足“快照更早”和“标签在拟合时点前已经可用”。这会自然清除紧邻验证月、但 30 天标签尚未成熟的记录。

# 教学示例:未运行验证
import numpy as np
import pandas as pd
from sklearn.dummy import DummyClassifier
from sklearn.model_selection import GridSearchCV, cross_validate

# 用一个月份验证;训练行还必须在该折拟合时点前标签已成熟。
def expanding_month_splits(dates, label_available_at, min_train_months=6):
    dates = pd.to_datetime(dates).reset_index(drop=True)
    available = pd.to_datetime(label_available_at).reset_index(drop=True)
    months = pd.PeriodIndex(dates, freq="M")
    unique_months = sorted(months.unique())

    for i in range(min_train_months, len(unique_months)):
        valid_month = unique_months[i]
        fit_cutoff = valid_month.start_time
        train_mask = (months < valid_month) & (available <= fit_cutoff)
        valid_mask = months == valid_month
        train_idx = np.flatnonzero(train_mask)
        valid_idx = np.flatnonzero(valid_mask)
        if len(train_idx) and len(valid_idx):
            assert (available.iloc[train_idx] <= fit_cutoff).all()
            yield train_idx, valid_idx

cv = list(expanding_month_splits(
    X_dev["snapshot_date"], dev_label_available, min_train_months=6
))
if not cv:
    raise ValueError("开发期月份不足,无法进行带标签成熟约束的扩展窗口验证")

scoring = {
    "roc_auc": "roc_auc",
    "average_precision": "average_precision",
    "f1": "f1",
    "neg_log_loss": "neg_log_loss",
}
dummy = Pipeline([
    ("features", ChurnFeatureBuilder()),
    ("preprocess", preprocess),
    ("classifier", DummyClassifier(strategy="prior")),
])
print(cross_validate(dummy, X_dev, y_dev, cv=cv, scoring=scoring))

param_grid = {
    "classifier__C": [0.1, 1.0, 10.0],
    "classifier__class_weight": [None, "balanced"],
}
search = GridSearchCV(
    model, param_grid=param_grid, scoring=scoring,
    refit="average_precision", cv=cv, n_jobs=-1,
)
search.fit(X_dev, y_dev)
print(search.best_params_, search.best_score_)

每个验证月应同时含有所需类别,否则 ROC-AUC 等指标可能无定义;真实项目应在建折时检查样本量和正类数。这里不使用 StratifiedKFold,因为月度快照不是可随意打乱的独立样本。

4. 用同一个锁定模型选阈值,再测试一次

超参数只用较早开发期选择。GridSearchCV(refit=...) 会用选出的超参数在完整 X_dev 上重新拟合;而 X_dev 已明确限制为截至 6 月 1 日标签成熟的数据。下面克隆并拟合一次只是让“最终锁定模型”更直观。

关键点是:6 月概率、行动阈值和 8 月测试概率全部来自同一个已拟合模型。 定阈值后不再把 6 月阈值集并入训练,否则插补值、缩放参数、类别词表和模型系数都会变化,旧阈值不再对应新的概率尺度。

# 教学示例:未运行验证
from sklearn.base import clone
from sklearn.metrics import (
    average_precision_score, classification_report, confusion_matrix,
    f1_score, log_loss, precision_score, recall_score, roc_auc_score,
)

assert (dev_label_available <= FIT_CUTOFF).all()
assert (threshold_raw["label_available_at"] <= THRESHOLD_LOCK_AT).all()

# 该模型只使用在 2026-06-01 已成熟的开发标签拟合。
selected_model = clone(search.best_estimator_).fit(X_dev, y_dev)
threshold_prob = selected_model.predict_proba(X_threshold)[:, 1]

candidates = []
for t in np.linspace(0.05, 0.95, 91):
    pred = (threshold_prob >= t).astype(int)
    candidates.append({
        "threshold": t,
        "precision": precision_score(y_threshold, pred, zero_division=0),
        "recall": recall_score(y_threshold, pred, zero_division=0),
        "f1": f1_score(y_threshold, pred, zero_division=0),
    })
feasible = [row for row in candidates if row["recall"] >= 0.80]
chosen = max(feasible, key=lambda r: r["precision"]) if feasible else max(
    candidates, key=lambda r: r["f1"]
)
threshold = float(chosen["threshold"])

# 不重新拟合:阈值必须对应实际用于评分的同一模型。
final_model = selected_model

test_prob = final_model.predict_proba(X_test)[:, 1]
test_pred = (test_prob >= threshold).astype(int)
print("threshold:", threshold)
print(confusion_matrix(y_test, test_pred))
print(classification_report(y_test, test_pred, digits=3, zero_division=0))
print("ROC-AUC:", roc_auc_score(y_test, test_prob))
print("Average precision:", average_precision_score(y_test, test_prob))
print("Log loss:", log_loss(y_test, test_prob))

6 月标签要等到 8 月 1 日前全部成熟,才能锁定阈值;8 月样本可从该日起按快照评分,其测试指标则要等 8 月标签陆续成熟后再计算。测试输出是一次最终报告,不是新一轮调参依据。

若必须利用阈值期数据重训,就要再留出一个更晚、标签已成熟的阈值/校准集:先拟合真正要上线的模型,再在该独立集合上为这个模型定阈值。数据很少时,也可设计严格的时间嵌套 OOF 流程,但实现和计算成本更高。

5. 保存后直接预测原始记录

# 教学示例:未运行验证;只加载可信来源的 joblib/pickle
import joblib
import pandas as pd

artifact = {
    "model": final_model,          # 内含原始 schema 校验、特征、预处理和分类器
    "threshold": threshold,
    "feature_code_version": "churn-features==1.0.0",
    "label_definition": "snapshot 后 30 天内是否主动流失",
}
joblib.dump(artifact, "churn_pipeline.joblib")

loaded = joblib.load("churn_pipeline.joblib")
new_raw = pd.DataFrame([{
    "customer_id": "C9001",
    "snapshot_date": "2026-09-01",
    "tenure_months": 3,
    "monthly_charges": 118.0,
    "support_calls_90d": 4,
    "contract_type": "monthly",
    "payment_method": "card",
    "last_login_date": "2026-08-12",
}])
probability = loaded["model"].predict_proba(new_raw)[:, 1]
prediction = (probability >= loaded["threshold"]).astype(int)
print({"churn_probability": float(probability[0]), "action": int(prediction[0])})

加载环境仍需安装相同的 churn_features 模块及兼容依赖。生产制品还应记录代码提交、依赖锁文件、训练时间范围、标签可用截止时间、schema、模型参数、阈值、验证报告和负责人。

分类模型如何选择

表格分类可先建立三层短名单:

  1. DummyClassifier,确认是否超过先验规则;
  2. 正则化逻辑回归,速度快、适合稀疏 one-hot、较易解释;
  3. 一个树模型基线,如随机森林或梯度提升,用于表达非线性与交互。

SVM 可在数据量和尺度合适时作为有理由的候选,但概率输出和计算成本要单独考虑。选择模型还要看方差、延迟、内存、可解释性和维护成本,而不是只看一项分数。

多分类模型可能采用 One-vs-Rest、One-vs-One 或原生多分类目标。具体估计器的默认策略与参数可能随版本变化,应查看对应版本文档,并报告每个类别的混淆情况。

分类指标:匹配真正的决策

设流失为正类:TP 是预测流失且确实流失;FP 是误报;FN 是漏报;TN 是正确识别未流失。混淆矩阵是许多指标的起点。

scikit-learn 的模型评估指南强调,指标应与预测对象和业务决策对齐,而非机械使用默认 score。

多分类的平均方式

最好同时报告每类指标、support、macro 与 weighted,而不是只给一个总分。

类别不平衡、阈值与概率校准

先判断不平衡是现实基率还是采样偏差,再考虑:选择 PR 指标、召回率、precision@k 或成本;确保各验证期有正样本;尝试 class_weight;仅在训练折内重采样;最后按成本或容量调阈值。

阈值可采用约束式(召回至少 80% 时最大化 precision)、成本式或容量式(只联系 top-k)。如果下游需要“0.8 就约等于 80% 风险”,还要检查校准曲线和 Brier score,并在独立验证数据或交叉验证内拟合校准器。排序好不等于概率准。阈值或校准器必须对应最终评分模型;模型重新拟合后应重新验证它们。

回归:从房价案例理解模型与指标

将任务改为房价回归时,样本是一套在估价日可观察的房屋,标签是之后的实际成交价。面积、房龄、区域和估价日前市场统计可以作为特征;成交后的议价记录不能使用。

代表性基线包括 DummyRegressor(strategy="median")、Ridge/Elastic Net 和一个树集成模型。

豪宅大额错误特别昂贵时可重视 RMSE;强调典型金额误差时可优先 MAE。长尾目标可比较对数变换,并用 TransformedTargetRegressor 封装逆变换。还应按价格带、区域和时间报告误差。

调参、误差分析与可解释性

交叉验证策略就是对上线场景的假设。普通独立样本可使用分层 k 折;时间数据使用扩展或滚动窗口;目标是未见实体时使用分组验证。延迟标签还要求每折训练标签在该折拟合时点已经可用。报告均值时也应报告折间波动。

推荐顺序是:固定切分与主指标,建立 Dummy 和简单模型,修正流程问题,只搜索少数关键参数,比较验证均值、波动和成本,锁定特征、模型与阈值后再碰测试集。大量尝试会使普通验证分数产生选择偏差;要求严格无偏时可使用嵌套交叉验证。

误差分析应检查 FP/FN、新老客户、渠道、地区、缺失多的样本、罕见类别、边界值和最新月份。每轮只形成可验证假设,而不是盲目增加几十列。

逻辑回归系数、树重要性、置换重要性、PDP、ICE 和 SHAP 都是在解释模型,不自动代表因果。共线性、相关特征和高基数会影响解释;泄漏模型也能生成漂亮的解释图。

最终测试、部署和监控

最终报告至少包含数据时间范围、标签成熟截止时间、样本量、正类比例、主辅指标、固定阈值下的混淆矩阵、业务切片、基线比较、延迟、资源与已知限制。

部署时保存从原始 schema 到预测的完整 Pipeline,并校验字段名称、类型、范围、必填项和未知类别。上线监控分三层:

  1. 系统监控:请求量、延迟、错误率、空值和 schema 变化;
  2. 数据漂移:输入分布或类别比例变化;
  3. 概念漂移:P(y|X) 改变,同样行为对应的风险不同。

数据漂移不必然导致性能下降。若标签延迟 30 天,可先监控输入和预测分布,标签到齐后再计算 precision、recall、校准与业务收益。重训还需要触发条件、审批、回滚和重新验证。

常见错误与排障清单

症状常见原因优先检查
验证分数接近 1标签泄漏、重复样本、未来字段字段可用时间、主键、标签逻辑
时间回测异常乐观训练用了尚未成熟的延迟标签label_available_at、拟合时点与 embargo
验证好、线上差随机切分不符合时间/分组结构改做时间或分组回测
训练正常、预测缺列推理链未包含特征生成保存并加载原始 schema 到模型的 Pipeline
新类别报错编码器不接受未知类别handle_unknown="ignore" 与监控
accuracy 高但抓不到正类类别不平衡、阈值不合适混淆矩阵、PR 指标、阈值
换模型后约束失效沿用了旧模型选择的阈值用最终评分模型重新选阈值
结果波动大样本少、月份差异、随机性固定切分并报告多折波动
系数巨大或不收敛尺度差异、共线性、迭代不足缩放、正则化、范围和警告
MAPE 离谱真实值为零或接近零改看 MAE/RMSE
重要性违反常识泄漏、代理变量、相关特征逐列审计与切片分析
测试分数不断变好反复依据测试结果改方案重建真正未见的留出集

分阶段学习路径

阶段一:完成可靠基线

学习 pandas 类型、缺失和分组统计;任务定义;基本切分;插补、编码、缩放;逻辑回归、线性回归、树;混淆矩阵与 MAE/RMSE。练习:完成 Dummy 与逻辑回归 Pipeline,并写一页审计报告。

阶段二:掌握防泄漏实验

学习 ColumnTransformer、Pipeline、分层/分组/时间验证、标签成熟边界、超参数搜索、PR-AUC、log loss、R²、阈值和误差切片。练习:故意加入“取消日期”,比较错误流程与正确流程。

阶段三:把特征工程变成假设检验

学习时间窗口、比率、趋势、交互、长尾变换、特征选择、PCA、多分类平均、概率校准和置换重要性。练习:将流失改成原因多分类,并完成房价回归项目。

阶段四:完成可交付项目

学习数据与模型版本、schema 校验、批量/在线推理、漂移、延迟标签、回滚和重训。成果应包括问题合同、数据字典、审计报告、训练脚本、验证与测试报告、模型制品、推理示例和监控方案,而不只是 Notebook。

最终流程检查表

问题、审计与切分

特征与模型

交付与监控

成熟的传统机器学习流程,不是模型越来越复杂,而是每一步都能回答:为什么做、只从哪里学习、此刻哪些标签真正可用、上线后如何保持一致。守住预测时点、标签成熟时间和训练/测试边界,再用 Pipeline 固化原始数据到预测的整条处理链,分类与回归就能共享同一套可靠方法论。


编辑文章
分享这篇文章:

上一篇
vLLM、FSDP 与大模型训练到推理:一份端到端基础设施指南
下一篇
梯度下降优化器与梯度提升树:底层原理、演进脉络与选型指南