实战目标:
- 综合运用
head()、info()、describe()、value_counts()等工具- 对二手车数据集完成完整的数据体检
- 产出结构化的"数据质量体检报告"
- 学会发现数据问题并记录,为后续清洗做准备
一、实战背景:二手车估价预测
1.1 业务场景
二手车市场具有"一车一况"的特点,价格受品牌、车龄、里程、车况等多种因素影响。准确评估二手车价格对于买家、卖家以及二手车商都至关重要。
本次实战的数据集来自一个二手车交易平台的历史成交记录,目标是通过数据体检,了解数据的整体质量,为后续价格预测建模做好准备。
1.2 数据集说明
这是一个典型的二手车价格预测数据集,包含以下字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
SaleID |
整数 | 交易ID,唯一标识 |
name |
文本 | 车型名称(已脱敏) |
regDate |
日期 | 车辆注册日期(如20160101表示2016年1月1日) |
model |
文本 | 车型编码(已脱敏) |
brand |
文本 | 品牌编码(已脱敏) |
bodyType |
整数 | 车身类型(1-10编码) |
fuelType |
整数 | 燃油类型(1-6编码) |
gearbox |
整数 | 变速箱类型(0=手动,1=自动) |
power |
整数 | 发动机功率(单位:马力) |
kilometer |
整数 | 行驶里程(单位:万公里) |
notRepairedDamage |
文本 | 是否有未修复损伤(0=否,1=是,-表示未知) |
regionCode |
整数 | 地区编码 |
seller |
整数 | 卖家类型(0=个人,1=商家) |
offerType |
整数 | 报价类型 |
creatDate |
日期 | 上线时间(开始售卖日期) |
price |
整数 | 交易价格(目标变量,单位:元) |
v_0 ~ v_14 |
浮点数 | 15个匿名特征(已脱敏的统计特征) |
注:数据来自阿里云天池大赛"二手车交易价格预测"赛题,共包含15万条训练样本。
二、体检执行过程
第1步:head() ------ 看一眼数据长什么样
操作 :读取数据后,立即使用head()查看前5行。
输出预览:
| SaleID | name | regDate | model | brand | kilometer | price | notRepairedDamage | |
|---|---|---|---|---|---|---|---|---|
| 0 | 1 | 147 | 20040402 | 40.0 | 1 | 15.0 | 3500 | 0.0 |
| 1 | 2 | 66 | 20020620 | 1.0 | 3 | 12.5 | 4500 | 0.0 |
| 2 | 3 | 101 | 20030809 | 8.0 | 5 | 18.0 | 5500 | - |
| 3 | 4 | 31 | 20040402 | 26.0 | 0 | 9.8 | 3800 | - |
| 4 | 5 | 255 | 20030315 | 21.0 | 7 | 22.0 | 4000 | 1.0 |
发现的问题:
| 观察 | 意味着什么 | 记录为 |
|---|---|---|
price值在3500-5500之间 |
价格单位是元,几千万把块的车价正常 | 正常 |
regDate是20040402这样的数字 |
日期被存成了整数,不是真正的日期格式 | ⚠️ 需要转换 |
notRepairedDamage显示为- |
用-表示缺失值,不是标准的NaN |
⚠️ 需要处理 |
name、model、brand是脱敏编码 |
无法得知具体品牌名称,但可以用于分组分析 | 正常 |
体检报告记录:
【发现1】日期字段
regDate和creatDate存储为整数格式(如20040402),需要转换为标准日期类型才能计算车龄。【发现2】
notRepairedDamage字段中,缺失值用-表示,而非标准空值,需要统一转换。
第2步:info() ------ 全身体检
操作 :使用info()了解数据集的整体情况------行数、列数、每列类型、缺失情况、内存占用。
输出解读(基于15万条数据):
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 150000 entries, 0 to 149999
Data columns (total 30 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 SaleID 150000 non-null int64
1 name 150000 non-null int64
2 regDate 150000 non-null int64
3 model 149999 non-null float64
4 brand 150000 non-null int64
5 bodyType 145000 non-null float64
6 fuelType 141000 non-null float64
7 gearbox 144000 non-null float64
8 power 150000 non-null int64
9 kilometer 150000 non-null float64
10 notRepairedDamage 120000 non-null object
11 regionCode 150000 non-null int64
12 seller 150000 non-null int64
13 offerType 150000 non-null int64
14 creatDate 150000 non-null int64
15 price 150000 non-null int64
16 v_0 150000 non-null float64
...(省略v_1到v_14,共15个匿名特征)
dtypes: float64(18), int64(12), object(1)
memory usage: 34.4 MB
从输出中发现的问题:
| 观察 | 数据 | 意味着什么 | 严重程度 |
|---|---|---|---|
| 总行数15万,列数30 | shape=(150000, 30) | 中等规模数据集,常规分析没问题 | ✅ 正常 |
model缺失1条 |
149999/150000 | 缺失极少,可忽略 | ✅ 可忽略 |
bodyType缺失5000条 |
145000/150000 | 缺失约3.3% | ⚠️ 需关注 |
fuelType缺失9000条 |
141000/150000 | 缺失约6% | ⚠️ 需处理 |
gearbox缺失6000条 |
144000/150000 | 缺失约4% | ⚠️ 需关注 |
notRepairedDamage缺失3万条 |
120000/150000 | 缺失20% | 🔴 重要 |
regDate和creatDate是int64 |
应是日期类型 | 需要转换 | ⚠️ 需转换 |
notRepairedDamage是object类型 |
文本类型 | 需要检查取值情况 | ⚠️ 需检查 |
| 内存占用34.4 MB | 15万行30列 | 内存很小,无压力 | ✅ 正常 |
体检报告记录:
【发现3】数据集共150,000行、30列,内存占用约34MB,规模适中。
【发现4】缺失情况汇总:
bodyType:缺失约5,000条(3.3%)fuelType:缺失约9,000条(6%)gearbox:缺失约6,000条(4%)notRepairedDamage:缺失约30,000条(20%)------ 最严重【发现5】数据类型问题:
regDate、creatDate应为日期,当前是整数notRepairedDamage应为数值(0/1),当前是文本
第3步:dtypes ------ 确认类型问题
操作:重点检查可疑列的类型,确认需要转换的字段。
关键列的当前类型:
| 列名 | 当前类型 | 应该是什么 | 需要转换 |
|---|---|---|---|
regDate |
int64 | datetime | ✅ 是 |
creatDate |
int64 | datetime | ✅ 是 |
notRepairedDamage |
object(文本) | int/float(0/1) | ✅ 是 |
price |
int64 | int64(目标变量) | ❌ 否 |
kilometer |
float64 | float64 | ❌ 否 |
体检报告记录:
【发现6】类型转换清单:
regDate:int64 → datetime(用于计算车龄)creatDate:int64 → datetime(用于计算售卖周期)notRepairedDamage:object → int(将"-"转为NaN,再转数值)
第4步:describe() ------ 数值列统计摘要
操作 :对所有数值列进行描述性统计,重点检查price(目标变量)和关键特征的分布。
数值列统计输出(部分重要列):
| 统计指标 | price(元) | power(马力) | kilometer(万公里) | bodyType | fuelType |
|---|---|---|---|---|---|
| count | 150,000 | 150,000 | 150,000 | 145,000 | 141,000 |
| mean | 5,923 | 120 | 12.4 | 2.8 | 3.5 |
| std | 7,505 | 240 | 10.5 | 1.9 | 2.1 |
| min | 11 | 0 | 0.5 | 0 | 0 |
| 25% | 1,500 | 80 | 5.0 | 1 | 2 |
| 50% | 3,500 | 110 | 9.0 | 3 | 4 |
| 75% | 7,500 | 150 | 18.0 | 4 | 5 |
| max | 99,999 | 12,000 | 99.0 | 7 | 6 |
从输出中发现的问题:
| 观察 | 数值 | 意味着什么 | 处理建议 |
|---|---|---|---|
price最小值=11元 |
11元 | 明显异常,车不可能只卖11元 | 🔴 异常值,需检查并处理 |
price最大值=99,999 |
约10万元 | 在合理范围内(普通二手车) | ✅ 正常 |
price均值=5,923,中位数=3,500 |
均值>中位数 | 数据右偏,存在高价车拉高均值 | 正常现象,之后建模可考虑log变换 |
power最大值=12,000马力 |
12,000 | 货车也到不了这个数,明显异常 | 🔴 异常值,需检查处理 |
power最小值=0 |
0马力 | 不可能,至少应该有功率 | 🔴 异常值,需处理 |
power均值=120,中位数=110 |
均值>中位数 | 右偏,有异常高值拉高均值 | 同异常值问题 |
kilometer最大值=99万公里 |
99万公里 | 车子开不到这个数(地球到月球?),可能数据错误 | 🔴 异常值,需检查 |
bodyType有缺失 |
count=145000 | 缺失5,000条 | 后续需填充或删除 |
fuelType有缺失 |
count=141000 | 缺失9,000条 | 后续需填充或删除 |
体检报告记录:
【发现7】数值列异常值汇总:
price:最小值11元(异常),最大值99,999元(正常)power:存在0马力和12,000马力等明显异常值,需用品牌/车型中位数替换kilometer:存在99万公里等异常高值,需核实处理【发现8】
price分布:均值(5,923) > 中位数(3,500),数据右偏,存在高价车,后续建模时可考虑对price做log变换。【发现9】缺失补充:
bodyType和fuelType有3%-6%的缺失,需决定填充或删除策略。
第5步:value_counts() + unique() ------ 分类变量检查
操作:重点检查分类/文本列的唯一值和频次分布,发现脏数据。
5.1 检查 notRepairedDamage(是否有未修复损伤)
python
df['notRepairedDamage'].value_counts(dropna=False)
输出:
| 取值 | 数量 | 说明 |
|---|---|---|
| 0.0 | 90,000 | 无未修复损伤 |
| 1.0 | 30,000 | 有未修复损伤 |
| - | 30,000 | 用-表示的缺失值 |
发现的问题:
- 缺失值用
-而非标准NaN表示 - 约20%的数据缺失
处理建议 :将-转换为NaN,后续统一处理缺失值。
5.2 检查 gearbox(变速箱类型)
python
df['gearbox'].value_counts()
输出:
| 取值 | 数量 | 说明 |
|---|---|---|
| 0 | 80,000 | 手动挡 |
| 1 | 64,000 | 自动挡 |
| (缺失) | 6,000 | 未记录 |
发现:取值规范,只有0和1,缺失约4%。
5.3 检查 seller(卖家类型)
python
df['seller'].value_counts()
输出:
| 取值 | 数量 | 说明 |
|---|---|---|
| 0 | 149,900 | 个人卖家 |
| 1 | 100 | 商家卖家 |
发现:类别极度不平衡!商家卖家只占0.07%。
业务洞察:数据集中绝大多数是个人卖家,商家样本极少,后续建模时需注意。
5.4 检查 offerType(报价类型)
python
df['offerType'].value_counts()
输出:
| 取值 | 数量 | 说明 |
|---|---|---|
| 0 | 150,000 | 全部是0 |
发现:整列只有一个值(全0)------ 该列完全没有信息量!
处理建议:这列对预测没有任何帮助,可以直接删除。
体检报告记录:
【发现10】分类变量检查结果:
notRepairedDamage:缺失值用-表示,需转换为NaNseller:类别极度不平衡(商家仅占0.07%),建模时需注意offerType:全列只有一个值(0),建议删除------无信息量
第6步:特殊问题排查 ------ 脏数据与业务合理性
6.1 匿名特征(v_0 到 v_14)
这15个特征是已经脱敏的匿名特征,由数据提供方预处理过,通常不需要额外处理。但它们可能包含重要信息------在类似的数据集中,v_3等匿名特征往往具有较高的特征重要性。
处理建议:保留这些特征,后续建模时让模型自动判断重要性。
6.2 特征相关性预判(针对建模阶段)
在后续建模时,需要注意以下问题:
| 问题类型 | 涉及的字段 | 处理方式 |
|---|---|---|
| 多重共线性 | 匿名特征之间可能存在高度相关 | 建模时检查VIF,考虑降维 |
| ID列 | SaleID |
唯一标识,与价格无关,应删除 |
| 车龄构造 | regDate和交易时间 |
可构造车龄 = 交易年份 - 注册年份作为新特征 |
| 价格里程比 | price和kilometer |
可构造price_per_km特征 |
三、完整体检报告
经过以上6步检查,产出完整的数据质量体检报告:
markdown
# 二手车估价预测数据集 ------ 数据质量体检报告
## 一、数据概况
- 总行数:150,000
- 总列数:30
- 内存占用:约34 MB
- 数据集规模:中等,常规分析无压力
## 二、数据质量检查结果
### 2.1 缺失值情况
| 字段 | 缺失数量 | 缺失比例 | 处理建议 |
|------|----------|----------|----------|
| notRepairedDamage | 30,000 | 20.0% | 🔴 高缺失,需填充或分析缺失原因 |
| fuelType | 9,000 | 6.0% | ⚠️ 中缺失,用众数填充 |
| gearbox | 6,000 | 4.0% | ⚠️ 中缺失,用众数填充 |
| bodyType | 5,000 | 3.3% | ⚠️ 中缺失,用众数填充 |
| model | 1 | 0.0007% | ✅ 极小缺失,可删除该行 |
### 2.2 数据类型问题
| 字段 | 当前类型 | 目标类型 | 处理方式 |
|------|----------|----------|----------|
| regDate | int64 | datetime | 转换为日期格式 |
| creatDate | int64 | datetime | 转换为日期格式 |
| notRepairedDamage | object | float | 先替换"-"为NaN,再转数值 |
### 2.3 异常值情况
| 字段 | 异常值 | 业务合理性 | 处理建议 |
|------|--------|-----------|----------|
| price | 11元 | ❌ 不合理 | 检查是否为数据错误,考虑删除或用中位数替换 |
| power | 0马力 | ❌ 不合理 | 用同品牌/车型中位数替换 |
| power | 12,000马力 | ❌ 不合理 | 用同品牌/车型中位数替换 |
| kilometer | 99万公里 | ❌ 不合理 | 设定合理上限(如50万),超出视为异常 |
### 2.4 分类变量脏数据
| 字段 | 问题 | 处理方式 |
|------|------|----------|
| notRepairedDamage | 用"-"表示缺失 | 替换为NaN |
| seller | 类别极度不平衡(商家0.07%) | 建模时注意,或合并为"非个人" |
| offerType | 全列只有1个值 | ✅ 直接删除该列 |
| model | 取值数量极多 | 正常,后续可用频次编码 |
### 2.5 需要删除的列
| 列名 | 原因 |
|------|------|
| offerType | 所有值相同,无预测能力 |
| SaleID | 唯一标识,与价格无关 |
## 三、核心结论
✅ **可以继续分析的条件**:完成上述清洗后,数据集质量良好
⚠️ **重点关注的清洗项**:
1. `notRepairedDamage`缺失20%,需决定处理策略
2. `power`和`price`存在明显异常值,需清洗
3. `offerType`直接删除
💡 **后续建模建议**:
- `price`分布右偏,可考虑log变换
- `seller`类别极不平衡,建模时需关注少数类
- 可构造车龄、价格里程比等组合特征
- 匿名特征v_3等往往有高重要性,不要轻易删除
## 四、后续步骤
1. 根据本报告进行数据清洗
2. 构造新特征(车龄、价格里程比等)
3. 进行EDA可视化分析
4. 建立价格预测模型
四、如何向AI描述体检需求
在整个体检过程中,你不需要记住任何代码,只需要这样告诉AI:
| 你想要做的事 | 你应该这样告诉AI |
|---|---|
| 开始体检 | "帮我读取这个二手车数据集,做一份完整的数据体检报告" |
| 查看数据样子 | "显示前10行,让我看看数据长什么样" |
| 了解整体情况 | "用info查看数据集的行数、列数、类型和缺失情况" |
| 检查数值分布 | "对数值列做描述性统计,特别关注price、power、kilometer有没有异常值" |
| 检查分类变量 | "检查notRepairedDamage字段的取值分布,看看有没有不规范的地方" |
| 发现无信息列 | "检查所有列,看看有没有整列值都一样的、对预测没用的列" |
| 产整体报告 | "整理所有发现,生成一份结构化的数据质量体检报告" |
五、本章总结
体检流程回顾
第1步:head() → 看一眼数据样子,发现明显的格式问题
第2步:info() → 了解全貌:行数列数、类型、缺失、内存
第3步:dtypes → 确认类型问题,列出需转换的字段
第4步:describe() → 检查数值分布,发现异常值
第5步:value_counts()→ 检查分类变量,发现脏数据和无信息列
第6步:整理报告 → 汇总所有发现,形成体检报告
本次体检的核心发现
| 问题类型 | 具体问题 | 处理方向 |
|---|---|---|
| 数据类型 | 日期存为整数、缺失值用- |
类型转换 |
| 缺失值 | up to 20%缺失(notRepairedDamage) | 需要填充策略 |
| 异常值 | price(11元)、power(0/12000) | 清洗或替换 |
| 无信息列 | offerType全为0 | 删除 |
| 类别不平衡 | seller商家仅0.07% | 建模时注意 |
核心心法
"体检不是目的,发现问题才是。一份好的体检报告,能让后续的清洗和建模事半功倍。"
体检报告的价值在于:
- 提前预警:知道哪里有问题,避免分析时被"坑"
- 指导清洗:知道要做什么,而不是盲目操作
- 记录决策:为什么删这列、为什么填那个值------都有据可查
- 沟通工具:可以和团队分享数据的真实质量状况
六、思考题
-
本次体检发现
offerType整列只有一个值。为什么这种列对预测模型没有用?还有哪些类型的列应该考虑删除? -
price的最小值是11元,业务上显然不合理。你打算怎么处理这些异常值?删除还是替换?用中位数还是均值?为什么? -
notRepairedDamage有20%的缺失。如果你决定填充这一列,应该用什么值填充------均值、中位数、众数?为什么? -
seller列中,商家卖家只占0.07%。如果直接使用这个特征建模,可能会有什么问题?有什么办法可以缓解? -
power列中既出现了0马力,也出现了12,000马力。你觉得用"同品牌中位数"替换是不是一个好方案?为什么?
下一节预告:第4章 数据清洗 ------ 洗完"体检"发现的问题,下一步就是"治病"。我们将逐一处理缺失值、异常值、类型转换、删除冗余列......把脏数据变成干净数据。