ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

二手车价格预测数据挖掘大作业:从源码到实验报告的完整解析

2026/9/27 23:42:55 拓冰建站 浏览量
二手车价格预测数据挖掘大作业:从源码到实验报告的完整解析 简介这是一份面向计算机相关专业学生的数据挖掘课程大作业资源以二手车价格预测为实战主题适合作为课程设计、期末大作业或自学练习的完整案例。项目经导师指导并评审通过得分98分内容覆盖数据预处理、特征工程、模型构建与评估全流程涉及线性回归、决策树、随机森林等算法的实现与对比。资源包共27个文件约34.86MB包含2个Python源码文件、1个CSV数据集、1份DOCX实验报告以及9张PNG结果图、8个XML配置文件和项目说明文档等源码与报告均附有详尽注释便于理解每一步操作意图。数据集模拟真实市场数据涵盖品牌、车型、使用年限、行驶里程、车况等多维特征能反映价格与各因素间的复杂关系。目前已有69人学习读者可借此掌握数据清洗、特征选择、模型训练调优与验证的完整流程积累从问题定义到结果输出的实战经验。1. 二手车价格预测数据挖掘大作业从一份带注释的 Python 源码说起二手车价格预测是数据挖掘课程里最经典的回归类大作业之一它把数据清洗、特征工程、模型调参与结果解释串成一条完整链路几乎覆盖了数据挖掘实验的全部考核点。很多人拿到「二手车价格预测数据挖掘课程大作业Python源码及数据集与实验报告详尽注释.zip」这类资源时第一反应是直接跑通看准确率结果发现换一份数据集就崩或者报告里的结论根本复现不出来。问题不在代码本身而在于没有理解这套流程里每一步为什么这么设计。这篇笔记面向正在做数据挖掘大作业的学生和刚转行做数据分析的从业者把二手车价格预测从数据读取、特征构造、模型选型到实验报告撰写拆开讲清楚让你拿到源码后能改、能调、能解释而不是只会按运行键。数据集通常包含品牌、车型、上牌年份、行驶里程、排量、变速箱、燃油类型、所在城市和成交价等字段目标就是预测成交价这个连续变量。2. 二手车价格预测的数据集结构与特征工程怎么做2.1 先搞清楚字段类型再动手清洗拿到数据集后不要急着df.dropna()先按字段类型分类处理。二手车数据一般分三类数值型价格、里程、排量、车龄、类别型品牌、车型、变速箱、燃油类型、城市、文本型标题描述、配置说明。数值型看分布和异常值类别型看基数文本型看是否需要提取关键词。import pandas as pd import numpy as np df pd.read_csv(used_car.csv, encodingutf-8) # 先看整体信息不要跳过这一步 print(df.info()) print(df.describe()) # 按类型分组查看 num_cols [price, mileage, displacement, age] cat_cols [brand, model, gearbox, fuel_type, city] # 数值型看偏度和异常值 for col in num_cols: print(f{col}: skew{df[col].skew():.2f}, fmin{df[col].min()}, max{df[col].max()}) # 类别型看基数基数超过50的慎用独热编码 for col in cat_cols: print(f{col}: {df[col].nunique()} unique values)这段代码的逻辑是先建立字段分类意识再分别处理。skew()看偏度价格和里程通常右偏严重后续可能需要对数变换。nunique()看类别基数品牌可能只有几十个但车型可能有几百个车型直接独热编码会导致维度爆炸常见做法是保留高频类别、低频归为「其他」或者用目标编码。参数上注意encoding要根据实际文件调整中文数据集常见gbk或utf-8-sig如果价格字段带「万」字或货币符号需要先做字符串清洗再转数值。2.2 车龄和里程的组合特征比单独用更有信息量二手车定价的核心逻辑是「年份新、里程少、价格高」但车龄和里程单独作为特征时模型很难自动捕捉它们的交互关系。手动构造「年均行驶里程」这个组合特征往往比把两个原始特征丢给模型效果更好。# 从上牌日期计算车龄 df[reg_date] pd.to_datetime(df[reg_date], errorscoerce) df[age] 2024 - df[reg_date].dt.year # 年均里程总里程除以车龄车龄为0时设为1避免除零 df[age_safe] df[age].apply(lambda x: max(x, 1)) df[mileage_per_year] df[mileage] / df[age_safe] # 价格取对数缓解右偏 df[log_price] np.log1p(df[price]) # 品牌保值率用品牌中位数价格除以整体中位数 brand_median df.groupby(brand)[price].median() overall_median df[price].median() df[brand_retention] df[brand].map(brand_median) / overall_medianmileage_per_year这个特征的意义在于区分「老车但开得少」和「新车但开得多」两种情况前者通常车况更好、价格更高。brand_retention是目标编码的一种简化形式用品牌中位价与整体中位价的比值衡量保值能力注意这个特征在训练集上计算后要应用到测试集不能把测试集信息泄露进训练。注意目标编码类特征必须在交叉验证的每一折内部重新计算否则会造成标签泄露线下评分虚高但线上翻车。2.3 缺失值处理要分字段讨论不能一刀切二手车数据里缺失值很常见排量、变速箱、燃油类型都可能缺。不同字段的缺失含义不同排量缺失可能是新能源车变速箱缺失可能是信息未录入价格缺失的样本必须直接删除因为它是标签。# 价格缺失的样本直接删这是标签不能补 df df.dropna(subset[price]) # 排量缺失先看是否与燃油类型相关 print(df[df[displacement].isna()][fuel_type].value_counts()) # 如果缺失集中在新能源用0填充并加一个缺失标记 df[displacement_missing] df[displacement].isna().astype(int) df[displacement] df[displacement].fillna(0) # 类别型缺失用众数填充 for col in [gearbox, fuel_type]: df[col] df[col].fillna(df[col].mode()[0])加displacement_missing标记列是一个实用技巧让模型自己学习「缺失」这件事是否有信息量。如果实验发现这个标记列重要性很低再删掉也不迟。类别型用众数填充的前提是缺失比例不高一般低于10%如果某个字段缺失超过30%考虑直接丢弃该字段或把它当作一个独立类别「未知」。3. 模型选型与训练从线性回归到梯度提升的对比实验3.1 基线模型先用线性回归把流程跑通很多人一上来就上 XGBoost结果调参调到怀疑人生却不知道数据本身有没有问题。正确做法是先用一个简单的线性回归建立基线确认数据管道没问题再逐步换复杂模型。from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.preprocessing import StandardScaler, OneHotEncoder from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline from sklearn.metrics import mean_absolute_error, r2_score # 特征列划分 num_features [age, mileage, mileage_per_year, displacement, brand_retention] cat_features [brand, gearbox, fuel_type, city] # 预处理管道数值标准化类别独热 preprocessor ColumnTransformer([ (num, StandardScaler(), num_features), (cat, OneHotEncoder(handle_unknownignore, max_categories20), cat_features) ]) # 线性回归基线 lr_pipe Pipeline([ (prep, preprocessor), (model, LinearRegression()) ]) X df[num_features cat_features] y df[log_price] X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.2, random_state42) lr_pipe.fit(X_train, y_train) y_pred lr_pipe.predict(X_test) print(fMAE: {mean_absolute_error(y_test, y_pred):.4f}) print(fR2: {r2_score(y_test, y_pred):.4f})ColumnTransformer把数值和类别分开处理避免对类别做标准化这种无意义操作。OneHotEncoder的handle_unknownignore保证测试集出现训练集没见过的类别时不报错max_categories20把低频类别合并控制维度。注意这里预测的是log_price评估指标要在对数尺度上看最后再np.expm1还原到原始价格。3.2 梯度提升树为什么在表格数据上通常更强二手车价格和特征之间往往是非线性关系车龄对价格的影响不是线性的前三年贬值快、后面趋缓里程和价格的关系也类似。线性回归捕捉不到这种非线性而梯度提升树可以自动做特征交叉和分段拟合。from xgboost import XGBRegressor from sklearn.model_selection import cross_val_score xgb_pipe Pipeline([ (prep, preprocessor), (model, XGBRegressor( n_estimators500, max_depth6, learning_rate0.05, subsample0.8, colsample_bytree0.8, reg_alpha0.1, reg_lambda1.0, random_state42 )) ]) # 五折交叉验证比单次划分更可靠 cv_scores cross_val_score(xgb_pipe, X_train, y_train, cv5, scoringneg_mean_absolute_error) print(fCV MAE: {-cv_scores.mean():.4f} (/- {cv_scores.std():.4f}))参数含义逐个说清楚n_estimators500是树的数量配合learning_rate0.05使用学习率低就需要更多树max_depth6控制单棵树复杂度太深容易过拟合subsample0.8和colsample_bytree0.8是行采样和列采样增加模型多样性、抑制过拟合reg_alpha和reg_lambda是 L1 和 L2 正则项。这组参数不是最优解但作为起点比较稳建议先用交叉验证跑一遍再根据 CV 曲线调整。3.3 特征重要性分析帮你判断模型是否学到了合理的东西模型跑出高分不代表学到了正确的东西。如果特征重要性显示「城市」比「车龄」还重要那大概率是数据泄露或者某个类别特征编码有问题。import matplotlib.pyplot as plt # 训练完整模型后提取特征重要性 xgb_pipe.fit(X_train, y_train) model xgb_pipe.named_steps[model] # 获取独热编码后的特征名 ohe xgb_pipe.named_steps[prep].named_transformers_[cat] cat_names ohe.get_feature_names_out(cat_features) all_names num_features list(cat_names) importance pd.Series(model.feature_importances_, indexall_names) importance.sort_values(ascendingFalse).head(15).plot(kindbarh) plt.tight_layout() plt.savefig(feature_importance.png, dpi150)看重要性时重点关注三件事第一车龄和里程是否排在前列如果不是检查这两个字段是否被正确解析第二brand_retention这个构造特征是否有效如果重要性接近零说明品牌信息已经被独热编码覆盖了第三有没有某个独热编码出来的单一类别重要性异常高那可能是数据泄露的信号。4. 实验报告怎么写才不像流水账4.1 报告结构要围绕「问题-方法-证据」展开课程大作业的实验报告不是代码说明书老师想看的是你有没有分析思路。建议按这个结构组织问题定义与数据说明、探索性分析发现、特征工程方案与理由、模型对比实验、误差分析与改进方向。每一部分都要有图表支撑不能只有文字描述。探索性分析部分至少放三张图价格分布直方图说明为什么要取对数、车龄与价格的散点图说明非线性关系、品牌与均价箱线图说明品牌特征的必要性。这些图不只是装饰它们是你后续特征工程决策的依据。4.2 模型对比要用统一评估口径对比不同模型时必须用同一份训练集和测试集划分评估指标统一用 MAE 和 R2。如果预测的是对数价格报告里要同时给出对数尺度和原始价格尺度的指标否则读者无法直观判断误差大小。模型MAE对数尺度R2对数尺度MAE原始价格/元线性回归0.280.7218500随机森林0.210.8313200XGBoost0.180.8710800表格里的数字要根据你自己的实验结果填写不要照抄。重点是展示模型迭代的过程和每一步的改进幅度而不是只放一个最终结果。4.3 误差分析比追求高分更能体现水平挑出预测误差最大的十个样本逐个看它们的特征往往能发现数据问题或模型盲区。比如某辆车的实际价格远低于预测值一看是事故车但数据集里没有事故记录字段这就是数据本身的局限写进报告的「不足与改进」部分比硬调参提分更有价值。# 找出预测误差最大的样本 results X_test.copy() results[actual] np.expm1(y_test) results[predicted] np.expm1(y_pred) results[abs_error] abs(results[actual] - results[predicted]) top_errors results.nlargest(10, abs_error) print(top_errors[[brand, age, mileage, actual, predicted, abs_error]])这段代码帮你定位问题样本分析时关注误差大的样本是否集中在某个品牌或某个价格区间是否有特征值明显异常预测值是否系统性地偏高或偏低。这些发现直接对应报告里的改进方向。5. 避坑与排查二手车价格预测大作业里最容易翻车的五个地方5.1 价格字段带单位导致模型训练报错现象ValueError: could not convert string to float或者模型训练出来 R2 是负数。原因价格字段里混有「3.5万」「12.8万」这类字符串pd.read_csv默认按 object 读取直接丢给模型就报错。解决读取后先做字符串清洗用正则提取数字再乘单位。df[price] df[price].str.replace(万, ).astype(float) * 10000处理完用df[price].dtype确认类型。5.2 上牌日期格式不统一导致车龄计算出错现象车龄字段出现负数或者超过30的异常值。原因日期格式有「2018-05」「2018/05/01」「2018年5月」多种写法pd.to_datetime解析失败后返回 NaT减法运算产生错误结果。解决用pd.to_datetime(df[reg_date], formatmixed, errorscoerce)统一解析解析失败的样本单独检查。计算车龄后用df[age].describe()确认范围合理异常值手动核查或剔除。5.3 独热编码后训练集和测试集列数不一致现象ValueError: feature_names mismatch或者预测时维度对不上。原因训练集和测试集分别做了独热编码测试集里某个类别在训练集没出现导致列数不同。解决用Pipeline把编码器和模型绑在一起先fit训练集再predict测试集OneHotEncoder设置handle_unknownignore。不要手动分别编码再拼接。5.4 交叉验证时数据泄露导致线下分数虚高现象交叉验证 MAE 很低但换一份测试集效果差很多。原因在划分训练测试集之前就做了全局的缺失值填充或目标编码测试集信息泄露进了训练过程。解决所有涉及统计量的操作均值填充、目标编码、标准化都必须放在Pipeline里在交叉验证的每一折内部独立计算。用cross_val_score配合Pipeline可以自动处理这个问题。5.5 报告里的结论和代码实际输出对不上现象报告写「XGBoost 比线性回归 MAE 降低 30%」但代码跑出来只降了 10%。原因调参过程中改过代码但忘了同步更新报告或者报告里的数字是某次偶然运行的结果没有用固定随机种子复现。解决所有实验设置random_state报告里的每个数字都要能通过运行对应脚本复现。建议在报告附录里附上关键代码片段和运行环境说明Python 版本、主要库版本。6. 把这份大作业变成可复用的回归建模模板做完二手车价格预测之后最有价值的动作不是交完作业就丢而是把这套流程抽象成一个可复用的回归建模模板。我自己后来做房价预测、设备残值评估、保险定价这些任务时基本都是从这个模板改出来的省了大量重复劳动。具体做法是把代码拆成四个独立模块data_loader.py负责读取和初步清洗feature_engineer.py负责特征构造model_trainer.py负责模型训练和交叉验证evaluator.py负责指标计算和误差分析。每个模块之间用 DataFrame 传递数据接口保持稳定。这样换数据集时只需要改data_loader和feature_engineer模型和评估部分基本不用动。# model_trainer.py 的核心接口设计 class RegressionTrainer: def __init__(self, preprocessor, model, cv5): self.pipe Pipeline([(prep, preprocessor), (model, model)]) self.cv cv def train(self, X, y): scores cross_val_score(self.pipe, X, y, cvself.cv, scoringneg_mean_absolute_error) self.pipe.fit(X, y) return -scores.mean() def predict(self, X): return self.pipe.predict(X) def feature_importance(self, feature_names): model self.pipe.named_steps[model] return pd.Series(model.feature_importances_, indexfeature_names)这个类把交叉验证、训练和预测封装在一起换模型只需要传入不同的model对象。feature_importance方法要求模型有feature_importances_属性线性回归没有这个属性可以改成看系数绝对值或者用permutation_importance做模型无关的重要性分析。验证模板是否好用的方法很简单找一份新的回归数据集只改数据加载和特征工程部分看能不能在半天内跑出一版合理的结果。如果超过半天说明模块耦合太紧需要继续拆。我自己的习惯是每做完一个项目就往模板里沉淀一点现在这个模板已经覆盖了缺失值处理、类别编码、数值变换、模型对比和误差分析五个环节新项目上手基本就是填空。还有一个容易被忽略的点把每次实验的配置和结果记录下来。不需要多复杂的工具一个 CSV 文件就够字段包括实验编号、特征版本、模型参数、CV MAE、测试集 MAE、备注。这样当你想回头对比「上个月那版特征到底有没有用」时不用翻聊天记录和零散脚本。这个习惯帮我省了很多后悔药希望你也能用上。希望帮到你。本文还有配套的精品资源点击获取