☰
二手车价格预测实战:数据清洗、特征工程与多模型融合源码解析
2026/9/25 16:48:21 网站建设 项目流程

简介:面向机器学习与数据挖掘初学者及毕业设计学生,这是一套二手车交易市场大数据挖掘项目包。项目覆盖数据缺失值预测、交易价格预测与成交周期挖掘三个核心任务,采用多模型融合策略,对比XGBoost、随机森林、GBDT、梯度提升回归等模型效果。压缩包共二十四个文件,大小约16.88MB,以Python源码、Jupyter Notebook、训练好的pkl模型为主,另含可视化图片、项目配置与说明文档;目录按问题编号排列,两个Notebook分别处理问题一和问题二,Python脚本承担缺失值预测与类别转换等任务。目前已有九十八人浏览学习。适合作为课程设计、期末大作业或本科毕业设计的完整参考,读者可获取可运行代码、模型权重、数据样例与调优思路,直接复现二手车价格预测和成交周期挖掘流程。

1. 二手车大数据挖掘源码包:值得研究,但不是因为它自带一个能跑的模型

基于机器学习和多模型融合的二手车交易市场大数据挖掘源码包,多半出现在课程设计和毕设场景里,解压后是一堆 csv、几个 notebook、train.py 和一份项目说明。它要解决的问题很直接:给定车龄、里程、排量、变速箱、过户次数、城市,预测一台二手车的成交价。这个价值对买家是估值参考,对车商是收车依据,对做数据挖掘的人是完整的清洗、特征、建模、融合、验证闭环。适合三类人:赶课程设计或毕设的学生、想拿机器学习算法练综合项目的开发者、想评估二手车智能估价能不能落地的从业者。先说结论:多模型融合不会让分数暴涨,但会让边缘区间的预测稳很多,这部分恰恰是源码包最容易漏掉的东西。

2. 把交易记录洗到能喂给模型:清洗、编码与时间划分

解压这种源码包之后,先别急着跑模型。凡是来自非官方渠道的二手车交易数据,几乎都带着录入错误和平台口径差异。常见字段不外乎 car_id、brand、model_name、register_date、mileage、engine_capacity、gearbox、emission_standard、transfer_count、city、transaction_date、price,拿到手先做一次完整体检。很多源码包自带的模型跑分低,问题不在模型选型,而在数据根本没洗干净。

数据清洗的目标是让模型学到“车的价值”,而不是学到“录入人员的习惯”。比如有些平台把事故车单独标记,有些平台不标记,同一批车源放在一起训练时,模型会把事故车的低价归因到品牌或车龄上。所以清洗阶段宁可多砍,不要留着脏样本让树模型硬学。

2.1 清洗:缺失值、重复值、异常值一步到位

import pandas as pd import numpy as np df = pd.read_csv("car_transactions.csv", parse_dates=["register_date", "transaction_date"]) # 1) 去重:同一台车同一天只保留最后一次成交记录 df = df.drop_duplicates(subset=["car_id", "transaction_date"], keep="last") # 2) 异常里程:负数直接删;里程为 0 但车龄大于 1 年视为录入异常 df = df[df["mileage"] >= 0] df = df[~((df["mileage"] == 0) & (df["car_age_years"] > 1))] # 3) 车龄校核:成交日与上牌日之差必须落在 0 到 30 年之间 df["car_age_years"] = (df["transaction_date"] - df["register_date"]).dt.days / 365.25 df = df[(df["car_age_years"] >= 0) & (df["car_age_years"] <= 30)] # 4) 价格缺失:缺失率低于 10% 直接删行,高于 10% 用同品牌同车龄段中位数回填 price_missing_rate = df["price"].isna().mean() if price_missing_rate < 0.1: df = df.dropna(subset=["price"]) else: age_bins = pd.cut(df["car_age_years"], bins=[0, 3, 6, 10, 30]) df["price"] = df.groupby(["brand", age_bins])["price"].transform( lambda x: x.fillna(x.median()) )

这里的参数要解释清楚:keep="last"表示如果同一台车同一天有多条记录,保留最后写入的那条,因为后写入的更可能是最终成交状态;car_age_years大于 30 年的样本基本是录入错误,极少数真古董车属于业务异常值,直接删掉对回归任务影响极小;价格缺失率阈值 10% 是我常用的分界点,低于它删除行不会改变整体分布,高于它就必须用同品牌同车龄段的中位数兜底,避免直接删行把高缺失品牌全部剔出训练集。

