☰
结构化数据竞赛避坑:缺失值处理、过采样与baseline训练
2026/10/27 6:07:47 网站建设 项目流程

上一篇把数据的读取、字段含义和初步的分布摸了一遍,这一篇接着往下走,聊的是真正决定分数下限的那三件事:缺失值处理、过采样、baseline训练。这三件事听起来都像是调个库就能搞定的活儿,但我见过太多队伍在这一步埋雷——填充做完才发现把测试集信息漏进了训练集,过采样跑完发现交叉验证分数虚高得离谱,baseline 明明跑通了但提交上去排名倒数。问题不在代码写错了,而在于每一步背后的判断逻辑没想清楚:这份数据里的缺失到底是"没记录"还是"没发生"?不平衡比到什么程度才值得上 SMOTE?baseline 该用哪个模型打底才不至于后面越调越乱?这篇就按我自己的实操顺序,把这三个环节拆开讲透,包括参数怎么算、管道怎么封、坑在哪里,适合刚接手这类结构化赛题、想快速搭出一套能迭代的框架的朋友参考。

1. 赛题数据整体诊断与处理思路拆解

1.1 先给缺失情况画一张"体检报告"

拿到一份几十列、几万行的结构化数据,最忌讳的就是上来就df.fillna(0)。我习惯先做三件事,把缺失的"形状"摸清楚。第一件是算缺失率并按列排序,把字段分成四档:零缺失、轻度缺失(低于 5%)、中度缺失(5% 到 50%)、重度缺失(超过 50%)。这个分档不是为了套公式,而是为了决定处理方向——轻度缺失怎么填影响都不大,重度缺失要么直接砍列,要么保留缺失标记让模型自己判断,中间那档才是真正需要花心思的地方。

第二件是看缺失的分布是否与标签相关。这一步很多人跳过,但它极其关键。具体做法很简单,生成一个布尔列is_missing,然后分组统计正负样本比例。如果某一列的缺失率在正样本里是 30%、负样本里是 12%,那这个"缺失"本身就是强特征,你在填充的时候就必须额外保留一个xxx_isnull的标记列,否则填完之后信息就全丢了。我在一次风控类的赛题里就吃过这个亏,某个字段的缺失和违约高度相关,填充完中位数之后模型分数直接掉了两个点,回头加上标记列才补回来。

第三件是抽查具体值,判断是"真缺失"还是"伪缺失"。有些字段的空值其实是业务里的"未发生",比如消费金额为空可能意味着这个用户根本没消费过,填均值就是把"零消费"扭曲成了"平均消费";还有些字段的缺失被编码成了 0、-1、"unknown"、"未知"这类占位符,pandas 的isna()根本识别不出来,需要手动替换成np.nan再统一处理。这一步没有捷径,只能对着字段说明一个个过,但花半小时能省掉后面几小时的返工。

import pandas as pd import numpy as np def missing_report(df, target=None): rep = pd.DataFrame({ "missing_cnt": df.isna().sum(), "missing_rate": df.isna().mean().round(4), "dtype": df.dtypes.astype(str), "nunique": df.nunique() }).sort_values("missing_rate", ascending=False) if target is not None: rate_by_pos = df[df[target] == 1].isna().mean() rate_by_neg = df[df[target] == 0].isna().mean() rep["gap"] = (rate_by_pos - rate_by_neg).round(4) return rep

这段函数输出一张表,重点看两列:missing_rate决定处理策略,gap决定要不要保留标记。gap绝对值超过 0.05 的字段,我基本都会加一列缺失标记。

1.2 处理顺序为什么必须是"划分→填充→编码→采样→训练"

顺序错了,分数虚高,这是这类赛题最常见的翻车点。举个具体的例子:如果你先在全量数据上算中位数做填充,再划分训练集和验证集,那么验证集的填充值里已经包含了训练集的信息,这就叫数据泄漏。同理,先做 SMOTE 再划分数据集,等于让模型在训练阶段就"见过"由验证集样本插值生成的合成样本,交叉验证分数会漂亮得不像话,提交到线上直接现原形。

正确的流水线顺序是:先按标签分层划分训练/验证集 → 只在训练集上fit填充器和编码器 → 用fit好的对象去transform验证集 → 只在训练集上做过采样 → 训练模型 → 在原始分布的验证集上评估。注意最后一步,验证集绝对不能碰任何采样操作,它必须保持真实的不平衡比例,否则你评估出来的指标毫无意义。

我的做法是直接把整条链封进imblearn的 Pipeline 里,让fit和transform的作用域由框架来保证,而不是靠人肉记忆。这样写的好处是,做交叉验证的时候,每一折的填充、编码、采样都只在该折的训练部分完成,验证部分只做 transform,泄漏问题从结构上就被堵死了。

注意:sklearn的 Pipeline 和imblearn的 Pipeline 不是同一个东西。只要流程里包含采样器(SMOTE、RandomUnderSampler 等),就必须用imblearn.pipeline.Pipeline,用 sklearn 那个会在交叉验证时报错或者静默出错。

1.3 采样策略的取舍逻辑:不是所有不平衡都需要动手

很多人一看到标签分布是 1:10 就急着上 SMOTE,其实没必要。经验上,不平衡比在 1:4 以内,树模型基本能自己扛住,调class_weight就够了;1:4 到 1:20 之间是 SMOTE 的主战场,收益明显;超过 1:50 就要谨慎了,这时候少数类的样本量可能只有几十条,SMOTE 插值出来的样本高度同质化,反而会加剧过拟合,更稳妥的路线是欠采样 + 集成,或者干脆把问题转成异常检测的思路。

这里的不平衡比算法很简单:IR = 多数类样本数 / 少数类样本数。假设训练集有 8000 条负样本、400 条正样本,IR = 20,属于典型的"值得上采样"区间。但还要注意另一个数——少数类的绝对样本量。如果少数类只有 50 条,就算 IR 是 100,我也更倾向于先做数据增强或者干脆放弃这类样本的精细建模,因为 5 折交叉验证下每折的少数类只有 40 条,SMOTE 用它生成新样本的可靠性很低。

2. 缺失值处理的实操细节与判断依据

2.1 数值型字段的四种填充方案与适用条件

数值型缺失的填充方案,我按复杂度从低到高排了四档,实际用的时候按字段重要性挑,而不是全表统一。

第一档是中位数填充。选它而不是均值,理由很实在:赛题数据里数值字段几乎都有长尾和离群点,均值会被极端值拽偏,中位数对异常值不敏感。代价是所有缺失样本被赋了同一个值,方差被人为压缩了。对于树模型来说这个代价可以接受,因为树只关心分裂点的位置,不关心方差。

第二档是分组填充。如果字段之间存在明显的层级关系,比如"用户"下面有多个"订单",那用用户级的中位数去填订单级的缺失,比全表中位数合理得多。实现上就是groupby加transform,两行代码的事,但效果比全局填充好一个档次。

第三档是 KNN 填充。KNNImputer的思路是找特征空间里最近的 k 个样本,用它们的值加权平均。用之前必须先标准化,否则量纲大的字段会主导距离计算,填出来的结果毫无意义。k 值我一般取 5 到 10,样本量大就取小一点。这个方案的缺点是慢,几万行乘几十列可能跑几分钟,而且对高维稀疏数据效果差。

第四档是模型填充。把有缺失的列当标签,其他列当特征,训练一个回归或分类模型来预测缺失值。理论上最优雅,实际上最容易出事——如果填充模型本身过拟合,会把噪声当成规律灌进数据里,而且训练成本高。我一般只在少数几个关键字段上用它,而且会做对比实验,确认它真的比中位数填充带来提升才留下。

方案实现成本适用场景主要风险
中位数填充极低缺失率低于 20%,字段重要性一般压缩方差,忽略组间差异
分组中位数低有明确层级/分类结构小分组样本不足时不稳定
KNN 填充中特征相关性强,样本量中等需标准化,慢,高维失效
模型填充高关键字段,缺失率中等过拟合,误差传播

