简介:一份可直接运行的二手车价格预测Python项目,面向期末大作业、课程设计及机器学习入门实战人群。项目覆盖LightGBM、支持向量机回归、CNN、多元线性回归等主流模型,并配有实际二手车交易数据集,可完整体验数据预处理、特征交叉、交叉验证与模型优化的完整流程。压缩包共19个文件,以5个Python脚本、9个CSV数据文件为主,另有Notebook笔记、训练好的h5模型、npy权重及说明文档,整体大小约55.92MB。源码均经过本地编译和严格调试,含优化前后对比代码、特征交叉与十折交叉验证实现、标准化数据等细节,便于逐模块学习与二次开发。已有196人学习,适合需要完整项目参考或提升建模能力的Python学习者。
1. 机器学习二手车价格预测:一份把四类模型跑通的期末大作业源码
二手车定价在机器学习里是标准的回归问题,但把真实交易数据喂进模型后,情况往往和课本示例不太一样。这份基于机器学习的二手车价格预测及应用实现源码+数据集(Python期末大作业),是一份本地编译通过、评审得分98分的完整项目,包含多元线性回归、SVR支持向量机回归、lightGBM、CNN四条技术路线的可运行代码,以及原始特征表、训练集、测试集和每个模型的预测结果CSV。它不是调了单个模型的Demo,而是把特征工程、模型对比、交叉验证整个链条都走完的闭环。对正在找期末大作业参考的计算机专业学生,和想练手价格类预测实战的机器学习入门者,都能直接照着复现。我拆这个项目时,重点盯四个问题:数据怎么准备的、四个模型各自的实现差异在哪、优化前后精度能差多少、哪个环节最容易翻车。下面逐个说。
2. 数据准备与特征工程:从原始交易表到四套模型可用的CSV
2.1 used_car_train_20200313.csv 里的原始字段长什么样
这份资源里所有模型的输入源头,都指向used_car_train_20200313.csv这一张二手车交易数据表。这类数据常见的特点是:包含车辆的品牌车系、上牌年份、表显里程、排量、变速箱类型、过户次数、排放标准等字段,目标列就是成交价格。字段之间量纲差异极大——里程可能到十万公里级,车龄是个位数,排量在一到三之间,这就决定了后续所有模型必须区分对待原始特征。
打开表之后第一件事不是建模,而是检查缺失值和数据类型。常见做法是跑一句df.info()看每列的非空数量,再对数值列画分布。这个项目把训练数据单独放了一张表,没有直接给出测试集原始表,而是给出了两个已经加工好的lgb_train_final.csv和lgb_test_final.csv,说明预处理是在建模之前独立完成的。先厘清文件关系再动手,能避免后面拿着测试集当训练集反复调参的尴尬。
这个 csv 本身没有做二次脱敏,字段名基本可读。处理时我一般先把所有列名统一成小写并替换掉空格和特殊符号,防止后续 lightGBM 或 pandas 在特征名上出幺蛾子。原始表里如果存在大量重复的同类车型记录,价格分布会明显右偏,这一点会在第 6 章验证环节展开说,但预处理阶段就值得留意。
2.2 相似度较高的特征.txt:建模前先标记共线性特征
项目里有一份相似度较高的特征.txt,这是预处理阶段的关键产出。它的作用是把相关系数偏高的特征对提前挑出来,避免线性模型学出不稳定系数。为什么这一步重要?因为多元线性回归的闭式解里含有(X^T X)^-1,当两个特征高度相关时矩阵接近奇异,求逆结果会剧烈震荡,权重符号甚至会和业务直觉相反。lightGBM 这类树模型对共线性不敏感,但线性回归和 SVR 对它是敏感的。
生成这份文本的代码逻辑很简单,就是遍历数值列两两算 Pearson 相关系数,把绝对值超过阈值的特征对写进文件:
import pandas as pd df = pd.read_csv('used_car_train_20200313.csv') corr = df.corr(numeric_only=True) pairs = [] for i in range(len(corr.columns)): for j in range(i + 1, len(corr.columns)): if abs(corr.iloc[i, j]) >= 0.8: pairs.append((corr.columns[i], corr.columns[j], round(corr.iloc[i, j], 3))) with open('相似度较高的特征.txt', 'w', encoding='utf-8') as f: for col1, col2, r in pairs: f.write(f'{col1} <-> {col2}: {r}\n')阈值取 0.8 是通用做法,超过这个数说明两个变量携带的信息高度重叠。corr(numeric_only=True)是为了跳过非数值列,否则计算会直接报错或把类别列忽略掉。如果数据量在几万行以内,这个双重 for 循环不会慢,可以放心用。输出文件只作标记用途,后续到底是删掉一列、做交叉还是保留原样,取决于模型类型——我在第 4 章优化部分会展开这一点。
2.3 final版与标准化版CSV:三套中间产物的分工
lgb_train_final.csv和lgb_test_final.csv是给 lightGBM 直接吃的训练集和测试集,列已经过筛选,剔除了明显无用的文本型字段,长度约在十几到几十个特征之间。linear_regression_standardize_data.csv则是给线性回归准备的标准化特征矩阵,因为线性模型要求输入量纲统一。w_data.npy是线性回归训练完成后的权重向量缓存,用 numpy 的np.save存下来的,下次预测时直接加载,不用重新训练。
这三类文件的分工逻辑是:树模型容忍原始量纲,线性模型必须标准化,最终预测结果全部落成独立的 finalcsv 方便横向比对。切分训练测试集和标准化的核心代码如下:
from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler features = [c for c in df.columns if c != 'price'] X = df[features] y = df['price'] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) scaler = StandardScaler() X_train_s = scaler.fit_transform(X_train) X_test_s = scaler.transform(X_test)test_size=0.2表示拿 20% 的数据做测试,random_state=42固定随机种子,保证每次跑出来的切分结果一致,也方便和项目原始输出对比。注意scaler.fit_transform只能用在训练集上,测试集必须复用同一个 scaler 做transform,否则均值方差不一致,等于把测试集信息泄漏进了预处理流程,这在期末答辩时是会被扣分的点。
3. 四类模型逐个跑通:线性回归、SVR、lightGBM、CNN
3.1 多元线性回归代码.py:numpy闭式解与w_data.npy
多元线性回归在这个项目里承担的是基线模型角色,代码文件里的实现方式不是 sklearn 的LinearRegression,而是直接用 numpy 解正规方程,这种做法在教学场景里更直观,也方便把权重存成 npy 文件。
import numpy as np # X_train_s 是标准化后的特征矩阵,前插一列 1 作为偏置项 X_design = np.hstack([np.ones((X_train_s.shape[0], 1)), X_train_s]) # 闭式解:w = (X^T X)^-1 X^T y w = np.linalg.inv(X_design.T @ X_design) @ X_design.T @ y_train np.save('w_data.npy', w)np.ones那一列对应截距项,使得偏置被当作普通权重一起学习,不需要单独处理。@是矩阵乘法运算符,(X^T X)^-1在特征数少于几百时可以直接求逆,特征数一旦上千就建议改用np.linalg.pinv伪逆或梯度下降,否则矩阵求逆的时间和数值稳定性都会变差。
预测时要把测试集同样标准化,然后前插一列 1,再和w做点积。这套流程的边界在于:如果第 2 章标记出的高相关特征对没有被处理,X^T X接近奇异,权重会变得极大且符号怪异。所以这个模型能不能跑出合理结果,完全取决于预处理阶段有没有认真对待共线性。
3.2 张立静-SVR作业代码:为什么SVR必须吃标准化数据
SVR 全称支持向量回归,核心思路是找到一个回归函数,让大多数样本落在 ε 不敏感带内。项目里的张立静-SVR作业代码.ipynb和同名 py 文件就是这条路线,产出结果写在svr_final(1).csv里。
from sklearn.svm import SVR from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_train_s = scaler.fit_transform(X_train) X_test_s = scaler.transform(X_test) svr = SVR( kernel='rbf', C=100.0, epsilon=0.1, gamma='scale' ) svr.fit(X_train_s, y_train)kernel='rbf'是默认选择,适合特征和目标之间呈非线性关系的情况;C是误分类惩罚系数,值越大越不愿意容忍训练误差,但过大容易过拟合;epsilon定义了不敏感带的宽度,控制回归精度;gamma='scale'表示按1 / (n_features * X.var())自动计算 RBF 核的宽度,适合对特征尺度没有先验经验的场景。
为什么 SVR 必须吃标准化数据?因为 RBF 核计算的是样本间的欧氏距离,里程这个特征动辄十万,车龄只有个位数,如果不标准化,距离完全被里程主导,车龄、排量这些特征的效果会被淹没。这是 SVR 和树模型最大的区别。另一个实际问题是 SVR 训练复杂度接近 O(n²) 到 O(n³),样本量过万后非常慢,我拆这个项目时发现它适合做对比实验但不太适合直接上生产。
3.3 lightGBM模型优化前模型代码.py:默认参数下的基线精度
lightGBM 是这份资源里实际预测效果最好的模型,优化前的代码文件对应一个基础的 GBDT 配置,目的是先拿到一个可对比的基线分数。
import lightgbm as lgb params = { 'objective': 'regression', 'metric': 'rmse', 'boosting_type': 'gbdt', 'learning_rate': 0.05, 'num_leaves': 31, 'max_depth': -1, 'feature_fraction': 0.8, 'bagging_fraction': 0.8, 'bagging_freq': 1, 'verbose': -1, 'seed': 42 } lgb_train = lgb.Dataset(X_train, y_train) lgb_valid = lgb.Dataset(X_test, y_test, reference=lgb_train) model = lgb.train( params, lgb_train, num_boost_round=1000, valid_sets=[lgb_valid], callbacks=[lgb.early_stopping(stopping_rounds=50)] )objective='regression'对应 MSE 损失,预测目标是连续价格;metric='rmse'决定验证集上的早停依据。learning_rate=0.05是收缩步长,调小一点能提精度但需要更多轮数。num_leaves=31控制单棵树复杂度,叶子越多越容易过拟合。feature_fraction和bagging_fraction分别是列采样和行采样,两个值同时设成 0.8 是防止过拟合的常见组合。early_stopping(stopping_rounds=50)的意思是连续 50 轮验证分数没提升就停止训练,避免 num_boost_round 白设太大。
树模型不要求特征标准化,所以这里直接喂原始数值特征。优化前的代码结构在这份资源里就是教学用的基线,第 4 章会把它和特征交叉后的版本做对比。
3.4 cnn代码.py:一维卷积处理特征序列
CNN 出现在价格预测项目里看起来有点跨界,但思路是把每条样本的多个特征当作一个一维序列,用 Conv1D 提取局部组合模式。项目里的X_data.csv和Y_data.csv就是为 CNN 准备的输入输出文件,训练好的权重存成了20.h5。
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense, Conv1D, MaxPooling1D, Flatten n_features = X_train.shape[1] model = Sequential() model.add(Conv1D( filters=64, kernel_size=3, activation='relu', input_shape=(n_features, 1) )) model.add(MaxPooling1D(pool_size=2)) model.add(Flatten()) model.add(Dense(128, activation='relu')) model.add(Dense(1)) model.compile(optimizer='adam', loss='mse', metrics=['mae'])input_shape=(n_features, 1)要求把每条样本 reshape 成特征数乘 1 的矩阵,相当于把全部特征按固定顺序排成序列。kernel_size=3的卷积核会每次覆盖 3 个相邻特征做加权组合,filters=64表示同时学习 64 种不同的局部模式。MaxPooling1D做降采样压缩信息量,最后接全连接层输出一个价格值。
CNN 在这里的定位更多是探索性对比,结构上它把特征当作有顺序的序列处理,但实际上很多特征之间并没有真实的空间或顺序关系。训这个模型时要注意标签分布,价格如果右偏严重,直接拿原始价格算 MSE 会被少数高价样本主导,我一般会先对标签做log1p变换再训练,预测完再expm1还原。
4. 优化路径:特征交叉与十折交叉验证把lightGBM拉高一个身位
4.1 特征交叉:让高相关特征对的组合信息进入模型
项目里有个单独的文件lightGBM特征交叉后及十折交叉验证模型代码.py,这个文件名直接点出了优化阶段的两件事:特征交叉和 KFold 验证。先说特征交叉。
第 2 章里标记出来的高相关特征对,到了树模型这里不是简单地删掉一列,而是可以把它们组合成新特征,让模型直接看到两列之间的乘积关系。比如表显里程和车龄高度相关,那么里程 × 车龄这个新特征实际上代表的是年均行驶强度,这个强度比单独的里程或车龄更能解释车辆折旧程度。
# 假设 high_corr_pairs 来自第2章的相似度txt解析结果 for f1, f2 in high_corr_pairs: cross_name = f'{f1}_x_{f2}' df[cross_name] = df[f1] * df[f2]交叉特征的命名统一用_x_分隔,便于后续回溯是哪两个字段生成的。做乘法交叉是性价比最高的做法,因为它不改变原始列、不引入额外缺失值,而且对树模型来说,只要这个新特征能提供有效分裂信息,它就会被自动选中。除乘法之外,也有做差、做比值的变体,比如里程 / 车龄,这类比值特征有时比乘积更有解释力,但要看具体业务含义。
4.2 十折交叉验证:稳定评估而非单次随机划分
优化前代码用的是单次train_test_split,也就是随机切一次,训练一次,评估一次。这种做法的问题在于结果对随机种子敏感——同一份数据切法不同,得分可能波动好几个点。十折交叉验证的做法是把训练数据切成 10 份,轮流拿 9 份训练 1 份验证,最终把 10 次验证分数平均。
from sklearn.model_selection import KFold kf = KFold(n_splits=10, shuffle=True, random_state=42) fold_scores = [] for fold, (train_idx, valid_idx) in enumerate(kf.split(X)): X_fold_train = X.iloc[train_idx] y_fold_train = y.iloc[train_idx] X_fold_valid = X.iloc[valid_idx] y_fold_valid = y.iloc[valid_idx] fold_model = lgb.train(params, lgb.Dataset(X_fold_train, y_fold_train), num_boost_round=1000, valid_sets=[lgb.Dataset(X_fold_valid, y_fold_valid)], callbacks=[lgb.early_stopping(50)]) fold_scores.append(fold_model.best_score['valid_0']['rmse']) print('10折平均RMSE:', sum(fold_scores) / len(fold_scores))n_splits=10是交叉验证的折数,shuffle=True保证切分前打乱样本顺序,避免原始数据里有按时间或按地区排序导致的分片偏差。random_state=42保证实验可复现。每个 fold 都重新训一个模型,所以总训练时间是单次的 10 倍,这个代价换来的是一组更可信的分数区间。
这里有一个细节值得注意:交叉验证中的每个 fold 内部仍然用了早停,所以最终模型数量是 10 个,而不是 1 个。如果只是为了评估效果,10 个分数平均就够;如果还要拿最终模型做预测,常见做法是拿全量数据用同样的参数重新训一个,或者对 10 个模型的预测取平均,后者更稳但成本和复杂度都更高。
4.3 三个回归模型的预测结果对比
项目里最后落地的预测结果分布在svr_final(1).csv、linear_regression_test_final.csv、CNN_test_final.csv和优化后的 lightGBM 输出里。对比这四个结果时,评价口径主要看两个指标:RMSE 和 MAE。RMSE 对大的预测偏差更敏感,能暴露是否有极端错误;MAE 更贴近实际误差的平均水平,适合业务解释。
| 模型 | 输入特征 | 预处理要求 | 训练耗时 | 结果稳定性 |
|---|---|---|---|---|
| 多元线性回归 | 标准化数值特征 | 必须标准化、处理共线性 | 极短 | 对异常值敏感 |
| SVR | 标准化数值特征 | 必须标准化 | 样本过万后明显变慢 | 调参影响大 |
| lightGBM(基线) | 原始数值特征 | 基本不需要 | 快 | 高 |
| lightGBM(交叉+KFold) | 原始特征+交叉特征 | 不需要 | 中等 | 最高 |
| CNN | 按固定顺序排列的特征序列 | 推荐标签变换 | 需要GPU或较长CPU时间 | 方差偏大 |
实际复现时能看到一个典型趋势:线性回归和 SVR 在特征标准化到位的情况下能拿到一个合理的基线,但天花板明显;CNN 在数据量不够大时分数抖动厉害;lightGBM 加上特征交叉和十折交叉验证之后,RMSE 通常能比基线再降一截,这也是为什么优化后代码单独拆成了一个文件。需要说明的是,20.h5是 CNN 已经训练好的权重文件,如果机器上没有 TensorFlow 环境,可以直接加载这个文件做预测,省掉重新训练的成本。
5. 避坑记录:五条从数据集里踩出来的血泪经验
5.1 数据准备阶段的三次翻车
第一次翻车发生在处理缺失值的时候。现象是 lightGBM 训练不报错,但预测结果整体偏低,而且特征重要性里出现了一些业务上说不通的列。原因是 lightGBM 本身支持缺失值,会默认把缺失样本分到收益最大的一侧,但这个策略不等于业务合理的填充方案。解决方式是先统计每列缺失率,连续特征用中位数填充,离散特征用众数填充,并保留一张表记录哪些列被填充过,答辩时讲数据清洗就有据可依。
第二次翻车在跑 SVR 的时候,现象是训练过程极慢,一个晚上都没出结果。原因前面提过:RBF 核的 SVR 要计算样本两两之间的核矩阵,复杂度接近 O(n²),样本量到两三万之后就非常吃力。解决方法是先对训练集做随机抽样,用 5000 到 10000 条样本跑通流程,确认最终精度可接受再决定是否全量训练。这不是最优解,但在资源受限的期末作业场景里最实用。
第三次翻车在线性回归的权重上。现象是w_data.npy加载后,某些特征的权重符号和业务直觉完全相反,比如表显里程越高价格预测反而越高。原因是原始特征没有标准化就丢进正规方程,同时高相关特征对没有处理,导致X^T X接近奇异。解决方案是把训练和预测全部切到标准化后的数据上,并删掉或合并相关系数超过 0.8 的特征列,重新训练后权重符号回归正常。
5.2 模型训练与验证阶段的两个隐形坑
第四个坑出在 CNN 的标签处理上。现象是训练过程 loss 下降很漂亮,但还原成价格后预测值普遍偏低,且高价车型的预测误差极大。原因是价格分布右偏,少数高价样本把 MSE 撑得很大,网络为了降低整体 loss 把预测往中低价区间收缩。解决方式是对标签做np.log1p(y)变换后再训练,预测完np.expm1还原,同时把评估指标从单纯看 loss 改成同时看 MAE,才能反映真实误差量级。
第五个坑是 lightGBM 的特征名问题。现象是lgb.train直接抛异常,提示特征名里不能包含某些特殊字符。原因是原始 csv 的表头里存在空格、中文括号之类的字符,lightGBM 的 C++ 底层对特征名校验很严格。解决方式是在建模前统一清洗列名:
df.columns = [str(c).strip().replace(' ', '_').replace('(', '(').replace(')', ')') for c in df.columns]这条代码把空格替换成下划线,全角括号替换成半角,能规避绝大多数和特征名相关的报错。如果还不行,检查是否出现了重复列名,pandas 允许重复列名存在,但 lightGBM 不允许。
6. 结果验证与项目复用:先看残差再谈模型好坏
6.1 用残差分布完成模型验收
模型跑完拿到 RMSE 不是终点,我每次都会做一次残差分析再判断模型能不能用。所谓残差就是y_true - y_pred,把它画成直方图或散点图能暴露三类问题:一是残差均值明显不为 0,说明预测存在系统性偏差;二是残差方差随预测值增大而增大,说明高价区间拟合不足;三是残差分布出现明显长尾,说明存在极端样本在主导 loss。对这个项目来说,把测试集真实价格和预测价格拼成一张表,用 pandas 算残差列,再打印分位数,是最快的验收方式。
6.2 把整套流程迁移到其他价格预测场景
这份资源的价值不止于二手车。把这套流程抽象出来,它其实是一条通用的价格预测流水线:原始表清洗 → 共线性标记 → 标准化/交叉特征生成 → 四类模型对比 → 十折交叉验证 → 残差验收。换到租房价格预测、手机二手价预测、游戏账号估值这类场景,只需要替换数据表和字段名,流程不用大改。
从那以后,我每次拿到价格预测类项目,都会强制先跑一遍残差分位数再谈模型好坏,这个习惯就是从拆这份资源养成的。优化后的 lightGBM 代码和训练好的权重都在压缩包里,跳过了环境折腾的人可以拿20.h5直接复现 CNN 那条线,想自己动手的可以从张立静作业代码.py开始重跑。希望帮到你。
本文还有配套的精品资源,点击获取