第1章:数据读取与预处理——CSV/二进制文件解析、时间戳对齐、缺失值处理、异常值检测
大家好,欢迎来到《传感器数据采集与滤波算法精讲》。
我是你们这门课的主讲。做了十几年嵌入式,跟传感器数据打了太多交道。说实话,数据采集这步看着简单,但坑最多。我见过太多项目,算法选得挺好,最后栽在数据预处理上。
这一章,咱们就聊聊数据进来之后,第一件事该做什么。
1.1 数据读取:CSV与二进制文件解析
传感器数据存成什么格式?最常见的就是CSV和二进制。CSV直观,二进制省空间。但各有各的脾气。
1.1.1 CSV文件解析
CSV说白了就是逗号分隔的文本。Python里用pandas.read_csv()最顺手。但我建议你,别直接一把梭。
核心要点:先看文件头,再确定分隔符,最后处理数据类型。
import pandas as pd
# 我个人习惯先预览前几行
df = pd.read_csv('sensor_data.csv', nrows=5)
print(df.head())
# 确认分隔符,有时候是制表符\t或者分号;
df = pd.read_csv('sensor_data.csv', sep=',', header=0)
# 指定数据类型,避免pandas猜错
dtype_dict = {
'timestamp': 'str',
'accel_x': 'float32',
'accel_y': 'float32',
'accel_z': 'float32'
}
df = pd.read_csv('sensor_data.csv', dtype=dtype_dict)
小技巧:我在项目中遇到过,CSV里混了中文字符,导致编码报错。加个encoding='utf-8'或者encoding='gbk'试试。
1.1.2 二进制文件解析
二进制文件,嗯,这里要注意。它没有自描述性,你必须知道协议。
我曾经接手过一个项目,前任工程师离职了,二进制文件格式没留文档。我对着十六进制看了三天,才把数据结构猜出来。你想想看,这多耽误事。
常见的二进制格式有:
- 固定长度记录:每条数据长度相同,直接按字节偏移读取
- 可变长度记录:每条数据前有个长度字段
- 带时间戳的原始帧:通常包含帧头、数据体、校验和
import struct
import numpy as np
def parse_binary_sensor(file_path):
"""
解析固定长度的传感器二进制数据
假设每条记录:4字节时间戳 + 4字节float x + 4字节float y + 4字节float z
"""
records = []
record_size = 16 # 4 + 4 + 4 + 4
with open(file_path, 'rb') as f:
while True:
chunk = f.read(record_size)
if len(chunk) < record_size:
break
# 解包:I 无符号整型,3f 三个float
timestamp, x, y, z = struct.unpack('<I3f', chunk)
records.append((timestamp, x, y, z))
return np.array(records)
注意:字节序!大端还是小端?我吃过这个亏。用struct.unpack('<I3f', ...)里的<表示小端,>表示大端。拿不准就两种都试试,看哪个数据合理。
1.2 时间戳对齐——多传感器融合的第一步
多传感器系统里,每个传感器有自己的时钟。GPS可能用UTC,IMU用本地晶振,摄像头用帧率。时间戳不对齐,后面所有算法都是白搭。
说白了,时间戳对齐就是让所有数据站在同一个时间轴上。
1.2.1 时间戳归一化
先把所有时间戳转成统一格式。我个人习惯用Unix时间戳(秒或毫秒)。
import pandas as pd
# 假设CSV里时间戳是字符串 '2024-01-15 10:30:25.123'
df['timestamp'] = pd.to_datetime(df['timestamp'])
# 转成Unix时间戳(秒)
df['timestamp_unix'] = df['timestamp'].astype('int64') / 1e9
1.2.2 插值对齐
两个传感器采样率不同怎么办?比如IMU是100Hz,GPS是10Hz。你需要把GPS数据插值到IMU的时间轴上。
import numpy as np
def align_timestamps(source_time, source_data, target_time):
"""
将source_data插值到target_time的时间轴上
"""
aligned = np.interp(target_time, source_time, source_data)
return aligned
# 使用示例
imu_time = np.array([0.0, 0.01, 0.02, 0.03, ...]) # 100Hz
gps_time = np.array([0.0, 0.1, 0.2, 0.3, ...]) # 10Hz
gps_lat = np.array([...])
# 把GPS纬度插值到IMU时间轴上
gps_lat_aligned = align_timestamps(gps_time, gps_lat, imu_time)
避坑指南:我曾经直接用线性插值处理GPS数据,结果在转弯时位置偏差很大。后来改用样条插值,效果好了很多。对于运动剧烈的数据,线性插值不够用。
1.3 缺失值处理——数据不完整怎么办?
传感器掉线、通信丢包、存储故障,都会导致数据缺失。你想想看,一个时间序列里突然少了几行,算法直接崩了。
常见的缺失值处理方法:
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 直接删除 | 缺失比例很小(<5%) | 简单快速 | 可能丢失有用信息 |
| 前向填充 | 缓慢变化的信号 | 保持连续性 | 引入滞后 |
| 线性插值 | 近似线性变化 | 平滑过渡 | 对突变不友好 |
| 样条插值 | 非线性变化 | 更自然 | 计算量大 |
| 模型预测 | 有规律的数据 | 精度高 | 实现复杂 |
import pandas as pd
import numpy as np
# 检测缺失值
print(df.isnull().sum())
# 方法1:直接删除
df_clean = df.dropna()
# 方法2:前向填充
df['accel_x'].fillna(method='ffill', inplace=True)
# 方法3:线性插值
df['accel_x'].interpolate(method='linear', inplace=True)
# 方法4:限制连续缺失长度
# 如果连续缺失超过5个点,就不插值了
df['accel_x'] = df['accel_x'].interpolate(limit=5, limit_direction='both')
警告:别盲目插值!如果一段数据连续缺失超过总长度的20%,插值结果基本不可信。我建议你标记这些区间,在后续分析中降低权重。
1.4 异常值检测——揪出捣乱的数据点
传感器被撞了一下、电磁干扰、ADC饱和,都会产生异常值。这些点如果不处理,滤波算法会被带偏。
异常值检测,说白了就是找那些「不合理」的点。
1.4.1 基于统计的方法
最常用的是3σ原则和四分位距法。
import numpy as np
def detect_outliers_3sigma(data, threshold=3):
"""
基于3σ原则检测异常值
"""
mean = np.mean(data)
std = np.std(data)
lower = mean - threshold * std
upper = mean + threshold * std
outliers = (data < lower) | (data > upper)
return outliers
def detect_outliers_iqr(data, multiplier=1.5):
"""
基于四分位距检测异常值
"""
q1 = np.percentile(data, 25)
q3 = np.percentile(data, 75)
iqr = q3 - q1
lower = q1 - multiplier * iqr
upper = q3 + multiplier * iqr
outliers = (data < lower) | (data > upper)
return outliers
1.4.2 基于物理约束的方法
这个方法更靠谱。你知道传感器的量程,超出量程的就是异常。
def detect_outliers_physical(data, min_val, max_val):
"""
基于物理量程检测异常值
"""
outliers = (data < min_val) | (data > max_val)
return outliers
# 例如:加速度计量程±16g
accel_outliers = detect_outliers_physical(df['accel_x'], -16.0, 16.0)
1.4.3 基于变化率的方法
有些异常值在量程内,但变化率离谱。比如温度传感器,一秒内跳了50度,这肯定有问题。
def detect_outliers_rate(data, max_rate):
"""
基于变化率检测异常值
"""
diff = np.abs(np.diff(data))
# 第一个点没法判断,默认正常
outliers = np.concatenate(([False], diff > max_rate))
return outliers
# 温度变化率不超过5度/秒
temp_outliers = detect_outliers_rate(df['temperature'], 5.0)
经验之谈:我建议你把多种方法结合起来。先用物理约束筛一遍,再用统计方法筛一遍。对于筛出来的异常值,别直接删除,先看看是不是有规律。我曾经发现一批「异常值」其实是传感器进入了自检模式,数据本身是正常的。
1.5 实战建议——数据预处理的流水线
好了,知识点讲完了。咱们串一下,一个完整的数据预处理流水线应该怎么做。
- 读取原始数据:CSV用pandas,二进制用struct
- 时间戳归一化:统一转成Unix时间戳
- 多传感器对齐:以最高采样率的传感器为基准,插值对齐
- 缺失值处理:先看缺失比例,小则删除,大则插值
- 异常值检测:物理约束 + 统计方法 + 变化率,三重保险
- 数据保存:处理好的数据存成新文件,别覆盖原始数据
嗯,这一章就到这里。下一章咱们聊聊时域滤波,从最简单的移动平均开始。
记住一句话:数据预处理做得好,后面滤波事半功倍。预处理偷懒,后面全是坑。