2.2 树模型原生支持缺失,为什么还要填

这是个很实际的问题。LightGBM 和 XGBoost 确实能直接吃np.nan,内部会为缺失值单独走一个分支方向,理论上不填也能跑。但我实际的做法是:baseline 阶段不填,直接喂给 LightGBM 先跑通;进入迭代阶段后,对关键字段做填充并保留标记列,做 A/B 对比。

理由是树模型的缺失处理是"默认方向"机制,它把所有缺失样本统一送到某个子节点,这在大规模数据上很高效,但丢失了缺失样本内部的差异性。如果你的数据里缺失本身携带信息(前面提到的gap明显),那么显式填充 + 标记列的组合,往往比让模型自己猜要好。反过来,如果缺失完全是随机的,那填不填差别不大,省点代码直接喂进去更划算。

我在一次比赛里做过对照:同一份数据、同一个 LightGBM 参数,方案 A 直接喂 NaN,方案 B 用分组中位数填充 + 12 个标记列。5 折交叉验证的结果是方案 B 的 F1-macro 高出 0.008,提升不大但稳定。这个量级的差异在排行榜上可能就是几十名,所以值得做,但不值得花太多时间调填充策略。

2.3 类别型缺失:把"缺失"变成一个新类别

类别型字段的缺失,我一律不填众数,而是填一个固定的字符串,比如"__MISSING__",然后在独热编码时让它成为一个独立维度。这么做的逻辑是:如果这个字段的缺失和标签无关,那模型学到的这一维权重就接近零,没有损失;如果相关,模型就能直接利用这个信号。填众数的做法等于强行把缺失样本归到最常见的那一类,凭空制造了一批"伪同类",风险更大。

实现的时候有个细节要注意:OneHotEncoder默认遇到训练时没见过的类别会报错,必须设置handle_unknown="ignore"。这在比赛里特别重要,因为验证集或者测试集里出现训练集没有的类别值是很常见的,不设这个参数会直接崩掉。

还有一个容易被忽略的点是高基数类别字段。比如某个 ID 类字段有几千个不同取值,独热编码后维度爆炸,这时候要么做频次编码(用出现次数替代原值),要么做目标编码(用该类别下的标签均值替代),要么直接交给 LightGBM 的类别特征支持。这三种我按优先级排:LightGBM 原生类别特征最省事,目标编码效果最好但必须做折内交叉防止泄漏,频次编码最安全但信息损失大。

2.4 缺失率阈值的取法:为什么我定在 80% 而不是 50%

删列阈值这个东西没有标准答案,我的经验值是80%。理由是:缺失率超过 80% 的字段,剩余的有效信息已经很少,就算保留标记列,模型能学到的也只是"这批样本缺这个字段"这一个比特的信息,而它带来的噪声和过拟合风险往往大于收益。50% 到 80% 之间我倾向于保留,因为这类字段常常还是有区分度的,砍掉可惜。

删行则要谨慎得多。只有在缺失率极低(比如某一行缺了 90% 的字段)、而且这一行明显是脏数据的情况下才删。比赛数据本来就宝贵,尤其是少数类样本,删一条少一条,宁可填一个粗糙的值也不要轻易丢样本。我见过有队伍为了"数据干净"删掉了 15% 的行,结果少数类样本被删掉一半,模型直接学不动了。

注意:缺失率统计必须在划分数据集之前做,但填充值的计算必须在划分之后做。这两件事经常被混在一起,前者是描述性统计,后者是特征变换,性质完全不同。

3. 过采样的正确姿势与参数选择

3.1 先换评估指标,再谈采样

在动手采样之前,有一件事必须先做:把评估指标从 accuracy 换掉。假设正负样本比例是 1:20,一个把所有样本都预测为负类的模型,accuracy 是 95%,看起来很美,实际上毫无用处。这类赛题里我用的指标组合是:F1-macro(主指标)、AUC、以及混淆矩阵。F1-macro 对每个类别一视同仁,不会被多数类淹没;AUC 衡量排序能力,对阈值不敏感;混淆矩阵用来确认模型是不是在"一边倒"。

