第四章:数据探索与清洗——让原始数据“开口说话”
大家好,欢迎来到第四章。说实话,这一章可能是整个课程里最“脏”最“累”的活,但也是我最喜欢的一章。为什么?因为数据清洗做得好,后面建模调参就像在高速上开巡航;做得不好,嗯,你懂的,就像在烂路上开跑车,再好的算法也白搭。
我个人习惯,拿到ADAS数据的第一件事,不是急着画图,也不是跑模型。而是先坐下来,泡杯茶,好好看看这些数据到底长什么样。你想想看,连数据长啥样都不知道,你怎么敢用它来训练模型?
4.1 数据概览:info() 和 describe() 是你的“第一印象”
在Python里,pandas库给了我们两个非常趁手的工具:info() 和 describe()。这两个函数,说白了就是数据的“身份证”和“体检报告”。
import pandas as pd
import numpy as np
# 假设我们加载了一段ADAS的CAN总线数据
df = pd.read_csv('adas_can_data.csv')
# 先看基本信息
print(df.info())
info() 会告诉你:有多少行、多少列、每列的数据类型、有没有空值。我在项目中遇到过,有一次数据量特别大,几百万行,结果 info() 一跑,发现时间戳列被读成了object类型。这就是个大坑——时间戳不对齐,后面所有分析都是错的。
# 再看统计描述
print(df.describe())
describe() 会给出数值列的统计信息:均值、标准差、最小值、最大值、四分位数。这里有个小技巧:我一般会特别关注“min”和“max”这两列。如果某个传感器的速度最小值是-999,或者最大值是99999,那基本可以断定——数据里有异常值。
重要提醒: 不要只看describe()的均值!均值很容易被异常值“带偏”。比如有一次,一个激光雷达的点云数量均值是5000,但实际大部分时间只有2000,就是因为有几帧数据异常飙到了50000。所以,一定要结合四分位数来看。
4.2 缺失值处理:别让“空”坑了你
ADAS数据里,缺失值太常见了。摄像头被遮挡、雷达信号丢失、GPS信号弱……都会导致数据缺失。处理缺失值,我一般分三步走。
4.2.1 先定位缺失
# 查看每列的缺失数量
print(df.isnull().sum())
# 查看缺失比例
print(df.isnull().sum() / len(df) * 100)
我曾经遇到过一个项目,某个超声波传感器的数据缺失率高达40%。一开始大家都没注意,结果模型训练出来,在泊车场景下表现极差。后来一查,原来那段时间传感器硬件有故障。所以,缺失比例超过20%的列,我建议你慎重考虑要不要用它。
4.2.2 再决定策略
处理缺失值,常用的方法有几种:
- 直接删除: 如果缺失比例很小(比如<5%),而且数据量够大,直接删掉缺失的行。简单粗暴,但有效。
- 填充: 用均值、中位数、或者前向/后向填充。我个人习惯,对于时间序列数据,比如车速、加速度,用前向填充(ffill)效果最好。因为物理量是连续的,前一时刻的值最接近当前时刻。
- 插值: 对于更精细的场景,比如需要平滑的轨迹,我会用线性插值或样条插值。
# 前向填充
df['vehicle_speed'].fillna(method='ffill', inplace=True)
# 线性插值
df['vehicle_speed'].interpolate(method='linear', inplace=True)
我的小技巧: 填充之前,先画个图看看缺失的模式。如果缺失是随机的,填充没问题。但如果缺失是成片出现的(比如连续100ms都没有数据),那填充出来的数据其实不可靠。这时候,我宁愿把这整段数据标记为“不可用”,也不强行填充。
4.3 异常值检测:揪出那些“捣乱”的数据点
异常值,说白了就是那些明显不合理的数据。比如,一辆车的速度突然从60km/h跳到600km/h,或者GPS坐标突然漂移到几百公里外。这些数据如果不处理,模型会被它们“带沟里”。
4.3.1 基于统计的方法
最常用的就是3σ原则。如果数据服从正态分布,那么超过均值±3倍标准差的值,就可以认为是异常。
# 3σ异常检测
mean = df['vehicle_speed'].mean()
std = df['vehicle_speed'].std()
df['is_outlier'] = (np.abs(df['vehicle_speed'] - mean) > 3 * std)
但注意,ADAS数据很多并不服从正态分布。比如,车速的分布往往是偏态的,大部分时间在低速,偶尔高速。这时候用3σ可能会把正常的高速行驶误判为异常。所以,我更喜欢用IQR(四分位距)方法。
# IQR方法
Q1 = df['vehicle_speed'].quantile(0.25)
Q3 = df['vehicle_speed'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
df['is_outlier'] = (df['vehicle_speed'] < lower_bound) | (df['vehicle_speed'] > upper_bound)
4.3.2 基于物理约束的方法
这个方法,我个人觉得最靠谱。因为ADAS数据有明确的物理意义。比如:
- 车速不可能为负(除非倒车,但倒车也有合理范围)
- 加速度一般不超过±10 m/s²(急刹车也就8 m/s²左右)
- 横摆角速度一般不超过±1 rad/s
# 物理约束过滤
df_valid = df[
(df['vehicle_speed'] >= 0) &
(df['vehicle_speed'] <= 200) &
(df['acceleration'] >= -10) &
(df['acceleration'] <= 10)
]
避坑指南: 我曾经犯过一个错误——把所有异常值都删掉了。结果发现,有些“异常值”其实是真实发生的极端工况,比如紧急避障时的急转向。这些数据恰恰是模型需要学习的“边缘案例”。所以,我的建议是:先标记异常值,不要急着删除。等后面做场景分类时,再决定是保留还是剔除。
4.4 时间戳对齐:让所有传感器“步调一致”
ADAS系统里,不同传感器的采样频率是不一样的。摄像头通常是30fps(约33ms一帧),毫米波雷达是20fps(50ms一帧),激光雷达可能是10fps(100ms一帧)。这些数据要融合分析,必须先把时间戳对齐。
4.4.1 统一时间基准
首先,确保所有时间戳都转换到同一个时基。我一般用Unix时间戳(从1970年1月1日开始的秒数),精度到毫秒。
# 转换时间戳
df['timestamp'] = pd.to_datetime(df['timestamp'])
df['timestamp_unix'] = df['timestamp'].astype(np.int64) // 10**6 # 转为毫秒
4.4.2 重采样对齐
对齐的核心方法是重采样。比如,我想把所有数据都对齐到50ms的间隔:
# 设置时间索引
df.set_index('timestamp', inplace=True)
# 重采样到50ms,用前向填充
df_resampled = df.resample('50ms').ffill()
这里要注意,重采样会引入误差。如果原始数据是33ms一帧,重采样到50ms,意味着有些帧会被“跳过”,有些帧会被“重复”。对于要求不高的分析,这没问题。但如果要做精确的碰撞时间计算,我建议用插值而不是填充。
# 用插值对齐
df_resampled = df.resample('50ms').interpolate(method='linear')
核心要点: 时间戳对齐没有“完美”的方案。你只能在精度和计算量之间做权衡。我个人习惯,对于控制类算法(如AEB),用插值;对于分析类任务(如场景统计),用前向填充就够了。
4.5 实战小贴士:一个完整的清洗流程
好了,说了这么多,我给大家总结一个我常用的清洗流程。你可以把它当作一个模板:
- 加载数据,用info()和describe()快速概览
- 检查缺失值,根据缺失比例和模式决定处理策略
- 用物理约束和统计方法检测异常值,先标记不删除
- 统一时间戳格式,重采样到目标频率
- 最后再跑一次describe(),确认清洗效果
嗯,这一章的内容就到这里。数据清洗确实繁琐,但它是所有后续分析的基础。你想想看,如果地基没打好,上面盖的房子再漂亮,也经不起风吹雨打。下一章,我们会进入特征工程,把清洗好的数据变成模型真正能“吃”的养料。到时候见!