
简介面向大学生竞赛与课程设计的二手车价格预测优胜方案源码资料包源自阿里天池与Datawhale联合举办的赛事包含获奖方案的完整项目说明。压缩包共十五个文件以Python脚本为主体辅以md说明文档、txt依赖清单与少量备份文件整体仅约610KB体量轻巧当前已有42人浏览学习。资料完整覆盖数据预处理、特征工程、模型训练与集成融合等竞赛核心环节包含基于梯度提升树、神经网络及多模型加权融合等不同思路的建模代码并附预测结果数据表能够帮助使用者系统理解获奖方案的构建脉络并快速复现。适合计算机科学、应用数学、电子信息工程等相关专业的学生或科研人员作为课程设计、毕业设计或学术竞赛的参考范本其思路亦可迁移至房价、销量等同类回归预测问题。1. 二手车价格预测竞赛不是比模型是比谁更懂二手车的定价逻辑同一台车有人估 9 万有人报 11 万差价背后不是玄学而是对车龄、里程和品牌残值率的理解深度。某竞赛平台与某开源学习社区联合举办的二手车价格预测赛题要求根据车辆的多维信息预测成交价评估指标直接对标行业里的定价偏差。这类赛题的优胜方案通常不是最深的神经网络而是把特征工程、交叉验证和多模型融合做到极致的一套树模型方案。这篇笔记会从数据认知、特征构造、模型训练讲到源码组织完整拆解一套可复现的建模流程适合想入门结构化数据竞赛的开发者也适合正在做二手车估值系统的工程师。我们先从赛题数据本身开始。2. 赛题拆解与数据认知价格为什么是长尾分布以及我们要预测什么不少参赛者拿到数据后第一件事就是跑一个 LightGBM但如果不先理解价格分布和评估指标后续所有调参都是在黑匣子里瞎试。二手车价格预测本质上是一个回归问题但它的特殊之处在于目标变量跨越一万元到上百万元误差度量方式直接决定了你该优化什么。2.1 任务定义与评估指标你优化的不是误差而是对数误差官方在赛题说明里通常会把评估指标定义成 RMSLERoot Mean Squared Logarithmic Error也就是对真实价格和预测价格分别取对数后计算 RMSE。为什么要这样设计因为一辆 3 万的车少估 3 千和一辆 30 万的车少估 3 千在绝对误差上是相同的但在买卖双方的心理感知上完全不同。取对数后3 万车少估 3 千对应 log 误差约 0.130 万车少估 3 千对应 log 误差约 0.01这样就把“相对误差”作为惩罚核心更贴近二手车定价业务。所以在训练之前我一般会先写一个 RMSLE 计算函数用作所有模型验证的统一标尺。import numpy as np def rmsle(y_true, y_pred): # 将真实值和预测值都限制在非负范围避免对负数取对数 y_true np.maximum(y_true, 0) y_pred np.maximum(y_pred, 0) # 用 log1p 计算对数误差的均方根等价于对价格取 log 后的 RMSE return np.sqrt(np.mean((np.log1p(y_true) - np.log1p(y_pred)) ** 2))这段代码里有三个关键点np.maximum 是为了防止极端预测值出现负价格而做兜底np.log1p 是 log(x1)在价格接近 0 时比 log(x) 更稳定整个函数返回的是一个标量分数分数越低代表相对误差越小。后续每一次交叉验证、特征筛选、融合权重调整我都用这个函数来打分不额外看别的指标。明确了优化目标之后还需要知道训练数据长什么样。这里要提醒一下RMSLE 和 RMSE 的数值有很大差异不要拿 RMSE 的标准去评判 RMSLE。2.2 探索性数据分析先看价格分布、缺失值和异常值再谈建模EDA 看起来是体力活但往往决定了后续特征工程的走向。我用 pandas 加载训练集后第一件事是看 shape、字段类型和描述统计重点关注 price、mileage、reg_date 这三个字段的分布和缺失比例。常见的数据集里品牌、车系、上牌日期、行驶里程、排量、变速箱、燃料类型、过户次数、是否事故车等字段都会出现其中上牌日期和过户次数的缺失率往往不低这部分要先记录下来。价格分布是典型的右偏分布少量豪华品牌的高价车占据长尾如果直接用原始价格做回归模型会被那些几十万的车主导忽略了几万元区间的主流样本。所以我会同时画原始价格和 log 价格两个直方图确认取对数后的分布是否接近正态。import pandas as pd import matplotlib.pyplot as plt train pd.read_csv(train.csv) print(train.shape) print(train.describe(percentiles[0.01, 0.25, 0.5, 0.75, 0.99])) plt.hist(train[price], bins100) plt.yscale(log) plt.show() train[log_price] np.log1p(train[price]) plt.hist(train[log_price], bins100) plt.show() missing train.isnull().sum() print(missing[missing 0])上面这段代码里train.describe 的 percentiles 参数会额外给出 1% 和 99% 分位数方便快速发现极端价格plt.yscale(log) 让长尾更加直观最后打印的 missing 列表是后续填充策略的依据。如果看到 price 的最小值是 0 或者接近 0大概率是样本异常或者数据采集错误需要在特征工程阶段处理。一个常见的数据集字段说明可以整理成下面这样方便后续写代码时对照。字段名类型业务含义预处理关注点brand_idint品牌编码是否需要做目标编码reg_datedatetime上牌日期计算车龄mileagefloat行驶里程万公里缺失值、异常值powerint最大功率马力缺失填充gearboxstr变速箱类型转类别编码fuel_typestr燃料类型转类别编码transfer_countfloat过户次数缺失用 unknownpricefloat成交价万元仅训练集有需取对数从这张表可以直观看出真正需要手工处理的字段并没有多少但这不代表建模简单。price 和 mileage 之间是明显的非线性关系和 reg_date 之间存在车龄的单调衰减这些关系都靠特征工程去表达。2.3 从原始字段到有效特征车龄、里程、品牌残值率的构造逻辑原始字段很少能直接进模型。日期字段必须转成车龄里程数要考虑单位是否统一品牌这种高基数类别特征需要找到一种既能保留信息又不过拟合的编码方式。车龄的构造很简单用数据采集年份或者销售年份减去上牌年份。为什么不用上牌日期本身因为树模型无法理解“2017 年”这个数字的大小与价格衰减的关系但车龄“5 年”是一个连续变量模型可以学出“前 3 年贬值最快、5 年后贬值放缓”这种分段关系。里程也是一样单纯用“里程×车龄”不如“年均里程”更能反映车辆的使用强度。品牌残值率是这类赛题最重要的特征。不同品牌的三年保值率可能从 45% 到 75% 不等这种差异比单一品牌 ID 更有解释力。常见做法是统计每个品牌在训练集中的平均成交价作为该品牌的价格基准。import pandas as pd import numpy as np train[reg_year] pd.to_datetime(train[reg_date]).dt.year train[reg_month] pd.to_datetime(train[reg_date]).dt.month train[car_age] train[sale_year] - train[reg_year] train[mileage] train[mileage].astype(float) train[mileage_per_year] train[mileage] / (train[car_age] 1) brand_mean_price train.groupby(brand_id)[price].transform(mean) train[brand_price_mean] brand_mean_pricecar_age 单位是年sale_year 一般来自数据集提供的销售年份字段如果没有可以考虑用数据发布时间。mileage_per_year 的计算里加了一个 1是为了避免新车车龄为 0 时出现除零错误同时也平滑了分母。brand_price_mean 用 transform 一次性映射到每一行方便后续直接作为特征。这里要注意brand_price_mean 是用全量训练集统计出来的如果直接用于交叉验证会出现标签泄漏某条样本的预测值里包含了它自己所在品牌的平均价格信息。正确做法是在每一折内重新统计或者使用后面会讲到的目标编码。我在这里先展示特征构造的思路后面章节会给出防泄漏的实现。数据认知到此为止接下来进入特征工程和清洗环节。这个环节决定了你最终是站在排行榜中游还是上游。3. 特征工程与数据清洗决定名次的是特征不是调参特征工程是最能拉开排名差距的部分而它也是最容易被新手忽略的部分。很多参赛者拿着同一个 LightGBM 参数跑完就去看排名却不知道别人在特征上多花了两倍时间。3.1 缺失值填充与异常价格处理避免把事故车当正常车缺失值处理常常被一句 fillna(0) 带过但二手车数据里的缺失往往带有业务含义。比如过户次数缺失可能是车商没有录入也可能是数据源本身没有这项记录如果直接用 0 填充模型会学到“缺失从未过户”这种错误信号。我习惯对类别型缺失单独标记一个 unknown 类别让树模型自己去判断缺失与目标的关系。数值型字段的缺失要分情况排量、功率这类连续值往往和车型定位相关用中位数填充比均值更稳因为中位数不受极端值影响。如果字段缺失率超过 40%我倾向于直接剔除而不是强行填充因为填充一个半数以上都是缺失的字段等于是在制造噪声。价格异常值是另一个坑。有些样本成交价低到几百元对应的却是正常描述的车况这大概率是数据采集错误或者特殊交易情形。如果不处理模型会为了拟合这些离群点而扭曲整个分裂逻辑。同时还要留意价格和里程的联合异常比如里程只有 1 万公里但价格只有同款车的 20%可能是事故车如果数据集里带有事故车标记可以直接利用。train[transfer_count] train[transfer_count].fillna(unknown) train[displacement] train[displacement].fillna(train[displacement].median()) train train[(train[price] 0.5) (train[price] 100)] train train[~((train[mileage] 2) (train[price] train[price].quantile(0.1)))]第一行把过户次数缺失变成独立的 unknown 类别第二行用中位数填充排量避免均值被大排量车型拉高。后面的过滤条件我是按价格在 0.5 万到 100 万之间来筛具体阈值根据赛题的价格分位数调整最后一行过滤掉“低里程但极低价”的矛盾样本这类样本十有八九是异常数据。过滤比例如果超过训练集的 5%就要回头检查是否误杀了真实的特价车。3.2 类别特征编码与组合特征品牌、车系、变速箱如何进入树模型类别特征的编码方式直接影响树模型的表达能力。简单高基数类别比如车系如果直接用 LabelEncoder等于给车系赋予了一组任意整数编号树模型会误以为编号之间有大小关系。LightGBM 支持直读类别特征但需要在 Dataset 中指定 feature_name 和 categorical_feature 参数否则也会自动按数值处理。对于高基数类别我更推荐做目标编码用该类别下的价格均值作为特征。这样既编码了类别本身又编码了该类别的价格水平。但目标编码必须防泄漏不能直接在全体训练集上计算均值否则验证分数会虚高。from sklearn.model_selection import KFold def target_encoding(df, col, target, n_splits5, seed42): kf KFold(n_splitsn_splits, shuffleTrue, random_stateseed) df[te_ col] np.nan for tr_idx, va_idx in kf.split(df): tr df.iloc[tr_idx] va df.iloc[va_idx] mean_map tr.groupby(col)[target].mean() df.loc[df.index[va_idx], te_ col] va[col].map(mean_map) # 测试集中出现但训练集中没出现过的类别用全局均值兜底 df[te_ col] df[te_ col].fillna(df[target].mean()) return df这个函数的逻辑是把数据分成 5 折对每一折都用其余 4 折的类别均值去填当前折的对应取值。这样每一行的目标编码都只依赖不在这行本身的历史信息从机制上避免了泄漏。n_splits 用 5 还是 10 取决于数据量数据量在几万条以上时 5 折足够折数越多偏差越小但计算越慢。seed 固定后每次运行结果可复现。除了编码组合特征也有很大作用。我一般会构造品牌和车龄的交互、品牌和里程的交互以及排量和变速箱的组合。品牌残值率明显不同而同一品牌下5 年 10 万公里的车和 5 年 5 万公里的车价格差可能超过 15%这种差异只有通过交互特征才能被树模型捕捉到。train[brand_age] train[brand_id].astype(str) _ train[car_age].astype(str) train[brand_mileage_clip] train[brand_id].astype(str) _ pd.cut(train[mileage], 10).astype(str)brand_age 是一个字符串拼接得到的组合类别用来表达“某品牌在某车龄区间”的交叉效应brand_mileage_clip 把里程分成 10 档后再和品牌拼接相当于把里程分箱和品牌做了交互。这种手工组合特征在树模型里往往有奇效因为它们比树模型自己找的 split 更加贴近业务逻辑。3.3 价格对数变换与标签编码让回归目标更接近正态分布标签变换是最后一个关键动作。前面看到价格是长尾分布直接把原始价格作为目标模型在迭代中的梯度会被大价格样本主宰导致小价格样本拟合不足。取对数后目标分布变得对称梯度更均衡。在实现上我会把标签转换和特征编码放在同一个流水线里from sklearn.preprocessing import LabelEncoder le LabelEncoder() for col in [gearbox, fuel_type, color]: train[col] le.fit_transform(train[col].astype(str)) train[log_price] np.log1p(train[price]) feature_cols [car_age, mileage, power, displacement, gearbox, fuel_type, transfer_count, brand_price_mean, mileage_per_year, te_brand_id, te_model_id, brand_age] X train[feature_cols] y train[log_price]LabelEncoder 会把字符串类别转成从 0 开始的整数对树模型足够用。注意要保证训练集和测试集使用同一个 LabelEncoder 对象最好在训练完成后把训练数据中的类别到整数的映射保存下来推理时复用否则类别集合不同会导致编码错位。log_price 是后续所有模型训练的标签模型预测出来的是 log 空间的数值最终提交前要执行 np.expm1 还原成价格。特征工程做完后不要急着训模型先检查一下特征之间是否有多重共线性或泄漏。比如 brand_price_mean 和 te_brand_id 都表达了品牌价格水平两者高度相关树模型虽然不怕共线性但冗余特征会让重要性评估变得混乱。我的习惯是先跑一个 LightGBM 看特征重要性把重要性很低且确实业务重复的特征删掉这样后续模型收敛更快。现在进入模型训练和验证阶段。这一章会告诉你为什么选 LightGBM、交叉验证怎么做才不失真、融合到底该加权还是 Stacking。4. 模型训练与交叉验证从单模型到融合优胜方案是这样炼成的特征做到位之后模型选择变得相对简单。二手车价格预测这类表格任务树模型是当之无愧的主力而树模型里我优先选 LightGBM。4.1 基线模型选择为什么优先选 LightGBM 而不是线性回归线性回归的问题在于它假设特征和目标之间是线性关系。二手车价格和里程的关系显然不是线性的前 5 万公里折旧快过了这个区间折旧放缓。要让线性模型 work你需要手工构造分箱、交互、多项式特征这个成本非常高。LightGBM 通过梯度提升和直方图分裂自动学习分段常数函数正好匹配这种非线性关系。另一个理由是工程效率。LightGBM 的内存占用低训练速度快自带处理缺失值的逻辑还支持类别特征直读。在同样的特征集上LightGBM 的基线 RMSLE 通常比线性回归低 20% 到 40%这个差距不是靠调参能追回来的。我一般会先把下面这组参数作为起点而不是一上来就去搜索超参数。params { objective: regression, metric: rmse, learning_rate: 0.05, num_leaves: 31, max_depth: -1, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 1, lambda_l2: 1.0, verbose: -1, seed: 42, }learning_rate 设 0.05 是比较稳的起点如果想追求精度可以降到 0.02但需要增大迭代次数。num_leaves 是主要复杂度控制参数31 对应 2^5-1适合中低维特征如果特征很多可以适当加大到 63 或者 127但要注意过拟合。feature_fraction 和 bagging_fraction 都设 0.8相当于每棵树只用 80% 的特征和 80% 的样本是两种并行正则化。lambda_l2 加一点 L2 正则让权重不要走极端。这些参数不是固定的但作为基线已经能跑出不错的分数。4.2 K 折交叉验证与早停让验证分数真实反映线上表现交叉验证是评估模型的唯一可信标尺。单次留出验证的波动太大随机选一批数据当验证集运气好就高运气差就低。K 折交叉验证把训练集切成 K 份每份轮流当验证最后取 K 次验证分数的平均这样能减少评估方差。在二手车价格赛题里数据通常不是严格的时间序列随机 K 折是常见做法。但如果数据里有明显的采集日期字段而且价格随时间有趋势性变化就要改成按时间切分用较早的数据训练较晚的数据验证否则模型会偷看未来信息。import lightgbm as lgb from sklearn.model_selection import KFold from sklearn.metrics import mean_squared_error kf KFold(n_splits5, shuffleTrue, random_state42) oof np.zeros(len(X)) for fold, (tr_idx, va_idx) in enumerate(kf.split(X)): tr_X, tr_y X.iloc[tr_idx], y.iloc[tr_idx] va_X, va_y X.iloc[va_idx], y.iloc[va_idx] dtr lgb.Dataset(tr_X, labeltr_y) dva lgb.Dataset(va_X, labelva_y) model lgb.train( params, dtr, num_boost_round5000, valid_sets[dva], callbacks[lgb.early_stopping(100), lgb.log_evaluation(0)] ) oof[va_idx] model.predict(va_X, num_iterationmodel.best_iteration) score np.sqrt(mean_squared_error(y, oof)) print(fCV RMSLE: {score:.5f})这段代码有几个细节值得注意。dtr 和 dva 分属训练部分和验证部分early_stopping 在验证集上连续 100 轮没有提升时停止训练避免过拟合的同时不需要手动指定迭代轮数。predict 时加上 num_iterationmodel.best_iteration确保使用的是早停时的最佳模型而不是最后一次迭代的模型。每折训练完都把对验证集的预测存进 oof最后统一计算 RMSLE这个分数就是模型在随机 K 折下的真实估计。如果 CV 分数和线上分数差距很大优先检查是不是交叉验证切分方式与赛题数据的生成方式不一致。比如赛题数据本身是分多个批次的或者价格标签有时间聚集效应这时候随机 K 折的 CV 分数会虚高。4.3 多模型融合策略加权平均与 Stacking 的取舍单模型跑出合适的 CV 分数后融合是往上走的常见手段。融合的基础是多样性不同模型看到的数据视角不同它们的错误模式不同加权平均后误差会互相抵消。常见的组合是 LightGBM、XGBoost、CatBoost也可以加入岭回归模型专门拟合线性部分。简单加权平均是最稳的融合方式实现成本低不容易过拟合。权重不一定要精确搜索我会先看每个模型单独的 CV 分数分数越好的给越大权重。一般权重控制在 0.5、0.3、0.2 这种粗糙的比例即可过度调权重反而是在拟合验证集。pred_lgb model_lgb.predict(test_X, num_iterationmodel_lgb.best_iteration) pred_xgb model_xgb.predict(test_X) pred_cat model_cat.predict(test_X) final_pred 0.5 * pred_lgb 0.3 * pred_xgb 0.2 * pred_cat final_price np.expm1(final_pred)这里三个模型都输出 log 价格直接加权最后 expm1 还原。如果你手头只有两个模型权重可以改成 0.6/0.4。要注意的是融合前一定要确认所有模型用的是同一份测试集特征并且在相同的 log 空间里输出否则加权没有意义。Stacking 比加权平均更强但风险也更大。做法是把每折 OOF 预测作为第二层的特征然后用一个简单模型比如 Ridge去拟合真实标签。Stacking 的问题在于第二层很容易过拟合尤其是训练数据量不足一万条时融合收益会被过拟合吃掉。我的经验是如果 OOF 分数提升不超过 0.001就优先用加权平均只有数据量超过 5 万条或者单模型差异足够大时才值得上 Stacking。下面是一个在实践中被反复验证过的结论把交叉验证做扎实比调 100 组超参数都有效。CV 分数和线上分数的相关性是你判断一切特征和模型改动是否有效的唯一标准。只要 CV 和线上相关性好你的每次改进才是真的改进否则你只是在对着排行榜碰运气。5. 二手车价格预测的 5 个常见坑翻车现场与后悔药前面讲的都是“正确做法”但真正让你排位靠后的往往是那些看起来无害的处理习惯。这 5 个坑我都在复现这类赛题时踩过或者见别人踩过每一个都配了后悔药。5.1 标签泄漏把“新车指导价”直接当特征现象交叉验证分数极低单模型 RMSLE 能到 0.30 以内自己都觉得要拿奖了结果一提交线上分数比 CV 高出一大截排名还不如随机猜测。原因数据里通常会有“新车指导价”或者“厂商建议价”这类字段。它确实是很有用的价格锚点但它和目标成交价高度相关甚至可以说指导价已经包含了价格信息。当你把指导价原封不动作为特征时模型学到的是“成交价≈指导价×0.8”之类的映射可测试集里同样有这个字段所以训练时看似正确但实际上模型并没有学习到车况、里程这些真正的定价因素。一旦线上数据的指导价和成交价关系发生漂移立刻翻车。解决不要直接用指导价把它变成“贬值率”或“价格折扣”再作为特征贬值率成交价/指导价这样目标就和指导价解耦了。如果这个字段确实要保留也要在交叉验证时做敏感性分析看看去掉它对 CV 分数的影响。还有一种更保险的做法凡是和目标强相关且业务上属于“结果”的字段全部剔除。在复现阶段我还会单独算一下指导价和成交价的斯皮尔曼相关系数如果超过 0.9就提醒自己一定要做变换。5.2 缺失值填充太粗暴用 0 填充类别特征现象模型不报错特征重要性排名很高CV 分数却上不去而且在某些细分品牌上的预测严重偏离。原因0 在类别编码里成为了一个新类别但缺失并不等于“无”或“手动挡”这类含义。以变速箱字段为例如果缺失的车辆其实是自动挡你填 0 后模型可能把“0”分裂成一个单独分支专门预测一个奇怪的中间价格。树模型虽然能处理缺失但那是基于模型自己的分裂决策而不是你预先填充的武断值。解决类别字段的缺失统一用一个单独字符串 missing 填充然后再编码数值字段用中位数而不是 0 填充如果是布尔型字段比如“是否有事故”缺失就填 unknown。这样做的核心思想是让模型自己判断缺失模式与目标的关系而不是强加一个业务含义。如果数据量大我还会跑两个版本做对比一个用 0 填充一个用 missing 填充看 CV 分数的差异是否明显用数据说话而不是拍脑袋。5.3 交叉验证与线上不一致随机 K 折遇上时间漂移现象随机 5 折 CV 分数稳定在 0.25线上分数却是 0.36而且每次提交都稳定差 0.1 左右。原因赛题数据如果是按时间分批收集的汽车价格会受市场行情、新车降价、政策变化等多种因素影响。随机 K 折把所有样本混在一起等于让模型用后几个月的数据去“训练”前几个月的样本或者反过来说验证集里的样本在训练集里找到了来自未来的同类信息CV 分数自然虚高。线上评估时用的是严格按时间划分的新数据这种信息被切断分数就崩了。解决先确认数据里有没有时间戳比如上牌日期、采集日期。如果有按时间排序后对样本编号取前 80% 做训练、后 20% 做验证。如果你用的是随机 K 折至少保证 KFold 的 shuffleTrue并且固定 seed方便排查。还有一个验证方法是把数据按月份分组画一张“每个月平均成交价”的曲线看看是否存在明显趋势有趋势就按时间切分没有趋势才可以用随机 K 折。这个检查 10 分钟就能做完却能避免后面几天的无效工作。5.4 对数变换的陷阱预测值取 exp 后系统性偏低现象RMSLE 看起来合格但把预测价格从 log 空间还原后发现平均预测值比真实平均值低 5% 到 10%整体偏移。原因在 log 空间用 MSE 做优化等价于拟合目标的对数几何均值。当你把对数均值变换回原始空间时得到的是几何均值而真实数据的均值是算术均值两者天然存在差距。尤其当价格分布右偏时这个差距会更明显。LightGBM 的 objectiveregression 直接作用于 log 目标所以预测的 log 均值天然是偏低的。解决常见做法是做一个偏差校正系数。先用训练集每个测试样本的预测值乘以一个系数 cc 真实价格均值 / 预测价格均值这个系数用 OOF 预测计算得到然后套在测试集预测上。也可以尝试在训练时把目标换成原始价格配合 huber loss 等对异常值鲁棒的损失函数看 CV 分数的变化。我个人习惯用 OOF 校正系数简单有效。具体实现时我会把 OOF 预测的 exp 值和真实价格放在同一张表里算一个加权平均系数再把这个系数固化到 predict 脚本里避免每次提交时手动改。5.5 融合时权重拍脑袋没有考虑相关性现象两个单模型 CV 分数都是 0.26融合之后 CV 分数反而变成 0.27还不如取最好的单模型。原因融合的前提是模型之间具有多样性。如果你把同一个 LightGBM 用不同的随机种子跑了两次这两个模型的预测高度相关错误模式几乎一样加权平均不仅不会纠错还可能把某些正确预测平均偏。如果再加一个 CatBoost但它和 LightGBM 在特征使用上太接近融合收益也有限。解决在融合前先计算各模型 OOF 预测之间的皮尔逊相关系数。只保留相关系数低于 0.95 的模型进入融合如果两个模型相关系数超过 0.97说明它们几乎等价留一个就行。权重不要纯靠感觉可以用一个简单的网格搜索在验证集上遍历权重组合选出 CV 分数最低的组合但要在多折上确认稳定性防止过拟合验证集。我一般把权重搜索步长设为 0.1组合数量控制在 100 以内跑完后的最优权重如果和启发式权重差距很大反而要警惕是不是验证集太小。6. 把项目说明写成别人能复现的东西源码结构、运行顺序与你的第一个改进点一个能拿奖的方案源码质量和项目说明同样重要。很多人跑完模型就丢在一堆 Untitled.ipynb 里等到想复现的时候发现忘了哪步在前哪步在后。我习惯把代码拆成几个独立模块每个模块只干一件事然后通过 README 把整条流水线串起来。car_price_prediction/ ├── data/ │ ├── train.csv │ └── test.csv ├── src/ │ ├── preprocess.py │ ├── features.py │ ├── train.py │ └── predict.py ├── notebooks/ │ └── EDA.ipynb ├── README.md └── requirements.txtpreprocess.py 负责加载数据、缺失值填充、异常值过滤features.py 负责构造车龄、目标编码、组合特征并输出特征矩阵train.py 负责 K 折训练和保存模型每个模型存成 pkl 或 txtpredict.py 读取模型拼接测试集特征输出提交格式。运行顺序就是 preprocess 到 features 到 train 再到 predict每一步都从上一部的输出读数据避免重复执行。README 里我会写清楚四件事赛题任务和评估指标、依赖库版本说明、一条命令从头训练到预测的执行方式、以及复现时需要注意的坑比如目标编码必须用训练集的映射。不需要写成论文只要让另一个工程师照着做能在半小时内跑出和赛题排名对应的分数就算合格。如果你想在这个基础上做第一个改进我建议从验证策略开始把随机 K 折改成按时间切分或者加入 GroupKFold 按品牌分组往往比调模型参数更有效。我在最初做这类赛题时总把时间花在调参上后来发现把验证策略做扎实比调 100 组参数都管用。希望帮到你。本文还有配套的精品资源点击获取