如果赛题官方指定了评估指标,那就以官方为准,但私下做实验的时候我还是会同时看 F1-macro 和 AUC,因为官方指标有时候不够敏感,看不出模型结构的差异。比如某些赛题用加权 F1,权重按样本数分配,那它本质上还是偏向多数类,需要额外关注少数类的召回率。

3.2 SMOTE 家族怎么选,k_neighbors 怎么定

SMOTE 的核心逻辑是:对每个少数类样本,在它的 k 个最近邻里随机挑一个,在这两点连线上随机取一点作为新样本。理解了这一点,参数就好定了。

k_neighbors默认是 5,含义是"参考 5 个邻居来插值"。这个值不能超过少数类样本数减一,否则会报错。如果少数类只有 30 条样本,k 最多设到 29,而实际上设到 5 就够了,设太大反而会让新样本分布过于弥散、失去局部性。我的经验是:少数类样本少于 100 条时 k 取 3 到 5,100 到 1000 条时取 5 到 10,上千条时可以取到 10 以上。

至于用哪个变体:标准 SMOTE 最通用;Borderline-SMOTE只在类别边界附近生成样本,适合决策边界模糊的场景;ADASYN根据密度自适应决定每个样本生成多少新样本,适合少数类分布不均匀的情况。我在实际比赛里用得最多的是标准 SMOTE,因为它最不容易出幺蛾子,ADASYN偶尔能带来提升但结果波动较大,不适合作为主力。

还有一个重要限制:SMOTE 只能处理数值特征。如果数据里有类别列,要么先编码成数值(独热后可以,但插值出来的值没有意义),要么用SMOTENC专门处理混合类型。文本或高维稀疏特征上更是不建议用 SMOTE,因为线性插值在两个稀疏向量之间生成的样本,往往既不接近任何真实样本,也没有业务含义,纯属噪声。

from imblearn.over_sampling import SMOTE # 计算采样比例:把少数类补到多数类的 60% n_maj, n_min = 8000, 400 target_ratio = 0.6 n_min_target = int(n_maj * target_ratio / (1 - target_ratio) * (n_min / n_maj) * 0) # 简化写法 sampling_strategy = {1: int(n_maj * 0.6)} # 把标签为 1 的类补到 4800 条 smote = SMOTE( sampling_strategy=sampling_strategy, k_neighbors=5, random_state=42 )

这里sampling_strategy我不建议直接设成"auto"(补到与多数类等量),因为把 400 条补到 8000 条意味着要生成 7600 条合成样本,合成比例高达 19:1,数据里全是插值出来的"假样本",模型会严重过拟合。补到多数类的 50% 到 70% 通常是个甜点,既缓解了不平衡,又保留了真实样本的主导地位。

3.3 用 imblearn Pipeline 锁死作用域

前面提过,采样器必须包在imblearn的 Pipeline 里。下面是我常用的完整结构,包含缺失填充、编码、采样、建模四个环节:

from sklearn.compose import ColumnTransformer from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.impute import SimpleImputer from imblearn.pipeline import Pipeline from imblearn.over_sampling import SMOTE from lightgbm import LGBMClassifier num_cols = [...] # 数值列 cat_cols = [...] # 类别列 num_pipe = Pipeline([ ("imputer", SimpleImputer(strategy="median")), ("scaler", StandardScaler()) ]) cat_pipe = Pipeline([ ("imputer", SimpleImputer(strategy="constant", fill_value="__MISSING__")), ("ohe", OneHotEncoder(handle_unknown="ignore", sparse_output=False)) ]) preprocess = ColumnTransformer([ ("num", num_pipe, num_cols), ("cat", cat_pipe, cat_cols) ], remainder="drop") clf_pipe = Pipeline([ ("prep", preprocess), ("smote", SMOTE(sampling_strategy=0.6, k_neighbors=5, random_state=42)), ("model", LGBMClassifier( n_estimators=600, learning_rate=0.05, num_leaves=31, min_child_samples=20, subsample=0.8, colsample_bytree=0.8, random_state=42, n_jobs=-1 )) ])