2.2 标称字段怎么进模型:品牌、车型、变速箱的编码策略

品牌、车型、变速箱、排放标准都是文本型字段。LightGBM、XGBoost、CatBoost 这类树模型虽然声称“处理类别特征”,但不代表可以直接把字符串丢进去。我的习惯是先做低频归并,再做整数编码。

# 品牌:样本量小于 100 的合并成 OTHER,避免低频品牌带偏树模型 brand_counts = df["brand"].value_counts() df["brand"] = df["brand"].where(df["brand"].map(brand_counts) >= 100, "OTHER") df["brand_code"] = df["brand"].astype("category").cat.codes # 变速箱:先归并再独热 df["gearbox_group"] = df["gearbox"].replace({ "手自一体": "自动", "双离合": "自动", "无级变速": "自动", "手动": "手动" }) df = pd.get_dummies(df, columns=["gearbox_group"], prefix="gb") # 排放标准:文本转数字,国二到国六分别映射 2-6 emission_map = {"国二": 2, "国三": 3, "国四": 4, "国五": 5, "国六": 6} df["emission_score"] = df["emission_standard"].map(emission_map).fillna(0)

品牌用astype("category").cat.codes而不是LabelEncoder,是因为前者对未出现过的类别更宽松,后面的交叉验证不会因为新品牌直接报错。变速箱归并成自动、手动两档,是因为国内二手车市场里双离合、手自一体、无级变速在定价逻辑上都算“自动挡”,拆太细会让小样本类别产生抖动。排放标准是典型的有序类别,映射成 2 到 6 的数值保留顺序信息,fillna(0)负责处理“未标注”这类缺失。

2.3 按时间划分数据集:防止把“未来”偷进训练集

二手车交易数据最容易被忽视的坑是随机切分训练集和验证集。如果 1 月到 6 月的车和 7 月到 12 月的车同时出现在训练集里,验证集分数会虚高,因为模型见过同一时间段的宏观行情。换句话说,训练集和测试集的时间窗口一旦重叠,模型就在“开卷考试”。

df = df.sort_values("transaction_date") split_date = df["transaction_date"].quantile(0.8) train = df[df["transaction_date"] <= split_date] valid = df[df["transaction_date"] > split_date] print(f"训练集时间范围:{train['transaction_date'].min()} ~ {train['transaction_date'].max()}") print(f"验证集时间范围:{valid['transaction_date'].min()} ~ {valid['transaction_date'].max()}")

quantile(0.8)表示按时间排序后取前 80% 作为训练集,后 20% 作为验证集,这是时间序列类回归任务里相对稳妥的划分。注意这里不要用random_state随机打乱,因为二手车价格天然受淡旺季和车辆保值率波动影响,只有严格按时间前后切割,验证集分数才对未来有参考意义。如果你的源码包里自带随机划分脚本,建议直接改成时间划分再重新跑一遍基线。

3. 特征工程决定融合模型上限:三个能直接抄的特征方案

多模型融合能弥补的是“模型偏差”,弥补不了“特征缺失”。二手车定价里最核心的信息就藏在那十几个原始字段里,怎么把车龄、里程、排量、品牌、车型转成模型真正用得上的数值,直接决定了融合模型的上限。特征工程不是玄学,是我做大数据挖掘项目时耗时最长的一步,通常比调参多花两到三倍时间。

3.1 原始字段的加工顺序:车龄、里程、排量先后手

车龄和里程是二手车定价的骨架。车龄直接从 register_date 和 transaction_date 算成 float,单位用年而不是天数,避免数值跨度过大导致树模型切分不稳定。里程的分布通常右偏严重,有一批一年跑五万公里的营运车,也有一年只跑五千公里的代步车,直接把原始里程塞进模型,高里程那侧会被树模型过度敏感。

我的习惯是先按车龄分段对里程做分组统计,再对里程做对数变换。这样做的理由是:同样 3 万公里,对一台 2 年车龄的车和一台 10 年车龄的车含义完全不同,单纯的全局变换无法体现这种差异。排量本身是高区分度字段,但注意部分平台把 1.5T 和 1.5L 混着标,处理前先看唯一值分布,把带 T 的字符串拆出来单独做一个涡轮增压标记。

3.2 两个高杠杆特征:保值率与价格密度

保值率是二手车定价业务里真正懂行的人会看的指标,公式是成交价除以新车指导价。它衡量的不是绝对价格,而是这台车在市场上的折旧速度。做特征时不需要对每台车算,而是按品牌和车龄段聚合出中位保值率,再回填到每一行。

