第一步实战:Python实现数据预处理

好了,终于到了动手环节。前面三章我们聊了抖动是什么、为什么要优化、以及整体三步走的框架。现在,咱们把袖子撸起来,真正写代码。

我个人习惯,拿到任何轨迹数据,第一件事不是急着滤波,而是先看看数据长什么样。你想想看,连数据质量都不清楚,后面再怎么调参数都是瞎忙活。

4.1 环境准备与数据加载

先列一下我常用的库。说实话,做轨迹处理,这几个基本就够了:

库名 用途 我常用的版本
pandas 数据加载、清洗、切片 ≥1.4.0
numpy 数值计算、数组操作 ≥1.21.0
scipy 信号滤波、插值 ≥1.7.0
matplotlib 可视化对比 ≥3.5.0

安装很简单,一行命令搞定:

pip install pandas numpy scipy matplotlib

接下来,加载数据。假设我们有一个CSV文件,里面存了轨迹点的经纬度和时间戳。我在项目中遇到过很多次,数据文件第一行往往是列名,但偶尔也有不带表头的,所以加载时最好看一眼。

import pandas as pd

# 加载轨迹数据
df = pd.read_csv('trajectory_raw.csv', header=0)

# 看一眼前5行,确认数据格式
print(df.head())

# 检查有没有空值
print(df.isnull().sum())

嗯,这里要注意。空值处理不能马虎。我见过有人直接dropna()把整行删了,结果轨迹断成了好几截。我的做法是:如果缺失值少于总数据量的5%,用前后均值插补;如果超过5%,就得考虑是不是传感器本身出了问题。

小技巧: 加载数据后,先打印 df.info() 和 df.describe(),能快速发现异常值。比如最大经纬度突然跳到几百,那肯定是脏数据。

4.2 数据预处理:时间对齐与重采样

轨迹数据最头疼的问题是什么?时间戳不均匀。GPS信号偶尔丢包,或者采样频率不稳定,导致相邻点的时间间隔忽大忽小。直接滤波的话,低频噪声和高频抖动会混在一起,根本分不清。

我的做法是:先做时间对齐,把数据重采样到固定频率。比如统一到10Hz,也就是每100毫秒一个点。

# 确保时间列是datetime类型
df['timestamp'] = pd.to_datetime(df['timestamp'])

# 按时间排序
df = df.sort_values('timestamp')

# 设置时间为索引,重采样到100ms
df_resampled = df.set_index('timestamp').resample('100ms').mean()

# 线性插值填补重采样后的空值
df_resampled = df_resampled.interpolate(method='linear')

# 重置索引
df_resampled = df_resampled.reset_index()

print(f"原始点数: {len(df)}, 重采样后点数: {len(df_resampled)}")

为什么会用线性插值?说白了,轨迹在短时间内近似匀速运动,线性插值已经够用了。如果你做的是高动态运动(比如无人机急转弯),可以考虑样条插值,但计算量会大不少。

避坑指南: 我曾经在重采样时忘了处理重复时间戳,结果插值出来的轨迹出现了"回退"现象——时间往前走,位置却往回跳。后来我加了一步 df = df.drop_duplicates(subset=['timestamp']),问题就解决了。

4.3 使用Scipy进行滤波

数据洗干净了,终于轮到滤波上场。我一般用两种滤波器:低通滤波 和 中值滤波。前者适合平滑整体轨迹,后者对去除离群点特别有效。

先看低通滤波。核心参数是截止频率,这个值怎么定?我有个经验法则:先看轨迹的频谱,找到抖动的主频,然后取它的1/3作为截止频率。

from scipy.signal import butter, filtfilt
import numpy as np

# 设计低通滤波器
def lowpass_filter(data, cutoff, fs, order=4):
    nyquist = 0.5 * fs
    normal_cutoff = cutoff / nyquist
    b, a = butter(order, normal_cutoff, btype='low', analog=False)
    y = filtfilt(b, a, data)
    return y

# 假设采样频率是10Hz,截止频率设为1Hz
fs = 10.0
cutoff = 1.0

# 对经度、纬度分别滤波
df_resampled['lon_filtered'] = lowpass_filter(
    df_resampled['longitude'].values, cutoff, fs
)
df_resampled['lat_filtered'] = lowpass_filter(
    df_resampled['latitude'].values, cutoff, fs
)