注意这里的sampling_strategy=0.6,它表示采样后少数类样本数占多数类的 60%,等价于我前面手算的比例。用浮点数比手算整数更省心,改动起来也方便。

再强调一次:验证集不做任何采样。做交叉验证时,cross_val_score或者cross_validate会自动在每一折的测试部分跳过采样器,这是 imblearn Pipeline 的设计保证。但如果你是手动划分训练验证集,那就必须自己在验证集上只调prep.transform而不碰smote。

3.4 采样之外的三条补充路线

SMOTE 不是唯一解,我通常还会并行试三条路线做对比。

路线一是class_weight。LightGBM 里设class_weight="balanced"或者手动传{0: 1, 1: 20},让损失函数对少数类更敏感。这个方案零成本、零风险,很多时候效果和 SMOTE 差不多,甚至更好,因为它是从损失函数层面修正,不会引入合成样本的偏差。

路线二是阈值移动。训练完模型后不直接用 0.5 做判定,而是用precision_recall_curve找 F1 最大的那个阈值。这个方法特别适合评估指标是 F1 或者 recall 的场景,几乎不花额外成本就能拿到一两个点的提升。我一般的做法是:交叉验证时用 OOF 预测算最优阈值,然后在测试集上套用这个阈值。

路线三是欠采样混合。先用RandomUnderSampler把多数类砍掉一部分(比如砍到原来的 50%),再用 SMOTE 补少数类。这样做的收益是数据量可控、训练速度快,代价是丢了部分多数类信息。在多数类样本极多(十万级以上)的时候,这个方法比纯过采样实用得多。

方案实现成本提升稳定性主要副作用
class_weight极低稳定概率校准偏移
SMOTE 过采样低中等合成样本同质化
阈值移动极低稳定需 OOF 预测配合
欠采样+过采样中中等丢失多数类信息

4. baseline 训练与迭代路径设计

4.1 baseline 的意义是"可对比",不是"分数高"

我见过不少人在 baseline 阶段就疯狂调参,想把初始分数拉到最高,结果后面做特征工程的时候没有干净的参照系,改了半天也说不清到底哪一步起了作用。baseline 的正确定位是一个稳定、可复现、可对比的下界。它不需要多强,但必须满足三个条件:固定的随机种子、固定的划分方式、固定的评估函数。有了这三样,后面任何一次改动都能做 A/B 对比。

我的 baseline 一般是三件套并行跑:逻辑回归(线性下界)、随机森林(非线性下界)、LightGBM(梯度提升下界)。逻辑回归必须配标准化,否则收敛慢甚至不收敛;随机森林几乎不用调参,n_estimators=300就能用;LightGBM 用一套保守参数,学习率 0.05、树数 600、叶子数 31。三个跑完,看分数分布,如果 LightGBM 明显领先,后面就以它为基座迭代;如果三者差距不大,说明特征的信息量有限,重点应该放在特征工程而不是模型调参。

from sklearn.model_selection import StratifiedKFold, cross_validate from sklearn.metrics import make_scorer, f1_score, roc_auc_score cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) scoring = { "f1_macro": make_scorer(f1_score, average="macro"), "auc": make_scorer(roc_auc_score, needs_proba=True) } scores = cross_validate(clf_pipe, X_train, y_train, cv=cv, scoring=scoring, n_jobs=-1, return_train_score=True) print("CV f1_macro: %.4f ± %.4f" % (scores["test_f1_macro"].mean(), scores["test_f1_macro"].std())) print("CV auc: %.4f ± %.4f" % (scores["test_auc"].mean(), scores["test_auc"].std()))

标准差这一项很值得看。如果 5 折的标准差超过 0.03,说明模型对数据划分太敏感,这时候光看均值没意义,得先解决方差问题——要么增加折数(比如用 10 折),要么换更稳定的模型,要么检查是不是有某几折的少数类样本特别少。

