
简介这套基于Python的二手车价格数据挖掘及预测项目定位为Python课程大作业与期末课程设计适合需要高分作业范例的数据挖掘初学者。资源包含完整源码、数据集、设计报告及详细注释覆盖数据清洗、特征分析、价格建模与预测展示等环节新手对照注释即可理解逻辑并快速部署使用。压缩包共27个文件以2个py源码为核心辅以csv数据集、docx设计报告、md说明文档及多张png图表工程文件与配置齐全整体大小34.86MB。已有211人学习浏览适合期末大作业、课程设计或毕业设计参考。读者可获得一套完整的满分项目方案从数据采集与探索、二手车价格影响因素挖掘到预测模型构建与结果可视化均有代码与图文记录设计报告和README还能用于梳理答辩思路、完善项目文档。1. 二手车价格预测课程大作业从选题到交付的完整闭环“二手车价格数据挖掘及预测”能成为Python课程大作业里的常青树不是因为选题新鲜而是它把数据挖掘课的几块硬骨头——数据清洗、特征工程、模型对比、结果分析——全部塞进一个生活费级别的数据集里做出来还不难看。多数课程作业的通病是“模型调包就完事”而这个方向天然逼着你处理脏数据、解释业务字段、对齐评估指标整套流程走下来正好覆盖答辩时老师最爱问的那几个问题。这篇笔记按我做过的一套交付物拆解源码怎么组织、数据怎么洗、特征怎么做、模型怎么选、报告怎么写以及那些会让你的作业从A掉到C的隐蔽坑点。2. 数据获取与初始清洗先让你的数据集能“喂”给模型2.1 字段构成与类型检查动手之前先看清家底二手车交易数据的常见获取途径有两种直接使用公开数据集或者写爬虫抓取二手车交易平台的挂牌记录。我更推荐后者——不是因为它更高级而是因为爬下来的数据更真实字段里的“脏”才让你有东西可写进设计报告。无论来源是哪里拿到数据后的第一步是检查字段类型和缺失情况。import pandas as pd import numpy as np df pd.read_csv(car_data.csv, encodinggbk) print(样本量:, df.shape) print(字段列表:, df.columns.tolist()) # 查看字段类型和缺失率这一步决定后面清洗策略 print(df.dtypes) print(缺失率:) print(df.isnull().mean().round(4) * 100)这里要注意两个点encodinggbk是因为国内二手车平台的导出文件经常是中文 GBK 编码直接用默认的 utf-8 读会报UnicodeDecodeError缺失率查看用isnull().mean()而不是isnull().sum()因为百分比能让你快速判断是零星缺失还是整列几乎为空。整列缺失率超过 60% 的字段建议果断丢弃否则后面填充和编码都会得不偿失。一个典型的数据集包含以下字段我把它们整理成一张表方便你对照字段名含义类型典型问题brand品牌字符串取值混乱如“丰田”和“TOYOTA”混用model车型款型字符串高基数类别过多age车龄年数值可能由上牌日期字符串生成出错mileage行驶里程万公里数值存在极端值如 99 万公里displacement排量L数值缺失率较高gearbox变速箱类型字符串手动/自动/手自一体表述不一accident是否事故车0/1部分平台用“有/无”这种文本price成交价格万元数值单位不统一有的按元记录2.2 清洗流程缺失值、异常值、单位统一清洗阶段的目标不是“把数据变整洁”而是“让后续建模不被个别脏样本带偏”。我一般按三步走先处理缺失值再处理异常值最后统一单位。# 1. 缺失值中位数填充数值列众数填充字符串列 num_cols [age, mileage, displacement] str_cols [gearbox, fuel_type] for col in num_cols: df[col] df[col].fillna(df[col].median()) for col in str_cols: df[col] df[col].fillna(df[col].mode()[0]) # 2. 异常值使用 IQR 法过滤里程和价格的极端值 def filter_outliers(df, col, lower_mult1.5, upper_mult1.5): q1 df[col].quantile(0.25) q3 df[col].quantile(0.75) iqr q3 - q1 lower q1 - lower_mult * iqr upper q3 upper_mult * iqr return df[(df[col] lower) (df[col] upper)] df filter_outliers(df, price) df filter_outliers(df, mileage)关于价格和数据清理有两点个人经验供你参考。价格异常值我不用 IQR 而是手动设阈值——二手车价格的长尾分布本来就重1.5 倍 IQR 会把一些真实的高价豪车误删。常见做法是结合业务设定上下界比如删除价格低于 0.5 万或高于 200 万的记录删除里程大于 50 万公里的车。价格字段的单位问题经常被忽略有的平台按“万元”记录有的按“元”记录如果混在一起没有统一价格分布会出现双峰模型训练时 MAE 直接崩掉。建议在清洗阶段就用一个函数统一单位并记录到报告里这是答辩时能讲的细节。2.3 数据集划分与数值归一化别让你的测试集“提前偷看”很多课程作业翻车就翻在划分顺序上先对整个数据集做标准化再切训练集和测试集。这个操作属于典型的数据泄漏——测试集的均值和方差参与了训练数据的变换模型在评估时等于已经见过测试集的分布信息导致测试集指标虚高。正确做法是先切分再在训练集上拟合标准化器。from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler X df.drop(price, axis1) y df[price] # 先划分后标准化random_state 固定保证结果可复现 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) # 训练集上用 fit_transform测试集上只用 transform num_features [age, mileage, displacement] scaler StandardScaler() X_train[num_features] scaler.fit_transform(X_train[num_features]) X_test[num_features] scaler.transform(X_test[num_features])这里还有一个容易忽略的坑fit_transform和transform的区别如果不写清楚设计报告里讲不出东西。fit是在训练集上计算均值方差transform是套用这些统计量做变换。测试集只能用transform不能重新fit否则你手里的评估结果全是虚的。答辩时主动讲这个细节比背十页理论都有用。3. 数据挖掘与特征工程把“车况”翻译成模型听得懂的数值3.1 价格分布分析与目标变量变换数据挖掘里的“挖掘”两个字在课程作业层面上最直接的体现就是探索性数据分析和特征工程。很多人拿到数据就开始fit模型结果模型效果差又找不到原因。先花半小时看价格分布能帮你少走整天的弯路。import matplotlib.pyplot as plt import seaborn as sns plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False # 价格分布直方图 箱线图观察偏度和离群点 fig, axes plt.subplots(1, 2, figsize(14, 5)) sns.histplot(df[price], bins50, kdeTrue, axaxes[0]) axes[0].set_title(价格分布原始) sns.boxplot(xdf[price], axaxes[1]) axes[1].set_title(价格箱线图) plt.tight_layout() plt.show() # 对数变换后价格分布 df[price_log] np.log1p(df[price]) sns.histplot(df[price_log], bins50, kdeTrue) plt.title(价格分布对数变换后) plt.show()二手车价格几乎必然呈现严重右偏几万块的车占大头几十万的豪车是长尾。直接拿原始价格做回归模型会把绝大部分权重用于拟合高价位样本导致中低价位段的预测误差很大。np.log1p是对数变换的常用实现它的好处是当价格接近 0 时避免出现负无穷。训练时用price_log作为目标变量预测完成后再用np.expm1还原成价格。这一步在报告里可以写成“目标变量分布分析与变换”是数据挖掘过程中一个完整的分析节点。3.2 数值特征与衍生特征把业务常识变成特征特征工程不是把每个字段糊到模型里就完事而是把“一个懂二手车的人会怎么评估价格”翻译成数值。比如同一个里程数放在 3 年车龄和 10 年车龄的车上有完全不同的意义单纯把mileage和age作为两个独立特征模型很难学到这层交互关系。一个简单有效的办法是构造“年均里程”# 衍生特征年均里程 总里程 / 车龄 df[mileage_per_year] df[mileage] / (df[age] 0.5) # 排量与价格的交互特征大排量车通常更贵但也分豪华品牌和平民品牌 df[displacement_times_brand_rank] df[displacement] * df[brand_rank] # 车龄分箱3年/5年/10年是二手车的几个心理价位节点 df[age_bin] pd.cut(df[age], bins[0, 3, 5, 10, 100], labels[0, 1, 2, 3])mileage_per_year这个特征能区分“两年开了 8 万公里的营运车”和“八年开了 8 万公里的家用车”后者价格明显更高。age_bin分箱的价值在于让树模型更容易切分出“准新车”“次新车”这样的价格带避免对连续年龄做过多细分。displacement_times_brand_rank则需要先给品牌按溢价能力排序编码这个编码过程本身就要结合业务理解写进报告比堆数据有价值得多。3.3 分类型特征编码高基数类别怎么处理不会翻车类别特征的编码方式直接决定模型的上限。常见的选择有OneHotEncoder、LabelEncoder和TargetEncoder。对于品牌、车型这样动辄几十上百个类别的字段直接 OneHot 会把特征矩阵撑得非常稀疏线性模型扛得住但训练速度慢树模型切分效率也低。我个人的常见做法是类别少小于 8 类用 OneHot类别多但有序如排量级别用 LabelEncoder类别多且无序如品牌用频率编码或者目标编码。from sklearn.preprocessing import LabelEncoder # 低基数类别OneHot 编码 df pd.get_dummies(df, columns[gearbox, fuel_type], drop_firstTrue) # 高基数类别频率编码——用类别出现次数代表其“常见程度” brand_counts df[brand].value_counts() df[brand_freq] df[brand].map(brand_counts) # 替代方案目标编码——用该品牌历史平均价格编码 brand_mean_price df.groupby(brand)[price].transform(mean) df[brand_target] brand_mean_price频率编码和标签编码的区别值得注意LabelEncoder会给类别随机分配整数这在树模型里没问题但放到线性模型里就等于强加了一个不存在的大小关系。频率编码则把“常见程度”作为特征值——常见品牌的车通常保有量大、维修成本低、保值率稳定这是一个有真实业务含义的信号。目标编码效果通常更好但要注意用交叉验证拟合来防止过拟合否则训练集上表现好得离谱测试集立刻露馅。4. 模型构建与预测从线性基线到集成模型的选型与调参4.1 评估指标与基线模型先定尺子再干活模型选型之前先想清楚用什么指标评估。二手车价格预测属于回归问题最常用的三个指标是 MAE、RMSE 和 R²。MAE 直接对应“预测价格平均差多少钱”最容易被非技术听众理解RMSE 对大误差更敏感适合关注极端偏差的场景R² 则用来衡量模型对价格方差的解释程度。我的建议是课程作业里主报 MAE 和 R²MAE 写在摘要里R² 留在模型对比表中。先跑一个线性回归作为基线后续所有模型都至少要超过它否则说明特征工程出了大问题。from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_absolute_error, r2_score # 极简基线模型不调任何参数只看特征工程的效果 model LinearRegression() model.fit(X_train, y_train) y_pred model.predict(X_test) mae mean_absolute_error(y_test, y_pred) r2 r2_score(y_test, y_pred) print(f线性回归 MAE{mae:.4f} R2{r2:.4f})线性回归的预测结果能立刻暴露问题如果 MAE 高得离谱先检查是否忘了做对数变换还原或者在训练时用了包含price_log的列作为特征。常见翻车现场是把标准化后的目标变量又放回特征矩阵里导致模型直接用答案预测答案训练集 R² 接近 1这种情况在答辩时一问一个准。4.2 随机森林与 XGBoost集成模型的参数怎么设以我做过几次课程设计的经验随机森林在这类表格数据上是最稳的选择不用做太多预处理、抗过拟合能力强、还能输出特征重要性用于报告分析。XGBoost 通常效果更好但调参复杂度高而且在小样本数据集上容易过拟合。课程作业场景下我会两个都跑用随机森林保底用 XGBoost 冲指标。from sklearn.ensemble import RandomForestRegressor rf RandomForestRegressor( n_estimators500, max_depth15, min_samples_leaf2, max_featuressqrt, random_state42, n_jobs-1 ) rf.fit(X_train, y_train) y_pred_rf rf.predict(X_test) print(f随机森林 MAE{mean_absolute_error(y_test, y_pred_rf):.4f} fR2{r2_score(y_test, y_pred_rf):.4f})这些参数不是随便填的它们各自对应一个实际问题n_estimators500足够让集成效果收敛再大收益有限但训练时间翻倍max_depth15在特征几十个的情况下够用太深容易让模型记住训练集中的噪声min_samples_leaf2强制叶节点至少有两个样本防止树把个别离群样本学进去max_featuressqrt是回归任务中随机森林的常见选项增加树之间的多样性。关于随机森林我个人的习惯是优先固定random_state让每次运行结果可复现。发散地说一句模型效果不理想时问题通常在特征工程而不是参数——调参只能微调不能点石成金。4.3 交叉验证与网格搜索在训练集内部选参数在测试集上反复试参数是大忌——一旦你用测试集反馈去调参测试集就变成了训练集的一部分评估结果等于作弊。正确做法是在训练集内部切出验证集或者直接使用交叉验证。from sklearn.model_selection import cross_val_score # 5折交叉验证每折验证一次取平均 cv_mae cross_val_score( rf, X_train, y_train, cv5, scoringneg_mean_absolute_error ) print(f5折交叉验证 MAE: {-cv_mae.mean():.4f})交叉验证后如果要进一步调参还有一个更省事的办法GridSearchCV网格搜索虽然计算量大但能把“选参数”这件事自动化结果也能直接写进报告。不过课程设计的数据量有限网格搜索过碎反而容易选到过拟合参数——我的习惯是把候选参数控制在 2×2×2 的范围内只调max_depth、min_samples_leaf和n_estimators找到结果稳定后就不再追求极限。交叉验证后如果训练集 R² 和验证集 R² 差距大于 0.1说明过拟合严重先回去加正则或减特征不要急着堆模型。5. 训练避坑二手车价格预测中最容易翻车的 5 个典型坑5.1 数据泄漏归一化写在整个数据集上测试指标虚高现象模型在测试集上 R² 达到 0.95训练集也接近 0.95但换一批新数据预测完全不准。原因对全量数据做了标准化或对数变换后才划分训练测试集。测试集的分布信息进入了训练过程模型相当于提前“背过答案”。这在二手车价格预测里尤其隐蔽——价格字段一旦参与全量变换泄漏几乎是必然。解决严格按“先划分、后变换、测试集只用 transform”的流程执行。代码层面强制用Pipeline封装预处理和模型避免中间步骤遗漏。在报告中解释这一设计还能作为亮点展示对数据泄漏问题的理解。5.2 价格单位不统一模型学到“万元车”和“元车”两个世界现象价格分布直方图出现两个簇一个在 5-15 附近另一个在 50000-150000 附近模型预测时出现系统性偏差。原因数据源中部分记录价格单位是“万元”部分记录是“元”。同类车型价格相差一万倍模型被迫拟合一个极不连续的映射。解决清洗阶段统一价格字段万元全部换算为元或反之并用业务阈值过滤明显异常值——例如价格小于 1000 元或大于 500 万元属于异常记录。检查方法很简单df[price].nunique()如果出现大量重复且呈现 10000 倍关系基本可以确定单位不统一。5.3 车龄特征构造错误字符串日期直接参与计算现象特征矩阵出现负数车龄或者车龄全部是整数但精度极差模型对老车价格预测波动巨大。原因用pd.to_datetime解析上牌日期后没有提取年份差值而是把datetime对象直接传给模型或者在不同年份抓取的数据混在一起没有统一计算基准日。解决统一用固定截止日期计算车龄例如# 以统一日期为基准计算车龄避免数据抓取时间不同造成偏差 reference_date pd.Timestamp(2024-12-31) df[age] reference_date.year - pd.to_datetime(df[reg_date]).dt.year5.4 高基数类别编码后特征爆炸稀疏矩阵拖垮线性模型现象OneHot 编码后特征数量从 30 涨到 5000线性模型训练时间拉长十倍且预测结果对微小参数变化极其敏感。原因车型字段有上千个取值每个取值都生成一列哑变量大量样本在这些列上都是 0模型学到的主要是稀疏矩阵里的噪声。解决对高基数字段改用频率编码或目标编码或者先按车型销量/价格中位数做层次聚类把上千个车型收敛到几十个“车型级别”再编码。这一步的决策过程值得写进设计报告——比调参有意义得多。5.5 随机森林的极端价格预测失效高价车总是被低估现象测试集中价格高于 50 万的车预测值普遍偏低 15%-30%低价车的预测相对准确。原因训练集中高价位样本占比过低树模型在分裂时几乎没有机会学习高价车的组合特征。这是回归树的通病——它对训练数据的分布尾部天然不敏感。解决对目标变量做log1p变换后再训练预测后expm1还原或者按价格分层抽样确保训练集中每个价位段都有足够的样本量。第二种做法在课程报告中解释起来更吃力但对于提升高价车预测精度有颇直接的帮助——个人经验来看效果确实明显。6. 设计报告与答辩让老师看到的不只是“调包”而是“调包思路”一份能拿高分的课程设计报告关键不是厚而是结构对。我一般按六个部分组织问题定义与业务背景、数据来源与清洗、数据分布分析、特征工程策略、模型选型与对比、结论与展望。每个部分都对应一个“为什么要这样做”的回答而不是贴代码截图。代码放在附录里正文只留关键结果。答辩时老师最常问的问题就三类为什么选这个模型怎么解决过拟合特征怎么来的模型选型问题用“对比实验”回答——线性回归、随机森林、XGBoost 三者的 MAE/R² 摆出来说明为什么随机森林适合中等规模表格数据、对离群值鲁棒、且特征重要性可解释。过拟合问题用“交叉验证 训练集/验证集误差差距”回答。特征来源问题用业务逻辑回答——年均里程比纯里程更有预测力因为它区分了营运车和家用车。源码注释是很多人偷懒的地方但恰恰是拉开差距的地方。我的习惯是每个函数前用三行 docstring 写清楚“输入是什么、输出是什么、为什么这样做”而不是解释每个 pandas 函数是什么意思。老师翻源码时看的不是语法而是你能不能把自己的设计决策说清楚。另外注意项目里不要出现模型文件路径写死、中文字体路径依赖本机环境这类问题换一台机器就报错会让整个项目质量大打折扣——这是我吃过亏的地方希望帮到你至少别在交付前夜才发现代码在别人机器上跑不起来。本文还有配套的精品资源点击获取