# 保值率:没有新车指导价时,用同品牌同车龄段的历史中位成交价代替 age_band = pd.cut(df["car_age_years"], bins=[0, 1, 3, 5, 8, 30]) brand_age_median = df.groupby(["brand", age_band])["price"].transform("median") df["brand_age_median_price"] = brand_age_median # 用字段缺失率决定向量:有 guide_price 就优先用 if "guide_price" in df.columns and df["guide_price"].notna().mean() > 0.5: df["value_retention"] = df["price"] / df["guide_price"].replace(0, np.nan) else: df["value_retention"] = df["price"] / (brand_age_median + 1)

第二个高杠杆特征叫价格密度。同一个车型最近 90 天成交笔数越多,说明这个车源市场竞争越激烈,买方砍价空间越大。直接用原始笔数会被热门车型带偏,所以要取对数。

df["deal_density"] = df.groupby("model_name")["car_id"].transform("count") df["log_deal_density"] = np.log1p(df["deal_density"])

这两个特征的逻辑要分开说:保值率捕捉的是品牌和车型的折旧曲线,让模型学会“同品牌同年限,保值率高的车应该卖得更贵”;价格密度捕捉的是供给端竞争,笔数越多越说明这车是大众流通款,价格透明、溢价空间小。两个特征都来自历史交易统计,注意只能基于训练集计算再映射到验证集,否则就提前把验证集的信息写进了特征。

3.3 分位数截断与 Log 变换:数值特征的“后悔药”

特征工程里最常翻车的操作是直接对原始数值做标准化或归一化。树模型不需要标准化,但非常需要截断。里程、成交价、车龄这类字段尾部往往有极端大值,LightGBM 对这类值做直方图分箱时,会让极个别样本单独占一个箱,白白增加切分点。

# 分位数截断:对里程和排量取 1% 到 99% 分位,把极端尾部拉回正常分布 for col in ["mileage", "engine_capacity"]: lo = df[col].quantile(0.01) hi = df[col].quantile(0.99) df[col] = df[col].clip(lo, hi) # 对数变换:成交价作为回归目标也做 log,让预测误差从绝对误差变成近似相对误差 df["log_mileage"] = np.log1p(df["mileage"]) df["log_price"] = np.log1p(df["price"])

quantile(0.01)和quantile(0.99)是我常用的截断区间,比 0.05/0.95 更保守,保住了尾部样本又不让极端值影响分箱。log1p比log多一个+1,是为了处理里程恰好为 0 的样本。成交价做 log 变换是最重要的一步:直接把原始价格当回归目标时,模型会把绝大部分拟合能力花在少数几十万的高价车上,导致几万元的主流代步车预测误差反而很大;log 之后误差近似变成百分比误差,这个问题会被天然抹平。

4. 多模型融合的原理与复现:把 LightGBM、XGBoost、CatBoost 拧成一股绳

多模型融合在二手车价格预测里不是炫技,而是刚需。原因是单模型各有偏科:LightGBM 对大规模样本训练快,但对噪声敏感;XGBoost 对缺失值处理强,但容易过拟合;CatBoost 对标称特征友好,却对数值特征的尺度变化不敏感;随机森林方差低,但上限也低。把它们放在一起,才有机会把各自的偏科互补掉。周志华《机器学习》里讲集成学习的那一章,核心观点就是“好而不同”:每个基模型要足够准,同时彼此差异要足够大。

4.1 为什么单模型跑不过融合:偏差与方差的那笔账

单棵决策树是高方差低偏差,bagging 通过平均把方差压下来;boosting 通过逐步拟合残差把偏差降下去,代价是方差变大。把四个不同算法做融合,本质上是在“偏差”和“方差”之间找一个更优的平衡点。更直白的说法是:如果四个模型在大部分样本上预测都差不多,但在不同样本上各有输赢,融合后就能把各自赢的那部分留下来。

判断一个多模型融合方案是否有价值,先看单模型的验证集表现。如果单模型里最好的 LightGBM 跑出 MAE 是 0.9 万,四个基模型融合后还是 0.9 万,说明基模型之间差异不够,融合没起作用。真正合理的期望是:融合模型比最好的单模型小幅提升 3% 到 5%,同时验证集上预测值的方差明显变小,也就是高估的低估的极端样本都更少了。

4.2 第一层基学习器的选型与参数基线