这里我用了filtfilt而不是lfilter,为什么?filtfilt是零相位滤波,不会引入时间延迟。做轨迹处理,相位延迟意味着位置偏移,这在后续的路径匹配中会带来大麻烦。

再看中值滤波。它对付那种突然跳变的点特别管用——比如GPS在隧道里短暂失锁,位置突然漂出去几十米。

from scipy.signal import medfilt

# 中值滤波,窗口大小设为5
window_size = 5
df_resampled['lon_medfiltered'] = medfilt(
    df_resampled['longitude'].values, kernel_size=window_size
)
df_resampled['lat_medfiltered'] = medfilt(
    df_resampled['latitude'].values, kernel_size=window_size
)
核心要点: 低通滤波平滑整体,中值滤波去除毛刺。我通常先做中值滤波,再做低通滤波。顺序反了的话,中值滤波会把低通滤波已经平滑过的数据再次"切"出新的毛刺。

4.4 可视化滤波前后对比

光看数字不够直观。我习惯把原始轨迹、低通滤波结果、中值滤波结果画在同一张图上,一眼就能看出效果。

import matplotlib.pyplot as plt

plt.figure(figsize=(12, 6))

# 原始轨迹
plt.plot(df_resampled['longitude'], df_resampled['latitude'], 
         'b-', alpha=0.5, label='原始轨迹', linewidth=1)

# 低通滤波结果
plt.plot(df_resampled['lon_filtered'], df_resampled['lat_filtered'], 
         'r-', label='低通滤波 (截止1Hz)', linewidth=2)

# 中值滤波结果
plt.plot(df_resampled['lon_medfiltered'], df_resampled['lat_medfiltered'], 
         'g--', label='中值滤波 (窗口5)', linewidth=2)

plt.xlabel('经度')
plt.ylabel('纬度')
plt.title('轨迹滤波前后对比')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

画出来之后,你会看到:原始轨迹像喝醉了酒,到处乱晃;低通滤波后的轨迹平滑了很多,但拐弯处有点"钝";中值滤波保留了更多细节,但偶尔还有小毛刺。

我个人习惯,先看中值滤波的效果,如果毛刺还多,就加大窗口;如果轨迹变形了,就减小窗口。窗口大小一般取奇数,3、5、7比较常用。

4.5 本章知识体系

下面这张图,是我做预处理时的完整流程。每次拿到新数据,我都会按这个步骤走一遍,基本不会漏掉关键环节。

轨迹数据预处理流程 原始轨迹数据 加载 + 空值检查 + 去重 时间对齐 + 重采样 中值滤波(去毛刺) 低通滤波(平滑) 可视化对比 + 效果评估 建议:先中值滤波去毛刺,再低通滤波平滑整体轨迹

这张图就是我每次做预处理的"检查清单"。从原始数据到最终的可视化对比,每一步都有明确的输入和输出。你跟着走一遍,基本不会出大问题。

4.6 常见问题与调试建议

最后,分享几个我在项目中踩过的坑:

  • 滤波后轨迹变短了? 检查一下是不是滤波时用了边界填充方式。filtfilt默认会处理边界,但如果数据太短(少于50个点),边界效应会很明显。我一般会保证至少有100个连续点再滤波。
  • 低通滤波后轨迹拐弯处变形? 截止频率设得太低了。试着提高截止频率,或者改用Savitzky-Golay滤波器,它对拐弯处的保留更好。
  • 中值滤波后轨迹出现"平台"? 窗口太大了。中值滤波的窗口大小不要超过轨迹中最小转弯半径对应的点数。比如一个急转弯用了10个点完成,窗口就别超过5。
  • 可视化时两条线完全重合? 大概率是滤波没生效。检查一下滤波函数是否真的被调用了,或者滤波后的数据有没有被正确赋值到DataFrame里。
我的习惯: 每次改完参数,我都会把滤波前后的轨迹画出来,再算一下抖动幅度(比如标准差)的下降比例。如果下降超过80%,说明参数可能过狠了,轨迹的真实特征被抹掉了。一般下降50%-70%是比较合理的范围。

好了,第一步实战就到这里。数据预处理是后面所有优化的基础,这一步做扎实了,后面的抖动分析才能靠谱。你先把代码跑一遍,看看自己的轨迹数据滤波前后有什么变化。有问题随时回来翻翻这一章,我踩过的坑,你大概率也会遇到。