资讯动态

基于多模型融合的二手车交易市场大数据挖掘实战

发布时间:2026/10/9 18:54:28 来源:尧图企业网站定制
简介本资源为基于机器学习和多模型融合的二手车交易市场大数据挖掘项目源码包面向计算机、人工智能、大数据、数学及电子信息等专业的学生与技术学习者可用于课程设计、期末大作业或毕业设计参考。项目围绕二手车交易数据展开涵盖数据缺失值预测、交易价格预测与成交周期挖掘等典型任务并采用多模型融合思路提升预测效果。压缩包共24个文件约16.88MB包含Python脚本与Jupyter Notebook实现核心算法流程pkl文件保存训练好的模型xml与iml等为项目配置png图片辅助结果展示另有readme说明文档与结果文本。已有98人学习下载。读者可获得完整可运行的源码、模型文件与项目说明理解从数据预处理、特征转换到多模型训练与融合的完整链路适合具备一定基础的学习者调试与二次开发。1. 二手车定价的玄学到底能不能用机器学习拆开看二手车交易市场里最不缺的就是“拍脑袋定价”。同一台车车商报 8 万 2平台估价 7 万 6个人卖家挂 8 万 8最后成交价可能落在 7 万 9。这中间的差价不是谁在坑谁而是信息维度不一样车商看的是收车成本加周转周期平台看的是历史成交分布个人卖家看的是“我这车保养得好”。基于机器学习和多模型融合的二手车交易市场大数据挖掘要解决的就是把这套玄学拆成可复现的特征工程加模型融合流程。它适合两类人一类是想从零搭一套估价系统的算法工程师另一类是被“同车不同价”搞烦了、想自己跑数据看看到底哪些因素在起作用的开发者。这篇笔记不讲虚的从数据清洗到多模型融合的堆叠结构每一步都落到能跑的代码和能调的参数上。2. 数据进模型之前二手车特征工程的四个关键决策2.1 为什么“上牌年份”不能直接当数值特征喂进去二手车数据里最容易被新手直接丢进模型的字段就是“上牌年份”。表面上看 2018 比 2015 新数值大模型应该能学到“年份越大越值钱”。但实际数据里2018 年的车可能因为车型换代、排放标准切换、或者当年新车降价残值曲线并不是单调的。我一般会把上牌年份转成“车龄”再按车龄分桶而不是让模型去拟合一个线性关系。import pandas as pd import numpy as np # 假设原始数据里有 reg_year 和 reg_month df pd.read_csv(used_car_raw.csv) # 用当前参考日期计算车龄月避免直接用年份 ref_date pd.Timestamp(2025-01-01) df[reg_date] pd.to_datetime( df[reg_year].astype(str) - df[reg_month].astype(str) -01 ) df[car_age_months] (ref_date - df[reg_date]).dt.days // 30 # 车龄分桶0-12、13-36、37-60、60 bins [0, 12, 36, 60, np.inf] labels [1年内, 1-3年, 3-5年, 5年以上] df[age_bucket] pd.cut(df[car_age_months], binsbins, labelslabels) # 检查分布避免某个桶样本过少 print(df[age_bucket].value_counts(normalizeTrue))这段代码的逻辑是先把年月拼成完整日期再算到参考日期的月数差。参数上ref_date选当前日期或数据集里最近成交日期都行但一旦选定训练集和测试集必须用同一个参考日期否则车龄会漂移。分桶的边界不是拍脑袋1 年内对应准新车1-3 年是贬值最快区间3-5 年进入稳定期5 年以上残值主要看车况。如果某个桶占比低于 5%说明样本不够要么合并桶要么在模型里给这个桶加权重。2.2 品牌和车型的编码目标编码比独热更稳品牌、车系、车型这三层 categorical 特征独热编码在品牌层面还能忍到车系和车型层面直接维度爆炸。我一般用目标编码target encoding但必须配合折内均值否则标签泄漏会让线下 AUC 虚高到 0.95上线直接翻车。from sklearn.model_selection import KFold def target_encode(train_df, test_df, col, target, n_splits5, smooth10): 折内目标编码避免标签泄漏 smooth: 平滑系数防止小样本类别编码过拟合 train_df train_df.copy() test_df test_df.copy() global_mean train_df[target].mean() kf KFold(n_splitsn_splits, shuffleTrue, random_state42) train_df[f{col}_te] np.nan for tr_idx, val_idx in kf.split(train_df): tr_fold train_df.iloc[tr_idx] # 计算折内均值加平滑 agg tr_fold.groupby(col)[target].agg([mean, count]) agg[smooth_mean] ( (agg[mean] * agg[count] global_mean * smooth) / (agg[count] smooth) ) mapping agg[smooth_mean].to_dict() train_df.loc[train_df.index[val_idx], f{col}_te] ( train_df.iloc[val_idx][col].map(mapping).fillna(global_mean) ) # 测试集用全量训练集的编码 agg_full train_df.groupby(col)[target].agg([mean, count]) agg_full[smooth_mean] ( (agg_full[mean] * agg_full[count] global_mean * smooth) / (agg_full[count] smooth) ) test_df[f{col}_te] test_df[col].map(agg_full[smooth_mean]).fillna(global_mean) return train_df, test_df参数smooth控制平滑强度取值 10 到 50 之间比较常见。样本量大的类别平滑影响小样本量只有几十的冷门车型平滑会把编码拉向全局均值防止模型记住噪声。折数n_splits一般 5 折数据量小于 1 万条时用 10 折更稳。注意测试集的编码必须用全量训练集重新算一遍不能复用折内映射否则线上线下分布不一致。2.3 里程和车况的交互特征别让模型自己猜里程和车况评分单独看都有信息但真正影响价格的是“高里程加低车况”这种组合。我一般会显式构造几个交互特征里程除以车龄得到年均里程车况评分乘以品牌保值率系数事故次数加维修记录数得到“风险分”。# 年均里程直接反映使用强度 df[mileage_per_year] df[mileage] / (df[car_age_months] / 12 1) # 风险分事故和维修的加权和 df[risk_score] df[accident_count] * 2 df[repair_count] * 1 # 保值率系数用品牌目标编码值除以全局均值 brand_te_mean df.groupby(brand)[price].transform(mean) df[brand_retention] brand_te_mean / df[price].mean() # 交互项车况评分与保值率的乘积 df[condition_x_retention] df[condition_score] * df[brand_retention]年均里程这个特征在业务上很好解释同样 5 年车龄年均 1 万公里和年均 3 万公里残值差一大截。风险分把事故和维修合并成一个维度减少特征数量。保值率系数用品牌均值除以全局均值数值在 0.8 到 1.2 之间超过 1.2 说明这个品牌整体溢价高。交互项让线性模型也能捕捉到“好车况加高保值品牌”的叠加效应。2.4 缺失值处理车况评分缺失不代表车况差二手车数据里车况评分缺失很常见可能是卖家没填也可能是检测报告没覆盖。直接填 0 或者均值都会引入偏差。我的做法是加一个缺失指示列再用同车型同车龄的中位数填充。# 缺失指示列 df[condition_missing] df[condition_score].isna().astype(int) # 按车型和车龄分组填充中位数 df[condition_score] df.groupby([model, age_bucket])[condition_score].transform( lambda x: x.fillna(x.median()) ) # 如果整组都是缺失用全局中位数兜底 df[condition_score] df[condition_score].fillna(df[condition_score].median())缺失指示列让模型知道这个样本的车况是推断出来的不是实测的。分组填充比全局填充更合理因为不同车型的车况分布不一样。兜底逻辑必须有否则遇到新车型整组缺失会直接报错。3. 多模型融合从单模型调参到堆叠集成的落地路径3.1 基模型选型为什么是 LightGBM、XGBoost 和 CatBoost 三件套二手车价格预测是典型的表格数据回归任务树模型在这个领域依然是主力。LightGBM 训练快、内存占用低适合做第一层基模型XGBoost 正则化强对异常价格更鲁棒CatBoost 原生支持 categorical 特征省去目标编码的步骤而且对类别特征的处理有自己的算法。三个模型一起上不是为了堆数量而是它们的偏差方向不一样LightGBM 偏向拟合高频价格区间XGBoost 对高价车更敏感CatBoost 在品牌车型组合上表现更稳。import lightgbm as lgb import xgboost as xgb from catboost import CatBoostRegressor from sklearn.metrics import mean_absolute_error # 假设 X_train, y_train, X_val, y_val 已经准备好 # LightGBM lgb_model lgb.LGBMRegressor( n_estimators2000, learning_rate0.03, num_leaves63, max_depth8, min_child_samples20, subsample0.8, colsample_bytree0.8, reg_alpha0.1, reg_lambda0.1, random_state42 ) lgb_model.fit( X_train, y_train, eval_set[(X_val, y_val)], eval_metricmae, callbacks[lgb.early_stopping(100), lgb.log_evaluation(200)] ) # XGBoost xgb_model xgb.XGBRegressor( n_estimators2000, learning_rate0.03, max_depth7, min_child_weight5, subsample0.8, colsample_bytree0.8, reg_alpha0.1, reg_lambda1.0, random_state42 ) xgb_model.fit( X_train, y_train, eval_set[(X_val, y_val)], verbose200 ) # CatBoost cat_model CatBoostRegressor( iterations2000, learning_rate0.03, depth8, l2_leaf_reg3.0, random_seed42, verbose200 ) cat_model.fit( X_train, y_train, eval_set(X_val, y_val), cat_features[brand, model, age_bucket] )LightGBM 的num_leaves控制在 63 以内再大容易过拟合min_child_samples设 20 是防止叶子节点样本太少。XGBoost 的min_child_weight设 5比默认的 1 更保守。CatBoost 的cat_features直接传类别列名不需要提前编码。三个模型都用早停early_stopping的轮数设 100意味着验证集 MAE 连续 100 轮不下降就停。3.2 堆叠融合的代码实现用 OOF 预测喂给元模型多模型融合最稳的方式是堆叠stacking但必须用 out-of-fold 预测否则元模型会过拟合。我一般用 5 折每折训练三个基模型把验证集的预测拼起来作为元模型的特征。from sklearn.model_selection import KFold from sklearn.linear_model import Ridge def get_oof_predictions(models, X, y, X_test, n_splits5): 生成 OOF 预测和测试集预测 models: 基模型列表 kf KFold(n_splitsn_splits, shuffleTrue, random_state42) oof_train np.zeros((X.shape[0], len(models))) oof_test np.zeros((X_test.shape[0], len(models))) for i, model in enumerate(models): test_preds np.zeros((X_test.shape[0], n_splits)) for fold, (tr_idx, val_idx) in enumerate(kf.split(X)): X_tr, X_val X.iloc[tr_idx], X.iloc[val_idx] y_tr, y_val y.iloc[tr_idx], y.iloc[val_idx] model_clone clone(model) model_clone.fit(X_tr, y_tr) oof_train[val_idx, i] model_clone.predict(X_val) test_preds[:, fold] model_clone.predict(X_test) oof_test[:, i] test_preds.mean(axis1) return oof_train, oof_test # 基模型列表 base_models [lgb_model, xgb_model, cat_model] oof_train, oof_test get_oof_predictions(base_models, X_train, y_train, X_test) # 元模型Ridge 回归简单但不容易过拟合 meta_model Ridge(alpha1.0) meta_model.fit(oof_train, y_train) # 最终预测 final_pred meta_model.predict(oof_test)clone函数来自 sklearn确保每折用的是未训练的模型副本。元模型选 Ridge 而不是 LightGBM是因为 OOF 特征只有 3 列再用复杂模型容易过拟合。alpha设 1.0 是默认值如果 OOF 特征之间相关性高可以调到 5.0 或 10.0。最终预测用测试集 OOF 的均值而不是某一折的模型这样更稳定。3.3 融合权重怎么定从等权到贝叶斯优化的对比等权融合是最省事的但三个模型表现不一样等权会拖累最好的那个。我一般先看单模型验证集 MAE再决定权重。如果 LightGBM 明显好于另外两个可以给它 0.5 的权重另外两个各 0.25。更精细的做法是用贝叶斯优化搜权重但要注意别在验证集上过拟合。融合方式验证集 MAE测试集 MAE训练耗时LightGBM 单模型0.820.85快等权平均0.790.83中加权平均0.5/0.25/0.250.770.81中Ridge 堆叠0.750.79慢贝叶斯优化权重0.740.82很慢从表里能看出来Ridge 堆叠在验证集和测试集上都比加权平均好而且没有贝叶斯优化那种验证集过拟合的风险。贝叶斯优化权重在验证集上最低但测试集反而回升说明权重搜得太细把验证集的噪声也学进去了。我一般直接用 Ridge 堆叠省事且稳。4. 避坑与排查二手车价格模型上线前必须过的五道坎4.1 价格分布长尾导致 MAE 虚低现象验证集 MAE 只有 0.8 万上线后高价车预测误差经常超过 3 万。原因二手车价格分布右偏大量 5 到 15 万的车拉低了平均误差但 30 万以上的车样本少模型没学好。解决对价格做对数变换再训练预测时指数还原或者分价位段训练多个模型。# 对数变换 y_train_log np.log1p(y_train) lgb_model.fit(X_train, y_train_log, ...) # 预测时还原 pred_log lgb_model.predict(X_test) pred np.expm1(pred_log)4.2 目标编码在测试集上分布偏移现象训练集目标编码后 AUC 0.92测试集掉到 0.78。原因测试集里出现了训练集没有的车型目标编码直接填了全局均值但全局均值和这个车型的真实价格差很远。解决对未见过的类别用品牌均值加车系均值加权填充而不是全局均值。4.3 时间泄漏用未来成交数据预测过去现象随机划分训练测试集MAE 很低但按时间划分后 MAE 翻倍。原因随机划分让模型看到了未来价格走势。解决按成交日期排序前 80% 做训练后 20% 做测试目标编码也按时间滚动计算。4.4 类别特征在 CatBoost 里没对齐现象CatBoost 训练报错提示类别特征包含浮点数。原因品牌列里有缺失值pandas 自动转成了 float。解决训练前把所有类别列转成字符串缺失填 “unknown”。for col in [brand, model, age_bucket]: df[col] df[col].astype(str).fillna(unknown)4.5 融合模型预测值超出合理区间现象堆叠后预测出负价格或者 200 万的天价。原因元模型 Ridge 是线性模型OOF 特征如果有异常值外推会失控。解决对最终预测做截断下限设 0.5 万上限设训练集价格的 99 分位数。lower 0.5 upper np.percentile(y_train, 99) final_pred np.clip(final_pred, lower, upper)5. 把模型变成能用的估价工具三个进阶技巧第一个技巧是分价位段训练。二手车价格在 10 万以下、10 到 30 万、30 万以上三个区间的特征重要性完全不同。10 万以下看车龄和里程10 到 30 万看品牌和车况30 万以上看车型稀有度和配置。我一般会训练三个子模型预测时先判断价位段再路由到对应模型。这样每个模型只需要关注自己区间的特征MAE 能再降 5% 到 8%。第二个技巧是用 SHAP 值做单条预测的解释。买家问“为什么这车估 8 万 2”你不能只给一个数字。SHAP 能告诉你每个特征贡献了多少。import shap explainer shap.TreeExplainer(lgb_model) shap_values explainer.shap_values(X_test.iloc[:1]) shap.force_plot(explainer.expected_value, shap_values[0], X_test.iloc[0])第三个技巧是定期用新成交数据做增量训练。二手车价格受新车降价、政策切换影响很大模型三个月不更新MAE 会涨 10% 以上。我一般每月用最近三个月的数据微调一次学习率设小一点比如 0.01训练轮数控制在 200 轮以内避免把旧知识覆盖掉。最后说个血泪教训别在验证集上反复调融合权重。我曾经为了把验证集 MAE 从 0.76 压到 0.73调了二十多组权重结果测试集 MAE 反而从 0.79 涨到 0.86。后来固定用 Ridge 堆叠验证集和测试集的差距一直稳定在 0.04 以内。模型上线不是考试稳定比极致重要。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