资讯动态

基于机器学习的二手车价格预测Python源码实战:从数据清洗到模型调优

发布时间:2026/10/12 22:59:06 来源:尧图企业网站定制
简介这份资源是面向机器学习入门与进阶学习者的二手车价格预测完整项目源码采用Python实现适合想通过真实回归任务掌握数据清洗、特征工程与模型训练的开发者参考。压缩包共20个文件约99.6MB包含4个Python源码文件、5个XML配置、3个CSV数据文件以及说明文档、项目配置与开源协议等覆盖从数据到模型输出的完整流程。其中model.py负责模型构建与训练main.py作为程序入口协调流程basline.py提供基准模型用于性能对比CSV数据则记录车辆品牌、年份、里程等属性便于直接复现实验。目前已有176人学习下载。读者可借此获得一套结构清晰的赛题级方案理解回归问题的建模思路、特征选择与降维方法并参考基准对比优化自身模型快速搭建可运行的预测系统。1. 从一份 21 文件的压缩包说起二手车价格预测源码能跑出什么二手车商最怕的不是收不到车是收回来定不准价。同一台 2015 年的车车况差半档、里程差两万公里成交价能差出一万多。人工估价靠老师傅的经验但经验没法复制也没法批量跑。这份基于机器学习的二手车价格预测 Python 设计源码解决的就是把「老师傅的直觉」变成「可复现的回归模型」这件事。压缩包解开是 21 个文件核心就四块main.py管流程、model.py建模型、basline.py给基准线、utlis.py做数据清洗配套两份 CSV 训练测试数据和一份weight_mix.csv权重文件。它适合两类人想拿一个完整回归项目练手的 Python 学习者以及需要快速搭出估价原型的二手车业务开发者。下面我按「数据长什么样 → 特征怎么处理 → 模型怎么训 → 怎么验证不翻车」的顺序拆一遍参数和坑都落到具体命令上。2. 数据先摸清楚CSV 字段解析与缺失值处理拿到任何一份价格预测源码第一件事不是跑main.py是打开 CSV 看列名。这个项目的数据文件是used_car_train_20200313.csv和used_car_testB_20200421.csv训练集带price列测试集不带这是典型的监督学习回归布局。二手车数据的字段通常分三类类别型品牌、车型、变速箱、燃油类型、数值型里程、车龄、排量、功率、匿名特征v_0到v_N这种脱敏列。匿名列在二手车竞赛数据里很常见别急着删它们往往是卖家或平台侧编码过的强特征。2.1 字段类型识别与读取参数先用 pandas 把数据读进来重点看dtypes和isnull().sum()。这一步决定了后面要不要做类型转换。import pandas as pd import numpy as np # 读取训练集low_memoryFalse 避免混合类型列被错误推断 train pd.read_csv(used_car_train_20200313.csv, sep , low_memoryFalse) test pd.read_csv(used_car_testB_20200421.csv, sep , low_memoryFalse) print(train.shape, test.shape) print(train.dtypes.value_counts()) # 看有多少 object / int / float print(train.isnull().sum()[train.isnull().sum() 0]) # 只打印有缺失的列这里sep 是关键参数。很多二手车数据集是空格分隔而不是逗号分隔如果直接read_csv默认逗号会把整行读成一列后面所有特征工程全废。判断方法很简单用文本编辑器打开 CSV 看第一行分隔符或者读进来后train.shape[1] 1就说明分隔符错了。low_memoryFalse是防止 pandas 分块推断类型时把某些列判成混合类型导致后续astype报错。2.2 缺失值处理策略不是所有空都填 0二手车数据里缺失值有讲究。数值型缺失比如功率power缺失填中位数比填 0 合理因为 0 功率在物理上不存在会污染模型。类别型缺失比如bodyType缺失填一个单独的unknown类别比填众数更安全因为缺失本身可能就是信息。# 数值列用中位数填充类别列用 unknown 填充 num_cols train.select_dtypes(include[np.number]).columns.tolist() cat_cols train.select_dtypes(include[object]).columns.tolist() for col in num_cols: if train[col].isnull().sum() 0: median_val train[col].median() train[col] train[col].fillna(median_val) test[col] test[col].fillna(median_val) # 测试集必须用训练集中位数 for col in cat_cols: train[col] train[col].fillna(unknown) test[col] test[col].fillna(unknown)注意测试集填充必须用训练集算出来的中位数不能各填各的。这是血泪经验如果测试集用自己的中位数等于引入了训练时看不到的分布信息线下验证分数会虚高上线就翻车。类别列填unknown后后面做标签编码时要确保训练集和测试集的编码映射一致否则会出现测试集某个类别在训练集没见过的报错。2.3 异常值筛查价格和里程的物理边界价格预测里最容易被忽略的是异常值。训练集price如果有 0 或者极端大值会严重拉偏回归模型。里程kilometer如果是负数或者超过 100 万公里基本是录入错误。# 价格下限过滤上限用分位数截断 train train[train[price] 100] # 去掉明显异常的低价 upper train[price].quantile(0.99) train train[train[price] upper] # 截掉 top 1% 极端值 # 里程合理性检查 if kilometer in train.columns: train train[(train[kilometer] 0) (train[kilometer] 100)]分位数截断比直接删 3σ 更稳因为价格分布通常是右偏的3σ 假设正态分布不成立。截掉 top 1% 是常见做法既保留了大部分样本又去掉了可能干扰训练的极端点。做完这一步记得重置索引train train.reset_index(dropTrue)否则后面按位置取数会错位。3. 特征工程从原始字段到模型能吃的矩阵原始 CSV 里的字段模型不能直接吃尤其是类别型字段。这一章讲怎么把品牌、车型这些文本变成数值以及匿名特征怎么筛。3.1 类别特征编码LabelEncoder 与 One-Hot 的取舍类别特征编码有两条路标签编码LabelEncoder和独热编码One-Hot。二手车数据里品牌这种高基数类别几十上百个取值用 One-Hot 会让特征维度爆炸通常用 LabelEncoder 或者目标编码。变速箱这种低基数类别手动/自动/其他用 One-Hot 更合适。from sklearn.preprocessing import LabelEncoder # 高基数类别用 LabelEncoder le LabelEncoder() for col in [brand, model, bodyType, fuelType, gearbox]: if col in train.columns: # 合并训练测试一起编码保证类别映射一致 combined pd.concat([train[col], test[col]], axis0).astype(str) le.fit(combined) train[col] le.transform(train[col].astype(str)) test[col] le.transform(test[col].astype(str))合并编码是重点。如果只对训练集fit测试集出现新类别时transform会直接抛ValueError: y contains previously unseen labels。合并后fit能覆盖所有出现过的类别测试集就不会报错。代价是测试集信息轻微泄漏到编码映射里但对树模型影响很小工程上可接受。3.2 匿名特征筛选v_ 列怎么挑匿名特征v_0到v_N通常已经做过脱敏和部分编码直接全用可能引入噪声。常见做法是先用方差过滤去掉近似常量的列再用相关性筛掉和价格几乎无关的列。from sklearn.feature_selection import VarianceThreshold v_cols [c for c in train.columns if c.startswith(v_)] if len(v_cols) 0: selector VarianceThreshold(threshold0.01) # 去掉方差极小的列 selector.fit(train[v_cols]) keep_v [c for c, k in zip(v_cols, selector.get_support()) if k] print(f匿名特征保留 {len(keep_v)} / {len(v_cols)})threshold0.01是经验值方差低于这个的列基本是常量留着只会增加计算量。如果匿名特征数量很大几百列还可以用SelectKBest配合f_regression取 top K但要注意别把弱特征全删了树模型对弱特征有一定容忍度。3.3 构造车龄特征用年份做减法二手车价格和车龄强相关。如果数据里有regDate注册日期或者creatDate上架日期可以算出车龄。这个特征往往比原始日期字段更有用。# 假设有 regDate 格式为 2010 这种年份 if regDate in train.columns and creatDate in train.columns: train[car_age] train[creatDate] - train[regDate] test[car_age] test[creatDate] - test[regDate] # 车龄为负的是录入错误置为 0 train[car_age] train[car_age].clip(lower0) test[car_age] test[car_age].clip(lower0)clip(lower0)处理的是上架日期早于注册日期的脏数据这种记录虽然少但不处理会让模型学到「负车龄」这种不存在的模式。构造完新特征后原始日期列可以删掉避免树模型在无意义的数值上分裂。4. 模型训练与基准对比basline.py 和 model.py 怎么配合这个项目里basline.py和model.py是两个独立入口前者给基准线后者是主模型。理解它们的分工才能知道改哪里有效。4.1 基准模型先跑通再优化basline.py的作用是给一个最简模型通常用线性回归或者决策树目的是验证数据管道通不通。别小看这一步很多人直接上 XGBoost结果数据里有字符串没处理干净报错报半天找不到原因。from sklearn.linear_model import LinearRegression from sklearn.model_selection import train_test_split from sklearn.metrics import mean_squared_error import numpy as np # 假设 feature_cols 是前面处理好的特征列 feature_cols [c for c in train.columns if c not in [price, SaleID, name]] X train[feature_cols] y train[price] X_train, X_val, y_train, y_val train_test_split(X, y, test_size0.2, random_state42) lr LinearRegression() lr.fit(X_train, y_train) pred lr.predict(X_val) rmse np.sqrt(mean_squared_error(y_val, pred)) print(fBaseline RMSE: {rmse:.2f})random_state42固定随机种子保证每次划分一致方便对比不同模型的分数。RMSE 是价格预测的常用指标单位是元解释起来直观。基准线跑出来的 RMSE 如果大得离谱比如几万先别调模型回去检查特征里是不是混进了 ID 列或者日期字符串。4.2 主模型树模型与参数起点model.py里大概率用的是树模型XGBoost / LightGBM / CatBoost 之一。这类模型对二手车这种混合了类别和数值特征的数据很友好不用做归一化也能自动处理特征交互。import lightgbm as lgb params { objective: regression, metric: rmse, learning_rate: 0.05, num_leaves: 31, max_depth: -1, min_data_in_leaf: 20, feature_fraction: 0.8, bagging_fraction: 0.8, bagging_freq: 5, seed: 42 } dtrain lgb.Dataset(X_train, labely_train) dval lgb.Dataset(X_val, labely_val, referencedtrain) model lgb.train( params, dtrain, num_boost_round2000, valid_sets[dval], callbacks[lgb.early_stopping(100), lgb.log_evaluation(100)] )learning_rate0.05配合num_boost_round2000和early_stopping(100)是稳妥组合学习率低一点靠早停决定实际迭代轮数避免过拟合。num_leaves31是 LightGBM 默认值数据量小的时候可以降到 15 左右。min_data_in_leaf20防止叶子节点样本太少导致过拟合二手车数据如果只有几万条这个值可以提到 50。4.3 weight_mix.csv 的用途推测与验证项目里有个weight_mix.csv从命名看是权重混合文件常见于模型融合场景——把多个模型的预测结果按权重加权。如果model.py里有多模型输出这个文件可能存的是各模型权重。# 读取权重文件检查格式 weight_df pd.read_csv(weight_mix.csv) print(weight_df.head()) print(weight_df.columns.tolist())如果它是model_name, weight两列那融合逻辑就是final_pred sum(w_i * pred_i)。验证方法把权重全设成相等看融合结果是否和单模型接近再调权重看验证集 RMSE 是否下降。如果权重文件是空的或者格式对不上说明它可能是训练过程中生成的中间产物不是必须的输入可以先忽略专注单模型调优。5. 避坑与排查跑这份源码最容易翻车的五个地方这一章是我实际跑类似项目时踩过的坑按「现象 → 原因 → 解决」写每条都对应这份源码的具体文件。5.1 现象main.py报 FileNotFoundError找不到 CSV原因源码里写的是相对路径比如pd.read_csv(data/used_car_train_20200313.csv)但你解压后 CSV 在根目录或者data文件夹名不对。项目正文里 CSV 和 ZIP 是并列的说明数据可能没解压。解决先确认used_car_train_20200313.zip是否已解压出 CSV。如果没解压用unzip used_car_train_20200313.zip解压。然后检查main.py里的路径把data/前缀去掉或者把 CSV 移进data/文件夹。最稳的办法是在代码开头加import os; print(os.getcwd())确认工作目录。5.2 现象训练时 RMSE 是 nan或者预测结果全是同一个值原因特征矩阵里有 inf 或 nan。常见于做了除法特征比如价格/里程但里程为 0或者 log 变换时遇到 0 或负数。解决在喂给模型前加一步清洗。X X.replace([np.inf, -np.inf], np.nan) X X.fillna(0) # 或者用列中位数另外检查price列有没有 0 或负数回归目标如果是 log 变换后的预测完要np.exp还原。5.3 现象LightGBM 报Do not support special JSON characters in feature name原因特征列名里有中文、空格或特殊符号。二手车数据里品牌名可能是中文做 One-Hot 后列名变成brand_宝马这种。解决统一重命名特征列。X.columns [ff{i} for i in range(X.shape[1])]或者用re.sub把非字母数字下划线的字符替换掉。这个报错在 LightGBM 里很常见XGBoost 相对宽松但统一重命名是最省事的做法。5.4 现象线下验证分数很好但测试集预测结果明显不合理原因特征工程里用了测试集信息。比如标准化时对全量数据fit或者填充缺失值时用了测试集中位数。这种泄漏会让线下分数虚高。解决所有fit操作只在训练集上做测试集只transform。检查代码里有没有pd.concat([train, test])之后做统计量的操作如果有改成先算训练集统计量再应用到测试集。5.5 现象utlis.py里的函数导入报错ModuleNotFoundError原因utlis.py这个文件名拼写可能是故意的utils 的变体但导入时写成了from utils import ...或者文件在子目录里没加__init__.py。解决确认文件名到底是utlis.py还是utils.py导入语句要和文件名完全一致。如果文件在子目录用sys.path.append(子目录路径)或者改成相对导入。项目里还有__pycache__和.pyc文件说明之前跑过如果报错和缓存有关删掉__pycache__重跑。6. 进阶技巧用交叉验证和特征重要性把分数再压一压单次train_test_split的验证分数波动大尤其是数据量不大的时候。想稳一点用 K 折交叉验证。下面这个写法可以直接替换model.py里的训练部分。from sklearn.model_selection import KFold import lightgbm as lgb import numpy as np kf KFold(n_splits5, shuffleTrue, random_state42) oof_pred np.zeros(len(X)) test_pred np.zeros(len(test)) for fold, (tr_idx, val_idx) in enumerate(kf.split(X)): X_tr, X_val X.iloc[tr_idx], X.iloc[val_idx] y_tr, y_val y.iloc[tr_idx], y.iloc[val_idx] dtrain lgb.Dataset(X_tr, labely_tr) dval lgb.Dataset(X_val, labely_val, referencedtrain) model lgb.train( params, dtrain, num_boost_round2000, valid_sets[dval], callbacks[lgb.early_stopping(100), lgb.log_evaluation(0)] ) oof_pred[val_idx] model.predict(X_val) test_pred model.predict(test[feature_cols]) / kf.n_splits rmse np.sqrt(np.mean((oof_pred - y) ** 2)) print(f5-Fold OOF RMSE: {rmse:.2f})shuffleTrue打乱顺序避免数据按时间排序导致折间分布不一致。oof_pred存的是每个样本在验证折上的预测用它算 RMSE 比单次划分更可靠。test_pred是 5 个模型预测的平均相当于模型融合通常比单模型稳。特征重要性可以直接从训练好的模型里拿用来做特征筛选。importance pd.DataFrame({ feature: feature_cols, gain: model.feature_importance(importance_typegain) }).sort_values(gain, ascendingFalse) print(importance.head(20))importance_typegain比默认的split更能反映特征对降低损失的贡献。如果发现前 20 个特征里有一半是匿名特征说明匿名列确实有信息量别删。如果某个原始字段比如name重要性极低可以考虑去掉减少噪声。最后说个习惯我每次跑这类源码都会先把main.py从头到尾读一遍把里面硬编码的路径、参数、随机种子全部标出来改成可配置的。这份源码的readme.txt和 Markdown 文档里可能有运行说明先看文档再动手比直接python main.py然后对着报错猜要快得多。从那以后我每次拿到新数据都强制先跑一遍head()和describe()确认字段和分布再写模型。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