二手车价格预测实战:从数据清洗到模型调参的完整流程
简介这是一套面向数据挖掘课程大作业的二手车价格预测完整案例适合正在学习Python数据分析、特征工程与回归建模的高校学生作为课程项目参考。压缩包内共27个文件主体包含Python源码、csv格式二手车数据集、docx实验报告与png可视化图表另有xml项目配置、README说明等辅助文件整体体积约34.85MB目录按代码、数据、文档分层组织便于对照学习。核心源码均配有详细注释覆盖数据清洗、特征处理、模型训练与价格预测的关键流程实验报告则系统梳理了分析思路、实验过程与结论可帮助读者快速迁移到同类预测任务。目前已有394人学习下载适合需要完成课程大作业或入门数据挖掘实战的读者直接参考。1. 二手车价格预测不是回归题是数据清洗题很多课程大作业把“二手车价格预测”当作一个模型比拼题默认数据拿到了、特征够用了剩下的交给 XGBoost。但真实跑下来你会发现模型选 LinearRegression 还是 LightGBM差距远没有“有没有处理价格长尾”和“有没有把品牌残值做对”来得大。二手车数据集的原始形态通常是爬虫抓下来的挂牌信息车龄、里程、排量、变速箱、过户次数混在一起价格字段里还有“面议”“一口价”和“5.8万”这种带单位的中文串。价格预测在这个场景里先是一个数据清洗问题再是一个特征工程问题最后才轮到模型选型。这篇文章按课程大作业的标准流程从数据预处理、特征构造、模型训练到实验报告组织方式把一套可以照抄的 Python 实现拆开讲清楚适合正在做这类题目或想拿真实数据集练手的人。2. 数据加载与清洗先把价格列变成可回归的数字2.1 二手数据集的常见脏数据形态课程大作业的数据集一般有两种来源一是老师给的 CSV列名可能是中文也可能是英文拼音二是自己爬的某二手车平台挂牌页。后者的问题更典型价格、里程、排量这些字段经常混着单位比如价格: 12.8万、里程: 3.2万公里、排量: 1.5L。即便老师给的是整理过的数据也大概率存在缺失值、异常值和一人多车导致的重复行。开始写代码之前先把文件读进来快速看一眼 shape、dtypes 和前几行。这一步花不了两分钟但能决定后面的清洗策略。import pandas as pd import numpy as np df pd.read_csv(car_data.csv, encodingutf-8) print(df.shape) print(df.dtypes) print(df.head())encoding参数在 Windows 下经常要换gbk否则读进来就是乱码。shape 输出能告诉你行数和列数dtypes 能看出哪几列被读成了 object——这些列就是需要清洗的重点。2.2 价格字段的清洗与区间转换价格列最常见的表现形式是两种字符串数字加单位12.8万、9万、面议价格区间10.5-12万这种情况多出现在“车主报价”和“平台估价”并存的页面上处理时我建议统一走“提取数字 → 区间取中值 → 剔除面议”这条路。区间取中值比取左端点更接近真实成交价因为挂牌价偏高的车最终会往下谈但区间本身已经框定了卖家的心理价格范围中值是一个稳定的代理变量。import re def parse_price(s): if pd.isna(s): return np.nan s str(s).replace(,, ) # 数值范围10.5-12万 nums re.findall(r\d\.?\d*, s) if len(nums) 0: return np.nan vals [float(x) for x in nums] if - in s or ~ in s: return np.mean(vals) if 万 in s: return vals[0] * 10000 return vals[0] df[price] df[price].apply(parse_price) df df.dropna(subset[price])正则\d\.?\d*负责把字符串里的所有数值抽出来万字出现时意味着单位是万元乘 10000 转成元。面议这类没有数字的字符串直接返回 NaN最后统一丢弃。这步做完之后用df[price].describe()看一眼 min、max 和分位数如果出现 0 元或者标价 300 万的异常车源说明清洗还没到底。2.3 缺失值与异常值的处理策略价格列清洗完接下来处理其余特征列。二手车数据集里缺失率高的通常是“过户次数”和“保养记录”这种非必填字段。处理方式按列的性质分三类数值型且分布近似正态用中位数填充比均值更抗离群值分类型用众数填充或者单独加一个is_missing标记列缺失率超过 60% 的列直接丢弃补出来的值也没有统计意义异常值的判定不要只盯标准差用分位数更稳妥。price列如果 99% 分位数是 80 万而某条数据是 200 万这种点大概率是豪车或录入错误。用np.percentile计算上下限并把超出部分截断到边界值比直接删除更能保留样本量。from scipy import stats # 针对数值列的异常值截断 for col in [mileage, engine_power, car_age]: q_low df[col].quantile(0.01) q_high df[col].quantile(0.99) df[col] df[col].clip(q_low, q_high) # 对价格做 log1p 变换压缩长尾 df[price_log] np.log1p(df[price])clip将 1% 和 99% 分位之外的极值拉到边界log1p是价格预测任务里非常关键的一步。车价长尾极重几万块的代步车和百万豪车在同一个量纲下MSE 会被大价格样本牵着走。取对数之后模型学的是“价格的对数”预测完再np.expm1还原误差分布更均匀。这一点在实验报告里写出来老师会觉得你是真做过而不是只会调库。3. 特征工程从“字段”到“能解释价格”的变量3.1 车龄与里程的交互特征原始数据集里通常给的是“上牌日期”而不是“车龄”需要把字符串转成 datetime再用当前年份减去上牌年份得到car_age。里程和车龄单独看都只是线性影响价格但两者结合能挖出更有解释力的信息年均里程 总里程 / 车龄。年均里程能识别出网约车或营运车转私用的样本这类车即使总里程不高损耗也远大于家用车。df[reg_date] pd.to_datetime(df[reg_date], format%Y%m%d, errorscoerce) df[car_age] 2024 - df[reg_date].dt.year df[annual_mileage] df[mileage] / (df[car_age] 1)1防止车龄为 0 的新车除零。annual_mileage大于 3 万公里的样本建议打上is_high_intensity标记作为后续模型的一个二值特征。这类业务规则的植入会让模型在新车和营运车之间自动学到不同的价格衰减曲线。3.2 品牌残值率从原始标签加工品牌列是最容易直接 One-Hot 但也是最浪费的列。德系、日系、国产的保值率差异很大直接用 One-Hot 会让模型把每个品牌当独立类别学不到“日系整体保值”这种跨品牌规律。常见做法是构造一个“品牌残值率”特征按品牌分组计算mean(price) / max(price)其中max(price)取该品牌新车的均价没有新车价格时可以退而求其次按品牌 车龄 0~1 年的样本均价值作为基准brand_price df.groupby(brand)[price].mean().sort_values() brand_top brand_price.index[-5] # 取均值最高的品牌作为基准 df[brand_resale_ratio] df[brand].map( df.groupby(brand)[price].mean() / df[df[brand] brand_top][price].max() )这段代码的思路是给每个品牌算一个“相对头部品牌的价格比例”比例大的品牌说明整体价格坚挺。模型拿到这个数比拿 50 个 One-Hot 稀疏列更容易学到梯度。品牌这个原始标签仍然保留在特征集里残值率只是补充。3.3 类别特征编码的分层策略二手车数据集里非数值列通常是 brand、series、gearbox、color、fuel_type。这几列的性质差别很大编码方式不能一刀切。特征列类别基数推荐编码方式理由gearbox2~3OrdinalEncoder手自一体可以赋 2自动挡保值率高fuel_type4~5One-Hot 或 Target Encoding油/电/混动对价格影响强且独立brand30Target Encoding避免 30 维稀疏哑变量series300不直接编码聚类或按价格聚合类别太多直接用会过拟合color8~15Target Encoding白色、黑色保值率稳定但不应单独建模Target Encoding 的做法是用该类别下目标变量的均值替代类别本身。这里的目标变量是清洗后的price_log注意要用交叉验证的方式计算均值否则直接用全量数据算会产生标签泄漏。sklearn 里有现成的TargetEncodersklearn.preprocessing0.24 之后可用自己写也很快。from sklearn.model_selection import KFold from sklearn.preprocessing import TargetEncoder kf KFold(n_splits5, shuffleTrue, random_state42) te TargetEncoder(smooth10, target_typecontinuous) # 这里用交叉验证防止过拟合 cv_preds np.zeros(len(df)) for tr_idx, va_idx in kf.split(df): te.fit(df.iloc[tr_idx][[brand]], df.iloc[tr_idx][price_log]) cv_preds[va_idx] te.transform(df.iloc[va_idx][[brand]]).ravel() df[brand_te] cv_predssmooth参数控制平滑程度值越大结果越往全局均值收缩适合样本量少的类别。Target Encoding 的坑在于验证集和测试集的类别均值必须严格由训练集算出上面循环里每次都在训练折上fit就是为了避免这一点。4. 模型训练与调参梯度提升树是默认答案但线性模型不能丢4.1 训练集划分与评估指标选用划分数据时不要直接train_test_split二手车数据里同品牌同年份的车高度相似随机划分会让模型偷偷看到“已经见过的车”的邻居。建议按车型或品牌分组切分GroupKFold按series划分保证同型号车的样本只出现在训练集或只出现在测试集这样评估出来的泛化能力更真实。评估指标用 RMSE 和 MAE 双指标。RMSE 对离群值敏感能暴露预测极端值的问题MAE 贴近业务理解直接告诉老师“平均偏差多少钱”。注意最后交报告时的价格已经是还原后的实际元不是 log 尺度所以评估时要把预测结果expm1回去再算。from sklearn.model_selection import GroupKFold from sklearn.metrics import mean_squared_error, mean_absolute_error import xgboost as xgb gkf GroupKFold(n_splits5) X df.drop(columns[price, price_log, brand, series]) y df[price_log] for tr_idx, va_idx in gkf.split(X, y, groupsdf[series]): X_tr, X_va X.iloc[tr_idx], X.iloc[va_idx] y_tr, y_va y.iloc[tr_idx], y.iloc[va_idx] model xgb.XGBRegressor( n_estimators800, learning_rate0.05, max_depth6, subsample0.8, colsample_bytree0.7, random_state42 ) model.fit(X_tr, y_tr) pred_log model.predict(X_va) pred np.expm1(pred_log) y_true np.expm1(y_va) rmse mean_squared_error(y_true, pred, squaredFalse) mae mean_absolute_error(y_true, pred) print(fRMSE{rmse:.0f} MAE{mae:.0f})np.expm1(pred_log)和np.log1p是一对互逆操作展开后是exp(x) - 1。注意一定要用同一个 sklearn 的KFold实例来拆分并同时服务于 Target Encoding 和模型评估避免因为划分方式不同导致交叉验证结果不一致。4.2 XGBoost 和 LightGBM 的参数对照课程作业里一般要求至少对比两个模型最常见的是“线性回归 vs XGBoost”或者“随机森林 vs LightGBM”。做对比时模型本身谁强谁弱其实不重要重要的是为什么某个模型在这个任务上更好。下面给一组 LightGBM 的参数模板直接复制到代码里就能跑出不错的结果。import lightgbm as lgb lgb_model lgb.LGBMRegressor( objectiveregression, n_estimators1000, learning_rate0.03, num_leaves63, max_depth7, min_child_samples20, subsample0.8, colsample_bytree0.8, reg_alpha0.1, reg_lambda0.1, random_state42, verbose-1 )num_leaves63对应max_depth6的树容量LightGBM 用叶子生长策略num_leaves比max_depth更影响模型复杂度。min_child_samples20防止叶子节点样本太少导致过拟合。reg_alpha和reg_lambda是 L1/L2 正则数据量只有几千条的时候建议都设成 0.1 左右能显著压低验证集波动。4.3 调参顺序和验证策略网格搜索是大家都知道的词但实际跑起来要注意顺序问题不要把n_estimators、learning_rate、max_depth一起塞进GridSearchCV。搜索空间呈指数增长课程数据量小还能承受几千个组合跑下来一个多小时实验报告里写起来也不好看。常用的顺序是先把learning_rate固定成 0.1n_estimators固定在 500用GridSearchCV粗调max_depth和min_child_samples锁定这两个之后再把subsample、colsample_bytree、reg_alpha、reg_lambda用贝叶斯优化或随机搜索精调。LightGBM 的训练很快所以这一步用RandomizedSearchCV可以一次多试几组组合50 次迭代基本够用。from sklearn.model_selection import RandomizedSearchCV from scipy.stats import uniform, randint param_dist { num_leaves: randint(31, 127), max_depth: randint(5, 10), min_child_samples: randint(10, 50), subsample: uniform(0.6, 0.3), colsample_bytree: uniform(0.6, 0.3), reg_alpha: uniform(0, 0.5), reg_lambda: uniform(0, 0.5) } search RandomizedSearchCV( lgb.LGBMRegressor(objectiveregression, n_estimators1000, learning_rate0.03, random_state42), param_distributionsparam_dist, n_iter50, cv5, scoringneg_mean_squared_error, random_state42, n_jobs-1 ) search.fit(X, y) print(search.best_params_)scoringneg_mean_squared_error注意是负的sklearn 默认最小化损失所以取负数。n_jobs-1会跑满所有 CPU 核。搜索完成后把best_params_里的参数填入模型再用整个训练集重新训练一次最后在测试集上评估并记录日志。5. 特征重要性与稳定性验证报告里最加分的图表数据5.1 用 SHAP 值验证特征解释方向课程大作业的实验报告通常要求画特征重要性图但重要性只告诉我们哪些特征重要没告诉我们“影响方向”。SHAP 能补上这个缺口每个特征对预测的贡献正负和大小一目了然。import shap explainer shap.TreeExplainer(model) shap_values explainer.shap_values(X_va) shap.summary_plot(shap_values, X_va, max_display15)s summary_plot生成的是蜂群图横轴是 SHAP 值正值代表推高价格。这张图画出来之后报告里可以明确写“car_age 的 SHAP 值随车龄增大显著为负年均里程大于 3 万时交互效应加剧价格下跌”比干贴一棵决策树更有说服力。SHAP 值的计算在树模型上是精确的不需要蒙特卡洛采样几万条样本也只需几十秒。5.2 残差分析发现定价偏差的系统性来源模型训练完成后应留出一折不参与调参做独立的残差分析。把真实价格和预测价格都还原成元然后按品牌分组看残差的均值。如果某个品牌的预测值系统性偏高或偏低往往意味着品牌编码或者残值率特征没有完全捕捉它的特殊性。residual y_true - pred residual_df pd.DataFrame({brand: df.iloc[va_idx][brand].values, residual: residual}) brand_res residual_df.groupby(brand)[residual].agg([mean, count]) print(brand_res.sort_values(mean).head(10))这步的输出直接决定实验报告的“不足与改进”部分怎么写。比如国产某品牌残值均值是 -1.2 万说明模型低估了它的价格——可能是置换补贴或者新车降价传导到二手车市场的时间差造成的。如果时间允许可以再按 every 车系构造一个趋近年份的孤岛特征下一版模型很有可能涨几个点的 R²。6. 实验报告的组织技巧把过程和结论写成可复现的技术文档实验报告不是把代码粘贴一遍就完事要求是“别人按照报告能复现出同样结果”。我见过的大部分课程报告问题都出在缺中间检查点只写了最终 RMSE没有写每个特征加入前后的变化。这里给一个四段式的报告骨架数据说明与预处理原始数据规模、清洗规则、最终保留的样本量和特征数特征工程列出每个特征的构造逻辑特别是 Target Encoding 的交叉验证细节模型对比至少三组实验线性基线、XGBoost、LightGBM记录训练时间和指标结果分析与改进方向SHAP 图 残差表 下一步能做的 2~3 个优化点报告里出现的每个数字都要能和代码对上比如“清洗后剩余 5321 条样本价格取对数后偏度从 3.1 降至 0.4”这类具体数值比任何判断都更有说服力。特征重要性图用 SHAP 的max_display15控制展示数量超过 20 个特征时图会挤成一团。数据集的划分种子要固定报告里写明random_state42这是可复现性的最低要求。最后提醒一个容易被忽略的步骤在提交代码前重新从原始 CSV 跑一遍完整脚本确认没有使用任何全局变量或内存中修改过的中间结果。课程作业的评分老师会直接运行python main.py任何一步意外报错都会让前面的所有工作白费。把这个流程想成交付一个命令行接口输入原始文件、输出模型指标和预测 CSV这才是数据挖掘课程大作业最稳妥的收尾方式。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →