1. 项目概述
"天池二手车价格预测赛"是阿里云旗下天池平台举办的经典数据科学竞赛项目。作为一名参加过三届天池竞赛的老兵,我发现这个赛题特别适合用来练习数据清洗和探索性数据分析(EDA)的基本功。不同于其他结构化数据预测任务,二手车价格数据具有几个鲜明特点:字段类型混杂(数值、类别、文本都有)、缺失值普遍、异常值频现,还有大量需要人工判断的特征相关性。
我在实际参赛过程中总结出一套高效的EDA工作流,能够用Python在2小时内完成从原始数据到可建模状态的转换。这个工作流后来被我复用到多个工业级数据预测项目中,效果稳定可靠。下面就以比赛数据集为例,手把手演示如何系统化地进行数据清洗和EDA。
2. 数据清洗实战
2.1 原始数据质量诊断
拿到比赛提供的used_car_train_20200331.csv文件后,我习惯先用"三看"法快速评估数据质量:
python复制import pandas as pd
df = pd.read_csv('used_car_train_20200331.csv')
# 一看数据结构
print(df.shape) # (150000, 31)
print(df.dtypes)
# 二看缺失情况
missing_ratio = df.isnull().sum()/len(df)
print(missing_ratio[missing_ratio>0].sort_values(ascending=False))
# 三看数值分布
print(df.describe())
通过这个快速检查,我发现几个典型问题:
- 车身颜色(colour)缺失率达30%
- 发动机功率(power)存在0值和700+的异常值
- 注册日期(regDate)与创建日期(creatDate)存在时间逻辑矛盾
- 部分数值特征存在量纲差异(如power的max=65535, min=0)
2.2 缺失值处理方案
对于二手车数据,我推荐采用分类型处理的策略:
-
高缺失率特征(>15%):
直接删除colour字段,因为30%的缺失率已经影响统计显著性。但要注意:在业务场景中需要先确认该字段是否关键特征。 -
中等缺失率特征(5%-15%):
如notRepairedDamage(修复记录),采用众数填充并新增缺失指示列:python复制df['notRepairedDamage_missing'] = df['notRepairedDamage'].isnull() df['notRepairedDamage'].fillna('unknown', inplace=True) -
低缺失率特征(<5%):
如model(车型),使用同类目下的中位数填充:python复制df['model'] = df.groupby('brand')['model'].apply(lambda x: x.fillna(x.median()))
注意:千万不要对测试集和训练集分别计算填充值!必须在全局或者按训练集统计量填充,这是我在第一次参赛时踩过的大坑。
2.3 异常值清洗技巧
二手车数据的异常值往往包含重要业务信息,我采用分位数和业务规则结合的方法:
python复制# 功率异常处理
Q1 = df['power'].quantile(0.01)
Q99 = df['power'].quantile(0.99)
df['power'] = df['power'].clip(Q1, Q99)
# 价格异常处理(目标变量)
df = df[df['price'] > 1000] # 删除低于1000元的记录
df = df[df['price'] < df['price'].quantile(0.99)] # 去除top1%高价车
# 日期逻辑校验
df = df[df['creatDate'] >= df['regDate']] # 创建日期不应早于注册日期
这里有个重要经验:对于price这种目标变量,建议先做初步清洗再深入分析,避免异常值干扰后续EDA判断。
3. 探索性数据分析(EDA)
3.1 单变量分析
我习惯用seaborn的displot和boxplot快速把握数据分布:
python复制import seaborn as sns
import matplotlib.pyplot as plt
# 价格分布(取对数后更明显)
plt.figure(figsize=(12,5))
plt.subplot(121)
sns.distplot(np.log1p(df['price']))
plt.subplot(122)
sns.boxplot(y='price', data=df)
plt.show()
从图中可以发现:
- 价格呈右偏分布,取对数后接近正态
- 存在少量超高报价车辆(箱线图上方的离散点)
3.2 特征相关性分析
二手车价格受多重因素影响,我采用热力图+散点图矩阵组合分析:
python复制# 数值特征相关性
corr = df[['price','power','kilometer','v_0','v_1','v_2','v_3']].corr()
sns.heatmap(corr, annot=True)
# 类别特征分析
plt.figure(figsize=(10,6))
sns.boxplot(x='bodyType', y='price', data=df)
plt.xticks(rotation=45)
关键发现:
- kilometer(行驶里程)与price呈现明显负相关
- power(功率)在200-300区间时价格最高
- bodyType为3(SUV)的车型溢价明显
3.3 时间特征工程
注册日期(regDate)是宝藏特征,我通常分解为多个维度:
python复制df['regYear'] = df['regDate'].dt.year
df['regMonth'] = df['regDate'].dt.month
df['carAge'] = (df['creatDate'] - df['regDate']).dt.days / 365
# 可视化车龄与价格关系
sns.jointplot(x='carAge', y='price', data=df, kind='hex')
这个分析揭示了一个有趣现象:3-5年车龄的二手车性价比最高,10年以上车龄的价格曲线出现翘尾(可能是收藏车型)。
4. 特征构造经验
4.1 基于业务知识的特征
根据汽车行业经验,我构造了几个关键特征:
python复制# 品牌溢价特征
brand_avg = df.groupby('brand')['price'].mean().to_dict()
df['brand_premium'] = df['brand'].map(brand_avg)
# 车型稀缺性
model_count = df['model'].value_counts().to_dict()
df['model_rarity'] = df['model'].map(model_count)
# 功率重量比(假设平均车重)
df['power_weight_ratio'] = df['power'] / 1.5
4.2 特征组合策略
通过特征交叉发现非线性关系:
python复制# 品牌+车龄组合
df['brand_age'] = df['brand'].astype(str) + '_' + (df['carAge']//2).astype(str)
# 地区+车型组合
df['region_model'] = df['regionCode'].astype(str) + '_' + df['model'].astype(str)
特别注意:组合特征会导致维度爆炸,建议先用target encoding编码后再使用。
5. 常见问题排查
5.1 内存优化技巧
当数据量较大时,我用这些方法节省内存:
python复制# 优化数据类型
df['bodyType'] = df['bodyType'].astype('int8')
df['brand'] = df['brand'].astype('category')
# 分块读取
chunks = pd.read_csv('big_data.csv', chunksize=50000)
df = pd.concat([chunk_process(c) for c in chunks])
5.2 稳定性验证
为确保EDA结果可靠,我必做两个验证:
- 时间维度验证:比较不同年份的数据分布是否一致
- 抽样一致性:多次随机抽样检查统计量波动范围
5.3 比赛专用技巧
在天池比赛中我发现几个关键点:
- 测试集的时间范围可能超出训练集,需要外推处理
- 部分字段在测试集中出现了训练集未见的类别
- 线上评测指标(MAE)对异常值敏感,需要特别处理
6. 完整代码结构建议
这是我总结的标准工作流模板:
code复制二手车EDA工作流/
├── data/
│ ├── raw/ # 原始数据
│ └── processed/ # 处理后数据
├── notebooks/
│ ├── 01_data_cleaning.ipynb
│ ├── 02_eda.ipynb
│ └── 03_feature_eng.ipynb
└── src/
├── clean_utils.py # 清洗函数
└── eda_utils.py # 分析工具
每个notebook都应包含清晰的Markdown说明和版本控制,这是我团队协作时血的教训换来的经验。