第一层是融合的地基。选型上我坚持“两个 boosting 加一个 bagging 再加一个纯线性模型”的组合,刻意拉开差异。LightGBM 和 XGBoost 虽然都是 boosting,但实现细节不同,可以同时保留;CatBoost 对类别特征有 ordered boosting 机制,和前面两者在特征处理上差异最大;随机森林负责把单棵树的抖动平均掉。

模型关键参数初始值说明
LightGBMnum_leaves31控制树复杂度,比 max_depth 更值得先调
LightGBMlearning_rate0.05学习率,配合 n_estimators 一起看
XGBoostmax_depth6深度太深容易在稀疏特征上过拟合
XGBoostsubsample / colsample_bytree0.8 / 0.8行列采样,降低方差
CatBoostdepth6对应树的深度,与 iterations 配合
CatBoostrandom_seed42保证复现
RandomForestn_estimators600树数量,越大越稳但训练越慢
from sklearn.ensemble import RandomForestRegressor import lightgbm as lgb import xgboost as xgb from catboost import CatBoostRegressor feature_cols = [ "car_age_years", "log_mileage", "engine_capacity", "emission_score", "brand_code", "log_deal_density", "value_retention", "gb_auto", "gb_manual" ] X = df[feature_cols].values y = np.log1p(df["price"].values) # 与 3.3 的 log_price 保持一致 base_models = { "lgb": lgb.LGBMRegressor( n_estimators=1200, learning_rate=0.05, num_leaves=31, subsample=0.8, colsample_bytree=0.8, random_state=42, verbose=-1), "xgb": xgb.XGBRegressor( n_estimators=1200, learning_rate=0.05, max_depth=6, subsample=0.8, colsample_bytree=0.8, random_state=42), "cat": CatBoostRegressor( iterations=1200, learning_rate=0.05, depth=6, random_seed=42, verbose=0), "rf": RandomForestRegressor( n_estimators=600, max_depth=12, min_samples_leaf=5, random_state=42), }

参数基线记住一个原则:先让四个模型用相近的迭代轮数和学习率跑起来,再单独调它们各自最敏感的那个参数。LightGBM 先调num_leaves,XGBoost 先调max_depth,CatBoost 先调depth,随机森林先调max_depth。不要一上来就 GridSearch,参数空间会爆炸,而且二手车数据量级下,提升未必比得上一次完善的特征工程。

4.3 Stacking:第二层为什么用 Ridge,而不用又一个 GBDT

第一层的四个模型各自输出一个预测值,把它们拼成一列新的特征矩阵,再用第二层模型拟合真实价格,这就是 Stacking。第二层的任务不是继续挖掘原始特征,而是学会“四个模型各说各话时,我听谁的”。这里的关键是:第一层的预测值传给第二层时,必须用 out-of-fold 预测,否则第二层学到的是第一层在训练集上的“死记硬背”。

from sklearn.model_selection import KFold from sklearn.linear_model import Ridge def oof_predict(model, X, y, n_splits=5): kf = KFold(n_splits=n_splits, shuffle=True, random_state=42) oof = np.zeros(len(X)) for train_idx, valid_idx in kf.split(X): model.fit(X[train_idx], y[train_idx]) oof[valid_idx] = model.predict(X[valid_idx]) return oof # 第一层:生成 out-of-fold 预测,同时在新样本上做平均预测 meta_train = np.column_stack([ oof_predict(base_models["lgb"], X, y), oof_predict(base_models["xgb"], X, y), oof_predict(base_models["cat"], X, y), oof_predict(base_models["rf"], X, y), ]) # 第二层:Ridge 线性回归,alpha=1.0 控制正则强度 meta_model = Ridge(alpha=1.0) meta_model.fit(meta_train, y)

n_splits=5是 trade-off 的常见取值,折数太少(如 3)out-of-fold 预测不够稳,折数太多(如 10)训练时间翻倍但提升有限。shuffle=True配合random_state=42保证每次切分可复现。第二层选 Ridge 是因为四个基模型的预测值相关性很高,几乎共线性,Ridge 的 L2 正则会把冗余权重压小,而不会像 LinearRegression 那样在共线性数据上产生权重震荡。千万不要在第二层继续叠一个 XGBoost,OOF 预测里已经包含第一层各自的系统误差,再用树模型去硬学,等于把误差又学了一遍。

新样本预测时,要先把四个基模型的输出拼接好再喂给 Ridge:

def blend_predict(models, meta_model, X_new): meta_features = np.column_stack([ models["lgb"].predict(X_new), models["xgb"].predict(X_new), models["cat"].predict(X_new), models["rf"].predict(X_new), ]) return np.expm1(meta_model.predict(meta_features)) # 还原真实价格

