3. 轨迹数据预处理基础
做轨迹分析这些年,我最大的体会就是:数据预处理花的时间,往往比建模本身还多。
为什么呢?因为原始轨迹数据很少是干净的。GPS丢星、Wi-Fi信号漂移、手机断连……随便哪个问题,都能让后面的PCA算法跑出离谱的结果。我刚开始做这个方向时,就吃过这个亏——拿了一堆原始数据直接算特征,结果PCA第一主成分居然被噪声主导,折腾两天才发现是预处理没做干净。
说白了,预处理是地基。地基歪了,上面盖的楼再漂亮也没用。这一章,咱们就踏实把地基打好。
3.1 原始轨迹数据长什么样?
在动手之前,先搞清楚我们要处理的东西。轨迹数据通常是时间序列,包含时间戳、经纬度(或x,y坐标)以及可能的附加信息(速度、方向、高度等)。
我见过最常见的原始格式是这样的(来自真实项目中的数据):
timestamp,latitude,longitude,altitude,speed,accuracy
2024-01-15 08:00:00,39.9042,116.4074,52.3,0.8,5.2
2024-01-15 08:00:01,39.9043,116.4075,52.5,1.2,4.8
2024-01-15 08:00:05,39.9048,116.4080,53.1,1.5,6.0
2024-01-15 08:00:12,39.9055,116.4088,53.0,0.9,20.1
2024-01-15 08:00:30,39.9060,116.4095,52.8,0.3,12.5
注意最后两行——时间间隔从1秒跳到了7秒和18秒,精度值(accuracy)也从5左右飙到了20。嗯,这里要特别留意。这种不规则采样和精度波动,就是预处理的切入点。
3.2 预处理三板斧:清洗、插值、对齐
我习惯把预处理拆成三个核心步骤,每个步骤解决一个具体问题。
3.2.1 数据清洗:干掉脏数据
轨迹数据里的脏数据,常见的有这几类:
- 离群点:一个点突然跳出去几百米,然后又跳回来。通常是GPS信号反射导致的。
- 静止重复点:设备没移动,但一直上报相同的位置,造成大量冗余数据。
- 精度超标点:accuracy > 50米的点,基本不可信。
- 时间戳异常:比如未来时间戳,或数据顺序乱序。
我曾经在一次项目中,发现某个轨迹片段里有一个点跑到隔壁城市去了。表面看是设备故障,查了日志才发现是车载GPS天线被车厢遮挡。嗯,所以清洗不只是技术问题,有时还得结合业务场景来判断。
3.2.2 时间戳插值:让数据变均匀
清洗完了,你会发现时间戳变得七零八落。原来的1秒间隔,现在有的位置跳了5秒,有的地方甚至缺了30秒。
怎么处理?我个人习惯用线性插值把数据补成固定的时间间隔。比如统一成1秒一个点。
代码实现其实不复杂:
import pandas as pd
import numpy as np
def interpolate_trajectory(df, target_freq='1S'):
# 先把时间戳设成索引
df['timestamp'] = pd.to_datetime(df['timestamp'])
df = df.set_index('timestamp')
# 生成目标时间序列
new_index = pd.date_range(
start=df.index.min(),
end=df.index.max(),
freq=target_freq
)
# 重采样 + 线性插值
df_interp = df.reindex(new_index).interpolate(method='linear')
return df_interp.reset_index().rename(columns={'index': 'timestamp'})
3.2.3 坐标对齐:统一坐标系
这一步容易被忽略,但我做过一个多源数据融合的项目,翻车就翻在这里。
不同设备用的坐标系可能不一样。例如:
| 坐标系 | 常见来源 | 备注 |
|---|---|---|
| WGS-84 (GPS) | 手机、车载GPS | 全球通用,经纬度 |
| GCJ-02 (火星坐标系) | 国内设备 | WGS-84加密偏移 |
| UTM | 专业测绘设备 | 平面直角坐标,单位米 |
| 自定义网格坐标 | 室内定位系统 | 需额外校准 |
如果你在做PCA之前没统一坐标,最终提取的特征可比性会很差。我一般会在预处理阶段就把所有数据转到UTM坐标——这样距离、速度的计算更方便,且PCA在直角坐标系下才有几何意义。
3.3 从原始坐标到结构特征:一个完整的预处理流程
说了这么多,直接上图。这是我处理轨迹数据时最常用的流程。
3.4 实战:代码与踩坑点
下面是一段比较完整的预处理函数,我在多个项目里复用过的。
def preprocess_trajectory(file_path, target_freq='1S', max_gap_sec=60):
# 加载原始数据
df = pd.read_csv(file_path)
df['timestamp'] = pd.to_datetime(df['timestamp'])
df = df.sort_values('timestamp')
# 第一步:精度过滤
df = df[df['accuracy'] < 50].copy()
# 第二步:速度阈值去离群点(按3点滑动窗口)
df['lat_diff'] = df['latitude'].diff().abs()
df['lon_diff'] = df['longitude'].diff().abs()
# 超过0.01度(约1km)的跳变视为离群
df = df[(df['lat_diff'] < 0.01) & (df['lon_diff'] < 0.01)].copy()
# 第三步:检查最大间隔,切分轨迹
time_diff = df['timestamp'].diff().dt.total_seconds()
split_indices = df.index[time_diff > max_gap_sec].tolist()
# 第四步:对每段做插值
segments = []
start = 0
for idx in split_indices:
segment = df.iloc[start:idx].copy()
if len(segment) > 2:
segment = interpolate_trajectory(segment, target_freq)
segments.append(segment)
start = idx
# 别忘了最后一段
segment = df.iloc[start:].copy()
if len(segment) > 2:
segment = interpolate_trajectory(segment, target_freq)
segments.append(segment)
return segments
你可以这样调用:
segments = preprocess_trajectory('gps_data.csv', target_freq='1S')
print(f"共切分出 {len(segments)} 段有效轨迹")
3.5 预处理完成后,我们要准备什么?
当数据变成干净、均匀的时间序列之后,就可以计算特征了。但注意——PCA处理的是特征矩阵,不是原始坐标。所以下一步我们要从轨迹点里提取出能描述运动模式的统计量。
我建议目前在预处理阶段,先把下面几个字段准备好:
- timestamp:统一后的时间戳
- x, y:UTM坐标系下的平面坐标
- is_valid:标记该点是否是插值得来的(可选)
- segment_id:区分不同轨迹段