小Cの已经记不起来的博客

机器学习入门:链路追踪 数据预处理

上周把两个月前写的一个用户流失模型重新跑了一遍,AUC直接从0.81掉到了0.76。数据文件没换,代码也没动过,就很离谱。排查了一晚上才发现,中间某次我图省事手动删过几十行脏数据,什么都没记,这次全量重跑,那几十行又混回来了。预处理这玩意儿不留痕是真的会翻车啊,而且翻得你一点脾气都没有...

下面是我踩完坑之后整理的一套做法,没什么高深的东西,全是笨办法,但实测管用。

链路追踪到底追踪个啥

这词儿本来是后端那边的,微服务调用链路那个意思。放到数据预处理里,说白了就一件事:你得随时能回答“这行数据从哪来、被我干过什么”。

原始数据 → 清洗 → 缺失值填充 → 编码 → 归一化 → 切分训练测试集,每走一步数据就变个样。中间任何一步没记下来,出了问题你就只能对着两份长得差不多的 CSV 干瞪眼,别问我怎么知道的。这种坑最恶心的是平时看不出来,训练正常、指标也正常,坏就坏在你想复现之前的结果时,根本对不上。

原始数据永远别动

第一条铁律。原始文件拿过来先 copy 一份,所有操作都在副本上做:

import pandas as pd

df_raw = pd.read_csv("data/user_behavior_20240501.csv")
df = df_raw.copy()  # 后面随便折腾,原始的还在

别嫌这步多余。等哪天预处理代码改崩了,你还能从头再来,不然就只能对着回收站赌运气了。

缺失值:先看,别上来就删

见过不少人拿到数据第一步就是 df.dropna(),一下干掉三成样本,模型能好才怪。先看一眼每列到底缺多少再说:

print((df.isnull().sum() / len(df)).sort_values(ascending=False))

缺得少的(5% 以内)可以填,数值列填中位数,类别列填众数;缺得太狠的列,比如 80% 都是空的,那这列本身就没多少信息量,整个丢掉反而干净。用了什么填充策略、按哪个字段填的,记下来,这些是链路上实实在在的一环。

归一化:fit 只能碰训练集

新手最容易踩的坑。要是在切分之前就对全量数据 fit scaler,测试集的信息就泄漏进训练过程了,评估指标看着挺美,一上线就露馅。正确顺序是先切分:

from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

然后 scaler 只在训练集上 fit,测试集只 transform。手动写很容易搞混,所以更推荐下面这种一劳永逸的做法。

用 Pipeline 把整条链路串起来

sklearnPipeline,我愿称之为官方自带的链路追踪。填充、编码、缩放、模型,全部打包成一个对象,每一步做了什么、按什么顺序做的,写得明明白白,还顺手把泄漏问题解决了:

from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.linear_model import LogisticRegression

num_cols = ["age", "days_since_last_login"]
cat_cols = ["city", "device_type"]

num_pipe = Pipeline([
    ("imputer", SimpleImputer(strategy="median")),
    ("scaler", StandardScaler()),
])

cat_pipe = Pipeline([
    ("imputer", SimpleImputer(strategy="most_frequent")),
    ("onehot", OneHotEncoder(handle_unknown="ignore")),
])

preprocess = ColumnTransformer([
    ("num", num_pipe, num_cols),
    ("cat", cat_pipe, cat_cols),
])

model = Pipeline([
    ("preprocess", preprocess),  # <--- the whole chain lives here
    ("clf", LogisticRegression(max_iter=1000)),
])

model.fit(X_train, y_train)

训练完直接把整个 pipeline 存下来,推理的时候加载同一个对象,线上线下处理逻辑想不一致都难:

import joblib

joblib.dump(model, "churn_pipeline_20240501.pkl")

文件名里带上日期,多存几个版本,回头想对比“上个月的填充策略”和“这个月的”,直接加载两份跑一遍就完了。不同版本的 sklearn 参数没准儿有点出入,报错了翻下文档就行。

最后,用大白话再记一份

Pipeline 管的是代码层面的链路,但还有一层它管不着,就是“为什么这么干”。随手维护个 notes.txt 就够了:数据是哪天从哪拿的、删了哪几行、为什么用中位数而不是均值填充、什么时候改过参数。不用多正式,能让自己三个月后看懂就行。

预处理这东西,说难真不难,全是细致活。所谓链路追踪,说白了就是逼自己把每一步都摆在明面上。前期多花十分钟记一笔,后面少熬一晚上排查,这笔账怎么算都划算。

评论

还没有评论。

发表评论

提交后评论将经过自动审核,审核通过后公开展示。

未在播放