np.expm1是log1p的逆操作,模型训练时目标做了 log 变换,预测输出必须还原回真实价格单位(万元)。这一步漏掉,预测结果整体会低一大截,售价十几万的车可能只预测出几万,这是最典型的翻车姿势之一,后面避坑章节还会展开。

5. 二手车多模型融合避坑排查:现象、原因、处理办法

做这种源码包项目,最花时间的往往不是建模而是排错。下面五条都是我从解压到验收的真实经历里总结出来的坑,每一条都按现象、原因、解决写清楚,照着排查能省半天时间。

5.1 zip 伪加密与解压路径乱码

现象:Windows 自带解压工具提示“文件损坏或密码错误”,明明项目说明里没写密码。用 7-Zip 打开却能看到完整文件列表。

原因:这是 zip 的伪加密机制。文件目录区标记了加密位,但数据区实际没有加密,常见于压缩工具版本差异或打包时误操作。也有可能是文件名带了非 UTF-8 编码,导致解压路径乱码。

解决:直接用 7-Zip 打开,忽略加密标记拖出文件;或用 Python 检查每个文件的加密标记位:

import zipfile with zipfile.ZipFile("project.zip") as zf: for info in zf.infolist(): # flag_bits 第 0 位为 1 表示加密标记,伪加密时数据其实没加密 if info.flag_bits & 0x1: print(f"{info.filename}: 加密标记={info.flag_bits & 0x1}")

flag_bits & 0x1是判断伪加密的标准姿势。如果是真加密,info.flag_bits还伴随其他位被置位,需要密码;如果只有第 0 位被置位,基本可以确认是伪加密,7-Zip 在解压时遇到这种情况会直接忽略加密标志。被乱码困扰时,优先用 7-Zip 的“恢复到指定目录”功能,它会按原始路径重建目录结构。

5.2 里程表被调:同车龄段里程分布右尾过长

现象:训练出来的模型对高里程车预测偏低,而且同品牌同车龄段里,个别里程奇高(比如 10 年车龄 40 万公里)的样本残差极大。

原因:调表是二手车行业的老问题,数据里混入这类样本后,模型会把里程的负效应学得过于激进;另一种情况是营运车转私家车,登记里程没重置,里程本身真实但车况和同年限私家车完全不同。

解决:按车龄段分组做里程截断,避免树模型被极端尾部带偏:

mileage_bands = pd.cut(df["car_age_years"], bins=[0, 1, 3, 5, 8, 30]) df["mileage_clipped"] = df.groupby(mileage_bands)["mileage"].transform( lambda s: s.clip(upper=s.quantile(0.99)) )

每个车龄段只取该段 99 分位作为上限,超出上限的里程按上限处理,而不是直接删掉样本。原因是被截断的样本还保留着品牌、车龄、排量等有效信息,只是里程不可信,截断比删除对模型更友好。另外,如果数据里有transfer_count字段,把变更次数大于 5 的样本单独打一个标记,这类车往往是多次转手、车况存疑的低价车。

5.3 排放标准文本直接入模,树模型学到的是字符串乱序

现象:用LabelEncoder把排放标准从“国二”到“国六”编码成 0 到 4,模型分数比不做这个字段更差。

原因:LabelEncoder赋的值不是按顺序给的,可能“国四”是 1、“国二”是 0、“国六”是 2,树模型在切分时读到的是这种无意义的整数排列,反而引入了噪声。

解决:手动维护有序映射,做成 2 到 6 的数值:

emission_map = {"国二": 2, "国三": 3, "国四": 4, "国五": 5, "国六": 6} df["emission_score"] = df["emission_standard"].map(emission_map).fillna(0)

fillna(0)把“未标注”和“新能源”这类特殊情况统一归到 0。如果业务里要区分新能源车,额外加一列is_new_energy布尔特征,不要让 emission_score=0 承担双重语义。注意排放标准对价格的约束力是逐年变化的,2019 年以后国六对旧排放标准车型的压制更强,可以考虑把 transaction_date 的年份也放进特征,让树模型自己学出这种交互效应。

5.4 目标编码泄漏:验证集分数虚高,上线就崩

现象:用全量二手车交易数据计算“每个车型的平均成交价”放入特征,验证集 R2 高达 0.97,但换个时间段的数据预测就差得离谱。

原因:这是典型的目标编码泄漏。车型平均成交价本身就是由价格算出来的,全量统计时已经包含了验证集的价格信息,模型等于提前看到了答案。

解决:特征里所有由 price 聚合出来的统计量,一律只用训练集计算,再映射到验证集:

train_stats = train.groupby("model_name")["price"].median().rename("model_median_price") train = train.join(train_stats, on="model_name") valid = valid.merge(train_stats.to_frame(), left_on="model_name", right_index=True, how="left")

这里的关键是“先切分、再统计”。如果不放心,还可以在训练集内部用交叉验证生成统计量,但这对二手车项目的增量不大,时间划分之后训练集内部再做 5 折统计,已经能把泄漏压到很低。记住一个判断标准:任何特征的聚合计算,只要分母包含验证集行,就一定泄漏。

5.5 模型文件版本错位:joblib 和 pickle 不是一个东西

现象:项目说明里写“加载 model.pkl 即可预测”,实际代码里却用joblib.load("model.pkl"),有时报出ModuleNotFoundError或者AttributeError: 'LGBMRegressor' object has no attribute 'best_iteration_'。

原因:LightGBM 的不同版本之间模型结构有兼容性问题,跨版本加载容易丢属性;另外 sklearn 官方现在推荐用 joblib 而是 pickle,两个格式混着用,文件头都看不出差别。

解决:先确认模型文件的实际格式。joblib 文件开头有 n_bytes 记录,pickle 文件开头有切分协议头;直接用文件管理器看后缀最省事,.pkl和.joblib都可能是同一个对象,但加载方式要对上。加载后永远先跑一个最小样例:

import joblib # 先确认文件格式,统一用 joblib 加载 model = joblib.load("model.joblib") sample = X_valid[:5] # 取 5 条验证集数据做冒烟测试 pred = model.predict(sample) assert pred.shape == (5,), "prediction shape mismatch"

assert pred.shape == (5,)这一行不是废话,它能在 1 秒内暴露列顺序不一致、模型文件加载错误这类黑匣子问题。更保险的做法是,如果源码包里同时有训练脚本和模型文件,直接重新训练一遍,因为真正值钱的是特征工程和融合逻辑,而不是带着版本烙印的模型二进制。我自己做这种项目时,从来不在交付物里放训练好的模型文件,只放训练脚本和特征定义,让接收方用自己的环境复现。

6. 用残差和价格段指标验收融合模型:能不能落地说了算

验收融合模型的方法不能只看验证集 MAE 或 R2。二手车价格区间跨度从一万多到几十万,一个全局 MAE 会掩盖低价车的系统性高估和高价车的极端误差。我的习惯是先看残差分布,再按价格段看相对误差。

残差等于真实价格减预测价格。如果残差在低价段全是负的,说明模型系统性低估了低价车;如果残差随价格升高明显增大,说明模型拿捏不住高价车。对这种右偏分布,我把残差除以真实价格得到相对残差,再按价格段分组看中位数,比全局 MAE 直观得多。

价格段样本量建议重点看的指标
5 万以下通常最大相对残差中位数,容易系统性高估
5 万到 15 万主流区间MARE,这段是模型主力得分区
15 万到 30 万中等绝对误差与相对残差一起看
30 万以上通常较小合并相邻段,样本太少单独看没有意义

价格段划分不是拍脑袋,5 万以下的车龄普遍超过 8 年,排放标准、维修成本这些因素开始主导价格;30 万以上样本通常稀少,融合模型在这里大概率过拟合,不要因为个别样本误差大就否定整个模型。

最后可以用 SHAP 对基模型做一次快速可解释性对齐,检查高价车的预测是否主要由品牌和车龄驱动,里程是否出现在特征重要性的前三位:

import shap explainer = shap.TreeExplainer(base_models["lgb"]) shap_values = explainer.shap_values(X_valid_sample) shap.summary_plot(shap_values, X_valid_sample, feature_names=feature_cols)

这里解释的是一个基模型而不是整个 Stacking,因为 Ridge 融合层没有树结构,无法直接做 SHAP。如果 SHAP 里里程的重要性排到第一,而业务常识告诉你品牌和车龄才该是第一,那就要回头查特征里是否混入了价格相关的统计量。我给自己定的验收铁律是:融合模型验证集表现必须稳定优于最好的那个单模型,否则就返回检查特征工程,而不是继续堆模型。这份从解压数据到诊断残差的流程,是我做这类二手车大数据挖掘项目踩了一圈坑之后沉淀下来的习惯,希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询