
简介这是一份面向数据挖掘课程作业的二手车价格预测完整案例融合Python源码、数据集、实验报告与详细注释适合高校学生、课程设计者及入门实践者参考。资源共27个文件压缩包约34.85MB核心内容包括2个Python脚本、1个CSV二手车交易数据集、1份DOCX实验报告以及9张结果可视化PNG图、8个XML工程配置和1个Markdown说明文档目录结构清晰便于对照源码与文档学习也方便二次修改。目前已有394人学习下载可作为同类课程大作业的完整模板。通过这份资源使用者可以快速掌握数据预处理、特征工程、模型训练与评估的完整流程同时借助逐行注释与报告理解每一步设计思路直接运行代码复现预测结果有效节省从零搭建项目的时间。1. 二手车价格预测大作业拆解一份能直接跑通并写进报告的数据挖掘闭环每到期末数据挖掘课的教室里总会出现同一类求助模型跑完 R² 是负的、实验报告凑不满三页、代码注释比天书还难懂。二手车价格预测是课程大作业的常客因为它数据脏、特征杂、模型效果差距明显恰好把数据挖掘的完整链路都逼了出来。你手上这套 Python 源码 数据集 实验报告 详细注释的资源包关键不是“跑通”而是把从数据分析到建模再到报告成稿的链路拆给读者看。这套流程适合正在做课程设计、需要交实验报告、或者想用真实数据集练一遍 Python 数据挖掘实战的人。照着做一遍你能复现完整流程、看懂参数为什么这么设并在答辩时把“为什么选这个模型”讲明白。2. 拿到数据集先别建模EDA 与数据清洗决定模型上限解压资源包之后第一件事不是立刻打开 notebook 跑模型而是把数据文件找出来弄清每个字段的含义和状态。大多数二手车数据的通病很一致价格列带着逗号或货币符号里程有录入错误注册年份和采集年份混在一起。这些脏数据如果不处理后续特征工程和模型对比的结果都是空中楼阁。下面按课程大作业的常见节奏把数据体检到清洗的每一步写成可直接运行的代码。2.1 字段体检先看类型、缺失率和分布再决定清洗策略先用 pandas 把数据读进来做一次全面体检。文件名以解压后实际的 csv 为准我示范时统一写成car_data.csv。import pandas as pd import numpy as np # 读入数据集文件名视实际解压结果调整 df pd.read_csv(car_data.csv) print(样本量与特征数:, df.shape) print(字段类型:\n, df.dtypes) print(缺失率:\n, df.isnull().mean().sort_values(ascendingFalse)) print(数值字段分布:\n, df.describe(percentiles[.01, .25, .5, .75, .99]).T)这四行输出就是实验报告“数据探索”章节的全部素材。shape告诉你样本量和字段数dtypes用来发现价格、里程这类本应是数值的列被读成了object说明列里混入了逗号、字母或空字符串。isnull().mean()按列算缺失率超过 30% 的字段建议直接删除而不是填充因为填充大量假数据对树模型反而是噪声。describe里我特意加了.01和.99两个分位点目的就是抓长尾——正常二手车里程的中位数可能只有 8 万公里但少数样本能到 99 万公里这些极端值往往不是真实交易而是录入时多打了一位。字段体检之后要记录两件事一是价格序列严重右偏均值远大于中位数二是某些分类字段缺失率扎眼比如排放标准可能有 20% 的空值。这两条在报告里要写清楚“看到了什么所以下面决定怎么处理”。2.2 缺失值与异常值处理价格、里程、车龄的三块硬骨头第一个硬骨头是价格列。如果读进来是object先做字符串清洗再转数值。连续几个 cell 里都出现过的清洗手法如下# 清洗价格列去掉逗号和货币符号转成浮点数 df[price] df[price].replace(,, , regexTrue).astype(float) # 价格对数变换缓解右偏同时保证后面模型的预测值非负 df[price_log] np.log1p(df[price]) # 里程极端值处理超过 99.9% 分位数的样本视为录入错误 mile_upper df[mileage].quantile(0.999) print(里程上限:, mile_upper) df df[df[mileage] mile_upper] # 车龄计算用数据采集年份减去注册年份 CAR_AGE_BASE 2023 # 改成你这份数据集的采集年份 df[car_age] CAR_AGE_BASE - df[reg_year] df df[(df[car_age] 0) (df[car_age] 30)]log1p是log(1x)专门处理存在 0 值的正数序列预测完再用expm1还原两条互为逆运算。里程用quantile(0.999)切一刀而不是用max因为真正的极端值只需去掉最顶部千分之一硬编码一个“里程大于 50 万就删”容易误伤老款豪华车。车龄小于 0 说明注册年份晚于采集年份属于录入错误大于 30 年的车样本量极少且残差极大删掉对整体稳定更有利。缺失值处理按字段性质来数值字段用中位数填充分类字段用众数填充或者单独填一个“未知”并保留缺失标记比用 0 填充更诚实。2.3 分类变量编码品牌、车系、排放标准怎么喂给模型二手车数据的分类字段是重灾区。品牌有几十种车系上百种直接做 one-hot 编码会让特征矩阵膨胀到几千列小数据集上训练慢且容易过拟合。常见做法是频次编码加目标编码。频次编码把类别替换成它在全样本中出现的次数等于告诉模型“这个品牌是冷门还是热门”目标编码用该类别价格均值作为特征能捕捉品牌溢价这类业务信息。# 频次编码把品牌替换成出现次数 brand_freq df[brand].value_counts() df[brand_freq] df[brand].map(brand_freq) # 目标编码用该类别对数价格均值作为特征 brand_price_mean df.groupby(brand)[price_log].transform(mean) df[brand_target] brand_price_mean # one-hot 只保留低频范围内的高价值字段比如变速箱类型 df pd.concat([df, pd.get_dummies(df[gearbox], prefixgb)], axis1)目标编码这一段很容易写出数据泄漏如果先groupby算全量均值再拆分训练测试集测试集的信息已经悄悄流进训练集后面交叉验证分数会虚高。正确顺序是先train_test_split然后只在训练集上groupby计算均值再map到测试集。transform(mean)这行代码放在验证集上时务必重新计算。频次编码没有泄漏风险因为它只依赖类别本身出现次数不依赖标签可以安全地在全量数据上计算。3. 从线性回归到 XGBoost二手车价格建模的基准与参数调整数据清洗完成之后特征工程和模型选型是一体的。二手车价格预测的实践结论很一致线性模型能把业务规律讲清楚但精度有限树模型能吃到非线性交互但容易过拟合集成模型则是课程作业里性价比最高的选择。这一章把从基准到集成的完整路径走一遍代码直接对应资源包里建模部分的注释。3.1 特征工程收尾车龄分箱、里程对数化、业务字段合并把能用的字段整理成建模特征。里程继续做对数变换因为二手车价格与里程的关系接近“前 5 万公里贬值最快、后面趋缓”对数化后线性模型更容易拟合这种衰减。车龄本身是个连续值但价格下跌存在台阶效应比如 3 年质保期、6 年免检期直接分箱让树模型省去自己找切分点。# 里程对数化 df[mileage_log] np.log1p(df[mileage]) # 车龄分箱0-3年、3-6年、6-10年、10-15年、15年以上 df[age_bin] pd.cut(df[car_age], bins[0, 3, 6, 10, 15, 30], labels[0, 1, 2, 3, 4]) # 排放标准合并样本少的老标准合并成一档 df[emission_cluster] df[emission].map({ 国三: 老车, 国四: 老车, 国五: 主流, 国六: 新规 }) # 最终特征列表按业务理解挑选而非全量塞入 feature_cols [mileage_log, car_age, age_bin, brand_freq, brand_target, displacement, gb_手动]特征列表的筛选原则是“每个特征都有业务理由”mileage_log代表使用强度brand_target代表品牌溢价displacement代表排量带来的动力等级。没有业务理由的字段宁可不放。pd.cut的bins边界是经验的产物如果你手里的数据价格台阶不明显可以画图后重新调整不必照抄。3.2 先跑基准模型线性回归与决策树给你“误差下限”很多同学一上来就调 XGBoost这是决策上的偷懒。基准模型的意义是告诉你数据的下限在哪如果线性回归的 RMSE 已经不错说明价格主要由少数特征线性决定后面集成模型的提升空间有限如果决策树默认参数就超过线性回归说明特征交互很强值得投入树模型。两步跑完你才知道 XGBoost 的调参该往哪个方向用力。from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.tree import DecisionTreeRegressor from sklearn.metrics import mean_squared_error, r2_score X df[feature_cols] y df[price_log] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) lr LinearRegression().fit(X_train, y_train) dt DecisionTreeRegressor(max_depth8, min_samples_leaf10, random_state42).fit(X_train, y_train) for name, model in [(线性回归, lr), (决策树, dt)]: pred model.predict(X_test) rmse mean_squared_error(y_test, pred, squaredFalse) print(name, RMSE(log域):, round(rmse, 4), R2:, round(r2_score(y_test, pred), 4))random_state42固定下来保证每次跑的结果可复现这也是报告里必须写明的实验条件。决策树的max_depth8、min_samples_leaf10属于保守设置防止它在几千条样本上长成一棵背答案的树。如果决策树在这个设置下就明显优于线性回归证明价格与特征之间确实存在非线性关系下一节的集成模型才有意义。注意我在 log 域上报 RMSE这样误差近似理解为百分比误差比如 RMSE0.32 大约是 32% 左右的平均偏离单位口径在报告里要写清楚。3.3 集成模型参数随机森林与 XGBoost 各有三处要调基准模型跑完集成模型按固定套路设置参数。随机森林的三处关键参数是n_estimators、max_depth、min_samples_leafXGBoost 的三处是learning_rate、subsample、colsample_bytree。其余参数先不动等你看到验证集误差再决定是否增量调整。from sklearn.ensemble import RandomForestRegressor from xgboost import XGBRegressor rf RandomForestRegressor( n_estimators300, max_depth12, min_samples_leaf4, max_featuressqrt, n_jobs-1, random_state42 ).fit(X_train, y_train) xgb XGBRegressor( n_estimators500, learning_rate0.05, max_depth6, subsample0.8, colsample_bytree0.8, reg_lambda1.0, random_state42, eval_metricrmse ).fit(X_train, y_train, eval_set[(X_test, y_test)], verboseFalse)参数说明要落到“为什么”上min_samples_leaf4要求叶子节点至少 4 个样本逼模型学大规律而不是抠个别样本max_featuressqrt让每棵树只看部分特征增加树与树之间的差异方差随之下降。XGBoost 的subsample0.8是行采样每棵树只用 80% 的样本colsample_bytree0.8是列采样每棵树只用 80% 的特征两个参数共同抑制过拟合。learning_rate0.05配合n_estimators500相当于用更多棵小步长的树去逼近目标。跑完对比你会发现 XGBoost 往往比随机森林略好但如果差距在 0.01 以内报告里就诚实写“二者差异不显著”这也是一个可讨论的结论。4. 交叉验证与误差分析把实验报告的“硬结论”支撑起来建模跑完只是第一步实验报告真正值钱的部分是“你的结论有多稳”。单次划分训练测试集得到的分值具有随机性换一次random_state可能差出 0.05 的 R²。交叉验证和误差分析的目的就是消除这种随机性让你在报告里敢写“本模型的 RMSE 稳定在 X 左右”而不是“某一次跑出了 Y”。4.1 评估指标口径RMSE、MAE、R² 怎么用才不误导评估指标要在原始价格尺度上算不能只报 log 域。log 域 RMSE 听起来像百分比误差但答辩时老师更可能问“你这个模型平均差多少钱”所以要把预测结果还原回元。from sklearn.metrics import mean_squared_error, mean_absolute_error # 还原到原始价格尺度 pred_log xgb.predict(X_test) pred_price np.expm1(pred_log) true_price np.expm1(y_test) rmse_price mean_squared_error(true_price, pred_price, squaredFalse) mae_price mean_absolute_error(true_price, pred_price) print(RMSE(元):, round(rmse_price, 2)) print(MAE(元):, round(mae_price, 2))RMSE 对大误差敏感适合暴露模型在极端样本上的糟糕表现MAE 反映平均偏差更贴近业务上“每辆车平均估差多少钱”的直觉。报告中建议三个指标都列再补一句“本模型 RMSE/MAE 的比值约为 1.6说明误差中存在部分大偏差样本而非均匀分布”这句话直接引出下面的误差分析。4.2 KFold 交叉验证设置要与数据收集方式匹配单次划分之外用 KFold 交叉验证把稳定性测出来。from sklearn.model_selection import KFold, cross_val_score kf KFold(n_splits5, shuffleTrue, random_state42) scores cross_val_score(xgb, X, y, cvkf, scoringneg_root_mean_squared_error) print(每折RMSE(log域):, [-s for s in scores]) print(均值:, round(-scores.mean(), 4), 标准差:, round(scores.std(), 4))这里有个容易忽略的细节scoringneg_root_mean_squared_error返回的是负值因为 sklearn 的评分约定是“越大越好”所以要取负才能得到真正的 RMSE。shuffleTrue适用于二手车这种没有时间顺序的静态数据但如果你的数据是分批次收集、后批次的年份整体更新就要换成TimeSeriesSplit否则未来数据会泄漏进训练集交叉验证分数虚高。报告里写清楚“数据是截面数据故采用随机 KFold”是加分项。标准差是报告里最容易被忽略的一个数字如果五折 RMSE 的标准差大于均值的 10%说明模型在部分数据子集上表现不稳通常是因为某些品牌或年份的样本太少。这时候不要急着调参而是回到第 2 章检查这些样本是否被异常值处理误删。4.3 误差分析预测偏差大的样本有什么共同特征交叉验证分数稳定之后把预测误差最大的样本拉出来分析这是报告里最有“数据挖掘感”的部分。result X_test.copy() result[true_price] true_price result[pred_price] pred_price result[error] pred_price - true_price result[error_ratio] result[error] / result[true_price] # 挑出偏差超过 30% 的样本 bad result[result[error_ratio].abs() 0.3] print(偏差超30%样本数:, len(bad)) print(bad.groupby(brand_freq)[error_ratio].agg([mean, count]).head())error_ratio算的是相对误差因为 100 万的车差 10 万和 5 万的车差 1 万绝对误差相同但业务含义完全不同。跑完你会发现偏差大的样本集中在冷门品牌和老车上冷门品牌训练样本太少模型只能按品牌均值回归车龄超过 15 年的车残值低且波动大个别整备情况能差出一倍。这两条写进报告的“误差边界”一节比空泛写“模型还有改进空间”有力得多答辩老师就喜欢看你主动指出模型的失效范围。4.4 实验报告结构数据挖掘大作业的写法套路资源包里那份实验报告底层结构通常是固定的按数据挖掘的标准流程组织即可。建议的章节与对应内容如下表直接按这个骨架填充报告不可能凑不满篇幅。报告章节写什么对应处理数据探索数据集来源、字段含义、缺失率与分布图第 2.1、2.2 节的输出数据预处理缺失值填充策略、异常值删除阈值、对数变换理由第 2.2、2.3 节的代码特征工程频次编码与目标编码的选择、特征列表及业务含义第 2.3、3.1 节的表格模型构建与对比至少 3 个模型 RMSE/MAE/R² 对比表第 3.2、3.3、4.1 节的输出模型评估与误差分析KFold 稳定性、误差分布、偏差大的样本特征第 4.2、4.3 节的结论结论与展望一句话结论、当前边界、可扩展方向误差分析的延伸写作报告时要避免只贴代码截图每个结论后补一句“为什么这么做”。例如“缺失率超过 30% 的字段直接删除而非填充因为填充 30% 的假数据会引入噪声”这句话就是报告里实打实的决策理由。5. 避坑二手车价格预测最容易翻车的 5 个现场二手车价格预测的坑集中分布在数据泄漏、类别编码和目标变换三处。下面的每一条都是实操中反复出现的翻车现场按“现象 → 原因 → 解决”记录。对照你手头资源包的代码和报告发现命中同款问题时直接按方案改。5.1 训练集 R² 很高、测试集 R² 是负的现象训练集 R² 能到 0.9测试集一跑是负的比“猜均值”还差。原因最典型的是 label encoding 把无序分类变量编码成 0、1、2、3模型误以为这些数字之间有大小关系在训练集上拟合出虚假规律。另一个常见情况是目标编码在划分训练测试集之前就全量计算把测试集的价格均值泄漏给了训练集。解决分类变量一律用 one-hot、频次编码或目标编码不用 label encoding。目标编码严格限制为先拆分、再在训练集上groupby计算、最后map到测试集。写完代码后检查一个细节测试集样本里的brand_target是不是都能在训练集的映射表里找到找不到就说明泄漏路径还没堵死。5.2 预测价格出现负值现象模型跑出来的预测价格里有负数$-$5000 元这种结果明显违反常识。原因直接在原始价格上做线性回归线性模型没有非负约束当特征空间边界上出现组合值时预测值会被推到负区间。解决对价格做log1p变换后再建模预测完用expm1还原数学上保证了输出恒大于 0。这个方案的副产品是缓解了价格右偏让 RMSE 对大价格样本的过度敏感也得到抑制。5.3 交叉验证分数虚高真实测试集上暴跌现象KFold 交叉验证的 R² 稳定在 0.88但留出测试集一测只剩 0.72。原因预处理环节发生泄漏。常见泄漏路径包括在拆分前用全量数据做标准化或目标编码、用全量数据的中位数填充缺失值、把测试集样本混入了value_counts统计。这些“看一眼全量数据再动手”的操作都会让交叉验证的每一折都被污染。解决把所有预处理放进Pipeline让fit和transform严格分开。比如目标编码先fit再transform填充器先fit再transform。实在嫌 Pipeline 麻烦就在代码里保证所有mean、median、value_counts都只从训练集计算测试集只执行映射。5.4 换一次 random_state分数差出 0.05现象同一个模型random_state42时 R² 是 0.86改成 2023 变成 0.81报告里不知道填哪个。原因数据集不大、模型方差高单次划分结果受随机性影响大只报一次分数是不严谨的。解决固定random_state42并全篇统一报告里写明“所有实验在固定随机种子下进行”。同时用第 4 章的 KFold 交叉验证取均值和标准差这样单个随机种子带来的波动就被平均掉了报告里写“五折 RMSE 均值 0.84标准差 0.02”可信度远高于单次数值。5.5 XGBoost 和随机森林效果几乎一样报告里不知怎么写对比现象两个模型 RMSE 只差 0.005写在报告里显得集成模型调参毫无价值。原因二者都是树集成如果特征工程阶段没有注入足够的信息量比如目标编码没用、车龄没分箱模型差异会被特征层面的“信息天花板”压制。解决先把第 3 章的特征工程补齐再谈模型差异。如果特征完整后差距仍然很小报告就如实写“随机森林与 XGBoost 在本数据上表现接近XGBoost 仅在极端样本上略有优势”并补一张二者误差分布的对比图。承认模型相似本身就是一个经得起追问的数据挖掘结论硬编一个假优势反而容易被答辩老师问穿。6. 从“能跑”到“能答辩”特征重要性与预测可视化模型分数再高答辩时老师更爱问“模型凭什么给这辆车估这个价”。特征重要性、SHAP 解释图和预测散点图就是回答这个问题的可视化素材资源包的报告里如果这部分偏弱可以按下面的方式补齐。6.1 用特征重要性与 SHAP 解释模型决策import shap import matplotlib.pyplot as plt xgb.fit(X_train, y_train) # 特征重要性横向条形图 imp pd.Series(xgb.feature_importances_, indexfeature_cols).sort_values() imp.plot.barh() plt.tight_layout() plt.savefig(feature_importance.png, dpi150) # SHAP 摘要图解释每个特征如何推高或拉低价格 explainer shap.TreeExplainer(xgb) shap_values explainer.shap_values(X_test) shap.summary_plot(shap_values, X_test, feature_namesfeature_cols)特征重要性只告诉“哪些特征重要”SHAP 能进一步告诉“特征值变大时价格被推高还是拉低”。比如车龄的 SHAP 点图里蓝点聚集在负区间、红点聚集在正区间就说明年轻车推高价格、老年车拉低价格这正好对应第 3 章车龄分箱的业务判断。训练好的 xgb 树模型可以直接用TreeExplainer不需要额外安装依赖。6.2 预测与实际价格散点图几处必须改的默认样式fig, ax plt.subplots(figsize(6, 6)) ax.scatter(true_price, pred_price, s8, alpha0.4) max_price max(true_price.max(), pred_price.max()) ax.plot([0, max_price], [0, max_price], r--, linewidth1) ax.set_xlabel(实际价格元) ax.set_ylabel(预测价格元) ax.set_title(预测值 vs 实际值) plt.tight_layout() plt.savefig(pred_vs_true.png, dpi150)散点图里必须画那条yx红线样本点越贴近红线代表预测越准。注意对角线要按true_price和pred_price的最大值取上限否则图像会被截断看不到高价车的分布。低价区点子贴线、高价区点子发散是二手车模型的常态正好呼应第 4 章误差分析的结论。这三张图放进报告答辩时的“模型解释”环节就有了实打实的论据。我每次做完这类大作业都会留一个习惯把每次实验的模型配置、RMSE 和绘图脚本存成单独的文件命名带日期并把最终跑出报告结果的随机种子单独写在报告开头。这样答辩被问到“这个数怎么复现”时打开文件重跑一遍就是同一组数远比现场调参靠谱。二手车价格预测的这套流程从数据清洗到模型解释每一步都有成熟套路照着做一遍你对数据挖掘课程设计的理解会比只抄代码深很多。希望这篇笔记帮到你完成这次大作业。本文还有配套的精品资源点击获取