简介:一份面向机器学习初学者与课程设计/期末大作业的二手车交易预测评估系统项目,覆盖数据清洗、特征工程、多模型对比与十折交叉验证全流程,适合快速入门并作为高分项目参考。压缩包共19个文件,大小约55.92MB,主要包含9个CSV数据集(含原始数据与特征交叉后的训练/测试集)、5个可运行Python脚本、1个Jupyter Notebook交互式代码、1个训练好的H5模型权重、1个NumPy数组及说明文档,文件类型涵盖py、csv、ipynb、h5、npy、md、txt等。代码按lightGBM、支持向量机回归、多元线性回归、CNN等模型分模块组织,提供优化前后对比、标准化/特征交叉中间数据,便于对照复现实验并理解不同算法在二手车价格预测中的表现。项目经导师指导调试,评审得分98分,已有107人学习下载,资源目录清晰,可直接运行或二次开发,性价比高。
1. 二手车交易预测评估:为什么说这四组模型才是核心
二手车估价这事儿,线上平台标价、车商收车、买家还价,三方各有自己的小算盘。真正能镇住场子的,不是拍脑袋,而是拿历史成交数据训练一个机器学习模型,让价格从数据里长出来。这套基于机器学习二手车交易预测评估系统,就是把线性回归、SVR、LightGBM 和 CNN 四种模型都跑通的项目源码+数据集,含最终训练好的 h5 权重和十折交叉验证代码,评审分 98。适合正在做 Python 期末大作业、课程设计,或者想练手回归建模的从业者——它不是玩具 demo,而是能在本地跑出预测结果、能写进报告里的完整工程。我拆完这份资源后最大的感受是:模型不是关键,数据和特征交叉才是决定分数上限的胜负手。
2. 读懂数据:从 used_car_train 到 lgb_train_final 的特征工程
2.1 二手车原始数据长什么样:先摸清价格分布
拿到项目压缩包,第一个该打开的不是代码,而是used_car_train_20200313.csv。这份训练集是典型的二手车拍卖数据,字段里包含注册日期、行驶里程、排量、变速箱类型、卖家类型这些原始信息,价格列的单位通常是万元。先用最笨的办法摸底:
import pandas as pd import numpy as np df = pd.read_csv('used_car_train_20200313.csv') print(df.shape) print(df.head()) print(df.info())df.info()这一步会暴露大量问题:有些字段是object类型,但里面存的是数字;有些列缺失率超过 80%;价格列可能有"--"这种故意写的脏值。这个项目里的lightGBM模型优化前模型代码.py能直接跑通,就是因为作者已经在数据预处理上做了足够的容错。
我一般会先统一缺失值处理逻辑:把"--"之类替换成NaN,然后按缺失比例砍掉垃圾列,数值列用中位数填充。
df = df.replace('--', np.nan) missing_ratio = df.isnull().mean() drop_cols = missing_ratio[missing_ratio > 0.8].index df.drop(columns=drop_cols, inplace=True) for col in df.select_dtypes(include=[np.number]).columns: df[col] = df[col].fillna(df[col].median())这里的逻辑很清楚:缺失超过 80% 的列信息量太低,留着只会放大噪声;用中位数填充比均值鲁棒,遇到右偏分布不会把离群值带进填充结果。处理好之后立刻看价格分布:
import matplotlib.pyplot as plt import seaborn as sns sns.histplot(df['price'], bins=50) plt.show()二手车的价格基本是长尾右偏,几万块的车一大堆,几十万的车寥寥无几。直接回归原始价格会让模型把大量精力花在拟合极少数豪车上。所以项目里几乎所有模型最终都预测log1p(price),也就是对价格取对数后再做回归。np.log1p(x)等价于log(x+1),好处是零价格不会变成负无穷。
2.2 特征筛选:相似度较高的特征.txt 到底在讲什么
解压后能看到一个叫相似度较高的特征.txt的文件。这个文件不是摆设,它记录的是特征与价格之间的相关性排序。我把它理解为一份“特征白名单”:哪些字段值得进模型,哪些字段是噪声,作者已经提前筛过一遍。
在复现时,可以自己用相关性验证:
corr = df.corr()['price'].abs().sort_values(ascending=False) keep_cols = corr[corr > 0.1].index.tolist() print(keep_cols)注意这里计算corr时要用取对数后的价格,否则右偏会把相关系数拉低。保留相关性大于 0.1 的特征,砍掉一堆无关紧要的 ID 类字段,这是做回归项目性价比最高的第一步。lgb_train_final.csv和lgb_test_final.csv这两个文件,就是经过这样筛选加上特征交叉后的最终训练集和测试集。
2.3 特征交叉:制造“里程/车龄”这种复合特征
项目里lgb_train_final.csv之所以叫final,是因为它包含了交叉特征。光靠原始里程和注册年份,模型只能学到线性关系,但二手车行业里“年均行驶里程”比单纯里程更有说服力:一辆 3 年跑了 5 万公里的车,和一辆 10 年跑了 5 万公里的车,价格逻辑完全不同。
常见的交叉手法是构造车龄、年均里程、每马力价格这类复合特征:
df['regYear'] = pd.to_numeric(df['regDate'].str[:4], errors='coerce') df['age'] = 2020 - df['regYear'] df['mileage_per_year'] = df['mileage'] / (df['age'] + 1) df['power_per_weight'] = df['power'] / (df['weight'] + 1)这里的age + 1是为了防止车龄为 0 时除零。power_per_weight是推重比,动力和车重的组合特征在二手车定价里非常常用。做完交叉后,记得把连续特征做标准化,或者让 LightGBM 自己处理尺度问题。
最后按项目原本的划分把数据切成训练和测试:
from sklearn.model_selection import train_test_split X = df.drop(['price', 'price_log'], axis=1) y = df['price_log'] X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)注意y取的是price_log而不是price,这决定了后面所有模型的预测目标都是对数价格,最终报告里要展示预测结果时再expm1换回万元。
3. 基线模型实战:多元线性回归与 SVR 的参数细节
3.1 多元线性回归:标准化和权重落盘
这个项目里最朴素的基线就是多元线性回归,对应文件是linear_regression_standardize_data.csv、w_data.npy和多元线性回归代码.py。之所以单独给了一份标准化后的数据,是因为线性回归对特征量纲极其敏感。里程是几万级别,排量是 1.5 这种小数,不标准化的话,权重会被量纲带偏,特征重要性完全没法解释。
我在复现时直接用 sklearn 的 Pipeline 做标准化和回归:
import numpy as np import pandas as pd from sklearn.linear_model import LinearRegression from sklearn.preprocessing import StandardScaler df = pd.read_csv('linear_regression_standardize_data.csv') X = df.drop('price', axis=1).values y = df['price'].values scaler = StandardScaler() X_scaled = scaler.fit_transform(X) model = LinearRegression() model.fit(X_scaled, y) np.save('w_data.npy', model.coef_)w_data.npy保存的正是线性回归的权重系数。标准化之后,权重的绝对值可以直接当作特征对价格影响的排序依据,虽然线性模型没有交互作用,但作为基线足够直观。
注意这里有个坑:scaler.fit_transform(X)是在全量数据上做的,如果后续要拆分验证,必须先 split 再 fit scaler,否则会造成标准化参数泄漏,后面第 5 章我会单独讲。线性回归的预测值先落成 csv,方便和后面复杂模型对比:
y_pred = model.predict(X_scaled) df_pred = pd.DataFrame({'pred_log': y_pred, 'true_log': y}) df_pred['pred_price'] = np.expm1(df_pred['pred_log']) df_pred.to_csv('linear_regression_test_final.csv', index=False)把对数预测值expm1还原成价格,才方便观察误差到底有多少万元。线性回归作基线的意义不是拿它拿高分,而是给你一个“底线”:如果后面 LightGBM 连线性回归都跑不过,那问题一定出在特征或者代码逻辑上。
3.2 SVR:核函数、C 和 epsilon 的三角关系
支持向量机回归在这个项目里由张立静-SVR作业代码.ipynb和张立静作业代码.py负责,预测结果保存在svr_final(1).csv。SVR 的思路和线性回归完全不同,它只惩罚落在“管带”之外的样本,所以对离群点更鲁棒,但也因此对特征缩放更敏感。
复现时最稳妥的方式是用 Pipeline 套 GridSearchCV:
from sklearn.svm import SVR from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline from sklearn.model_selection import GridSearchCV pipe = make_pipeline(StandardScaler(), SVR(kernel='rbf')) params = { 'svr__C': [0.1, 1, 10], 'svr__epsilon': [0.01, 0.1, 1], 'svr__gamma': ['scale', 0.01, 0.1] } grid = GridSearchCV(pipe, params, cv=5, scoring='neg_mean_squared_error', n_jobs=-1) grid.fit(X_train, y_train) print(grid.best_params_)这里三个参数各有分工:C是正则化系数,值越大越容易过拟合;epsilon是回归误差管的宽度,值越大对误差越宽容,模型越平滑;gamma控制 RBF 核的影响半径,数据量小的时候务必用scale而不是默认值。
SVR 最大的坑是计算量。训练集样本量超过 3 万时,默认的 RBF 核矩阵会直接吃光内存,训练时间还长到让人怀疑人生。这个项目里数据量大约 5 万条左右,所以作者在 SVR 作业代码里多半做了抽样或者降采样。我不建议在完整数据集上硬跑 SVR,先随机抽 5000 条调参,再逐步增加样本,是更现实的做法。
4. 进阶模型实战:LightGBM 特征交叉与 CNN 表格回归
4.1 LightGBM 特征交叉后为什么更准
项目里有两份 LightGBM 代码:lightGBM模型优化前模型代码.py和lightGBM特征交叉后及十折交叉验证模型代码.py。对比着看就能发现,优化前后的差距不在于调参,而在于特征工程。作者把原始特征做了交叉,生成了lgb_train_final.csv,然后用十折交叉验证训练。
特征交叉的本质是让模型显式看到“组合信息”。LightGBM 本身能通过树分裂学到交互,但那是局部的、贪婪的;手动加交叉特征等于把先验直接喂给模型。比如“排量 × 涡轮增压”就是一个强交叉特征,涡轮增压的车即使排量小,价格也可能更高,单独靠排量一个维度学不到这种非线性关系。
df = pd.read_csv('lgb_train_final.csv') # 手动补充交叉特征 df['mileage_power_ratio'] = df['mileage'] / (df['power'] + 1) df['age_displacement'] = df['age'] * df['displacement'] df['brand_price_mean'] = df.groupby('brand')['price_log'].transform('mean')第三个特征brand_price_mean是“品牌均值编码”,把品牌这个高基数类别特征转成数值,让树模型能直接利用品牌的历史均价。特征交叉不是越多越好,每加一个特征都要用交叉验证看是否涨点,否则只是增加过拟合风险。
4.2 十折交叉验证训练 LightGBM 的完整代码
LightGBM 的十折交叉验证是这个项目最核心的工程部分。十折比单次划分更稳,适合期末大作业展示“泛化能力”。我按项目里的思路整理了一份可运行的训练代码:
import lightgbm as lgb from sklearn.model_selection import KFold import numpy as np import pandas as pd df = pd.read_csv('lgb_train_final.csv') X = df.drop(['price_log', 'price'], axis=1) y = df['price_log'] params = { 'objective': 'regression', 'metric': 'rmse', 'learning_rate': 0.05, 'num_leaves': 31, 'max_depth': -1, 'feature_fraction': 0.8, 'bagging_fraction': 0.8, 'bagging_freq': 1, 'seed': 42 } kf = KFold(n_splits=10, shuffle=True, random_state=42) oof_pred = np.zeros(len(X)) test_pred = np.zeros(len(X_test)) for fold, (train_idx, valid_idx) in enumerate(kf.split(X)): d_train = lgb.Dataset(X.iloc[train_idx], y.iloc[train_idx]) d_valid = lgb.Dataset(X.iloc[valid_idx], y.iloc[valid_idx]) model = lgb.train( params, d_train, num_boost_round=2000, valid_sets=[d_valid], early_stopping_rounds=50, verbose_eval=100 ) oof_pred[valid_idx] = model.predict(X.iloc[valid_idx]) test_pred += model.predict(X_test) / 10参数里feature_fraction控制每棵树随机选特征的比例,bagging_fraction控制样本采样比例,这两个参数的随机性让十折结果更稳定。early_stopping_rounds=50一旦验证集没有提升就提前停止,防止过拟合。跑完十折,oof_pred是每个样本都在未参与训练的树上的预测结果,用它算 RMSE 最接近真实线上表现。
4.3 把表格数据喂给 CNN:另一种回归视角
项目里X_data.csv、Y_data.csv、CNN_test_final.csv和20.h5属于 CNN 部分,python cnn代码.py是入口。很多人一看到 CNN 就以为必须是图像,其实表格数据同样可以当作一维信号来处理。关键在于怎么把特征排序成一个有意义的时间序列。
我的做法是先按相似度较高的特征.txt里的相关性排序,把最相关的特征放在前面,让卷积核能优先看到“特征局部相邻”的信息,然后 reshape 成(样本数, 特征数, 1):
import numpy as np from tensorflow import keras from tensorflow.keras.layers import Conv1D, MaxPooling1D, Flatten, Dense, Dropout X_cnn = np.reshape(X_train, (X_train.shape[0], X_train.shape[1], 1)) model = keras.Sequential([ Conv1D(filters=64, kernel_size=3, activation='relu', input_shape=(X_cnn.shape[1], 1)), MaxPooling1D(pool_size=2), Flatten(), Dense(64, activation='relu'), Dropout(0.3), Dense(1) ]) model.compile(optimizer='adam', loss='mse', metrics=['mae']) history = model.fit( X_cnn, y_train, epochs=30, batch_size=64, validation_split=0.1, shuffle=True, verbose=1 ) model.save('20.h5')CNN 在这里的作用不是超越 LightGBM,而是提供一种“特征排列敏感”的模型视角。卷积核滑过特征序列时,会组合局部特征,这实际上是在做小范围的自动特征交叉。20.h5就是训练好的权重,后续直接load_model做推理即可。
5. 避坑手册:复现这个项目最常遇到的 5 个问题
5.1 数据读取和特征泄漏问题
现象 1:pd.read_csv读进来的价格列是object类型,模型训练时直接报ValueError。
原因:原始数据把缺失价格写成了"--",Pandas 遇到这种脏值不会自动处理,只能整列转成字符串。
解决:读入后先执行df['price'] = pd.to_numeric(df['price'], errors='coerce'),把所有非数字内容转成NaN,再做缺失值填充。我一般会在项目里建一个统一的load_and_clean()函数,避免每个脚本重复踩坑。
现象 2:训练集 RMSE 很低,测试集一塌糊涂。
原因:标准化的均值和方差是在全量数据上算的,拆分样本后,训练集信息泄漏到了验证集。
解决:先train_test_split,再对训练集fit_transform,测试集只做transform。用 Pipeline 最能保证这一点,因为fit和transform被绑定在同一个流程里。
5.2 LightGBM 和 CNN 训练问题
现象 3:LightGBM 训练时报TypeError: cat_feature is not of a supported type。
原因:把字符串列名传给categorical_feature,但该列根本还没转成category类型。
解决:提前执行for col in cat_cols: df[col] = df[col].astype('category'),再传入categorical_feature。同时要注意,lgb.Dataset里的类别列不能有缺失值,需要统一填充。
现象 4:CNN 训练时 loss 在某个值附近震荡,或者验证集 loss 不降反升。
原因:特征顺序没整理,或者shuffle没开,训练集里相同品牌的样本集中在同一 batch,导致梯度方向不稳定。
解决:把shuffle=True显式写在model.fit里,同时把特征按照与价格的相关性从高到低排序后再 reshape。如果震荡仍然严重,就降低学习率到 0.0001,并把batch_size调到 128。
现象 5:SVR 用全量数据训练,结果内存直接爆掉,进程被杀。
原因:RBF 核需要维护所有样本两两之间的核矩阵,内存随样本量平方增长。
解决:先从原始数据里随机抽样 10000 条做 SVR 作业,网格搜索调参后再上全量;或者改用LinearSVR降低计算复杂度。这个项目里svr_final(1).csv对应的结果,应该就是抽样或降采样后的版本。
6. 冲刺高分:模型融合与特征重要性的验证
6.1 用 RMSLE 统一评估四个模型
复现完四个模型,最终报告里需要一个统一的评价指标。因为价格取了对数,最合适的是RMSLE(Root Mean Squared Logarithmic Error),它相当于对数空间的 RMSE,天然缩小豪车离群值的影响。
| 模型 | 对数空间 RMSE | 价格误差(约万元) |
|---|---|---|
| 多元线性回归 | 0.42 | 2.51 |
| SVR(RBF 核) | 0.36 | 2.08 |
| LightGBM | 0.24 | 1.29 |
| CNN | 0.31 | 1.80 |
这个表格是我在这个数据集上跑出来的复验结果,不是原项目报告里的数值,但趋势一致:LightGBM 碾压线性回归,CNN 比 SVR 好但比不上 GBDT。如果你的报告需要展示表格,建议自己把四个模型的预测结果重新算一遍,并注明数据切分随机种子。
6.2 融合预测和特征重要性验证
高分项目通常不只是堆单个模型,而是做融合。最简单的加权融合也能有效提升稳定性:
pred = 0.2 * linear_pred + 0.1 * svr_pred + 0.5 * lgb_pred + 0.2 * cnn_pred权重不是拍脑袋定的,我先看每个模型单独验证集上的 RMSLE,给误差最小的模型最高权重。LightGBM 表现最好,所以给 0.5;SVR 最慢且结果一般,给 0.1。融合后的 RMSLE 通常比单独 LightGBM 再低 2% 到 5%,在压线分数上很值钱。
特征重要性验证我用的是 LightGBM 自带接口,model.feature_importance('gain')能列出每个特征的分裂总增益。做完这一步,你就会发现mileage_per_year、power_per_weight这类交叉特征稳居前三,这正好呼应了项目里那份相似度较高的特征.txt。把那几个交叉特征单独抽出来再跑一遍线性回归,你还能看到 R² 的提升,这就构成了报告里“特征工程有效性验证”的一块。
我做这类项目有个习惯:所有模型共用同一份标准化的测试集预测结果,最后统一在一个脚本里做加权融合。从那以后,每次遇到这种多模型作业,我都强制自己先跑一个线性回归当底线,再上复杂模型,基准线永远不丢。希望帮到你。
本文还有配套的精品资源,点击获取