4.2 交叉验证的分层设计不能省

不平衡数据上做交叉验证,KFold是绝对不能用的,必须用StratifiedKFold。原因很直接:KFold随机切分,可能出现某一折里少数类样本只有个位数甚至零个,那一折的 F1 会直接崩到零,均值被拉得面目全非。

除了分层,还有两个细节值得注意。第一是折数,5 折是默认选择,但如果少数类样本量少于 200 条,我会加到 10 折,让每折的验证集小一点,这样每一折的评估更充分,均值的估计更可靠。第二是重复,如果数据集不大(几千行),单次 5 折的结果偶然性很强,用RepeatedStratifiedKFold(n_splits=5, n_repeats=3)跑 15 次取平均,结果会扎实很多,代价是训练时间翻三倍。

还有一个隐藏的坑:分组交叉验证。如果数据里有用户 ID、设备 ID 这类字段,同一个 ID 对应多条样本,那必须用GroupKFold,保证同一个 ID 的样本不会同时出现在训练折和验证折里。否则模型会"记住"这个 ID 对应的标签,验证分数虚高,线上直接掉分。这类赛题里这种情况非常普遍,值得花两分钟检查一下。

4.3 最小可用特征集与实验日志

baseline 阶段的特征工程,我建议只做"最小可用集",也就是原始字段加上少量低风险的衍生特征,比如数值列的比值、时间列的星期几、类别列的频次。不要在这个阶段做目标编码、聚类特征这些复杂的东西,否则一旦分数不对,你分不清是模型的问题还是特征的问题。

比特征更重要的,是实验日志。我见过太多队伍改到后期完全忘了哪次改了什么,分数为什么涨的不知道,跌的也不知道。我的做法是在项目根目录维护一个 CSV,每次跑完实验追加一行,字段包括:实验编号、数据版本、特征版本、模型参数摘要、CV 均值、CV 标准差、备注。看着土,但真到了需要复现最佳结果的时候,这张表能救命。

实验编号关键改动CV F1-macroCV AUC结论
E01中位数填充 + LR0.58210.8103线性下界
E02同上 + RF0.61040.8367非线性有效
E03同上 + LGBM0.62880.8512当前最佳
E04加缺失标记列0.63510.8549保留
E05加 SMOTE(0.6)0.64170.8533F1 升 AUC 略降
E06SMOTE + 阈值移动0.65020.8541保留

4.4 从 baseline 往上迭代的四条主线

跑通 baseline 之后,提升路径我按投入产出比排序,大概是这么四条。

第一条是补特征。这是收益最大也最耗时的方向。具体做什么取决于赛题领域,通用的思路是:交叉组合(两个类别列拼接成新列)、聚合统计(用户级均值、最大、最小、计数)、时间差(首次到末次的时间间隔)、比率(两个数值列相除)。每加一批特征,都要回到实验日志里记录,并且确认 CV 分数是涨的、标准差没有变大。

第二条是调参。LightGBM 的参数里,learning_rate和n_estimators是一对,学习率小则树数多,一般 0.03 到 0.1 之间搭配 500 到 2000 棵树;num_leaves控制复杂度,31 是安全值,数据量大可以往上调;min_child_samples防过拟合,20 到 50 之间;subsample和colsample_bytree一般设 0.7 到 0.9。我的习惯是用 Optuna 跑 50 到 100 组,但只在固定的交叉验证划分上调,调完立刻在另一套随机种子的划分上验证,防止过拟合到验证集。

第三条是模型集成。LightGBM、XGBoost、CatBoost 三个跑完做加权平均,权重用 OOF 预测搜索。这一步通常能带来千分之几到百分之一的提升,性价比不错。再往上是 stacking,用 OOF 预测作为第二层的输入,但要注意第二层必须用简单的模型(逻辑回归就行),否则容易过拟合。

第四条是重采样策略的精细化。比如对不同的少数类子群分别做采样,或者用SMOTE加上Tomek Links做混合采样(先过采样再清理边界噪声)。这条路的收益不稳定,我一般放在最后试。

5. 常见问题与排查技巧实录

5.1 踩过的坑与对应解法速查

下面这张表是我和队友在几次比赛里实际遇到过的问题,按出现频率排序,遇到报错或者分数异常的时候可以直接对照。

现象可能原因排查与解法
CV 分数 0.95+,线上 0.6采样或填充在划分前做了,数据泄漏检查是否用了 imblearn Pipeline,验证集是否被采样
某折 F1 突然为 0该折验证集无少数类样本改用 StratifiedKFold,增加折数
OneHotEncoder 报未知类别测试集出现了训练集没有的类别设handle_unknown="ignore"
SMOTE 报 k_neighbors 错误少数类样本数小于 k+1调小 k,或先合并稀有类别
过采样后内存爆掉数据量翻十倍以上改用欠采样+过采样混合,或用生成器分批
训练集分数高验证集低合成样本同质化,过拟合降低采样比例到 0.3-0.5,加正则
CatBoost 报类别特征格式错误类别列含 NaN 或类型不一致统一转成字符串后再声明 cat_features
同一份代码两次结果不同随机种子未固定在划分、采样、模型三处都设 random_state

有一类问题特别隐蔽,值得单独说:标签编码错位。用过采样之后,y_train的索引会变,如果你之前手动维护了一个和原索引对齐的辅助数组(比如样本权重、或者原样本的 ID),采样后这个对齐关系就断了。表现出来是模型能训练但分数莫名其妙地低。解法很简单,采样后重新从smote.fit_resample的返回值里取y_res,所有辅助信息要么一起重采样,要么干脆别用。

5.2 几条不太写在文档里的实操心得

心得一:先跑通再优化,别追求一步到位。我见过有人第一天就搭了个七层 stacking,结果因为中间某一层的数据泄漏,分数虚高到 0.98,等发现的时候所有实验记录全部作废。正确顺序永远是:能跑通的最简版本 → 加特征 → 调参 → 集成。每一步都留一个可回退的版本。

心得二:把随机种子当成超参数来管。同样的流程用三个不同种子跑,如果 F1 的波动超过 0.01,说明你的结论不可靠,多试几次再下判断。我一般的做法是:快速实验用单次 5 折,候选方案用 3 个种子各跑一次取平均,最终方案用 5 个种子的 RepeatedStratifiedKFold。

心得三:验证集一定要留一份"绝对不碰"的。交叉验证用训练集内部划分,除此之外再从训练集里切出 10% 作为 holdout,全程只在最后确认模型的时候用一次。这份数据被污染了,你就失去了判断模型是否过拟合到 CV 的唯一标尺。

心得四:过采样之后概率不再可信。SMOTE 改变了先验分布,模型输出的概率是偏向少数类的,不能直接当置信度用。如果要输出概率,得做先验校正,公式是p_calibrated = p / (p + (1-p) * (prior_orig / prior_sampled))。评委如果看的是概率排序,这一步不影响结果;但如果看的是阈值为 0.5 的分类结果,那必须做阈值移动。

心得五:记录每次失败的实验。成功的原因往往相似,失败的原因千奇百怪。我有一份"否决清单",记着"某类特征加上以后 CV 涨了但 holdout 跌了,判定为过拟合"、"某组参数在 5 折上很好但在 10 折上崩了,判定为不稳定"。这些东西比成功记录更有价值,能让你少走很多回头路。

我个人在这类赛题上的体会是,缺失值、过采样、baseline 这三件事,代码层面的工作量其实很小,加起来不到两百行,真正花时间的是每一次决策背后的判断——这个字段的缺失值不值得保留标记、这个不平衡比该不该采样、这次涨分是真的还是噪声。把这几个判断做扎实了,后面加特征、调参、集成才有稳固的地基可踩;反过来,如果这三步里埋了一处泄漏,后面所有的努力都是在错误的分数上做优化。下一篇我会接着聊特征工程那部分,尤其是时间窗聚合和类别编码的具体做法,包括我踩过的一个目标编码泄漏的坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询