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天线被车厢遮挡。嗯,所以清洗不只是技术问题,有时还得结合业务场景来判断。

一个实用的过滤规则:对于高频轨迹(1秒/点),用速度阈值做初步过滤。比如,如果两个相邻点之间推算出的速度超过 100 km/h(步行是不可能达到的),基本可以判定为离群点。

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'})
注意:如果缺的缺口太大(比如超过60秒没数据),线性插值反而会引入误差。我建议在插值前先检查最大间隔,超过阈值就直接把这段切分成两段轨迹。

3.2.3 坐标对齐:统一坐标系

这一步容易被忽略,但我做过一个多源数据融合的项目,翻车就翻在这里。

不同设备用的坐标系可能不一样。例如:

坐标系常见来源备注
WGS-84 (GPS)手机、车载GPS全球通用,经纬度
GCJ-02 (火星坐标系)国内设备WGS-84加密偏移
UTM专业测绘设备平面直角坐标,单位米
自定义网格坐标室内定位系统需额外校准

如果你在做PCA之前没统一坐标,最终提取的特征可比性会很差。我一般会在预处理阶段就把所有数据转到UTM坐标——这样距离、速度的计算更方便,且PCA在直角坐标系下才有几何意义。

3.3 从原始坐标到结构特征:一个完整的预处理流程

说了这么多,直接上图。这是我处理轨迹数据时最常用的流程。

轨迹数据预处理标准流程 原始轨迹数据 清洗(离群点/精度过滤) 时间戳插值(1S 均匀采样) 坐标对齐(wgs84→UTM) 结构化轨迹(可算速度/方向) 注意点 • 清洗时别一刀切; 有些区域的高精度值 可能是信号导致的。 • 插值前检查最大间隔。 • 坐标系务必统一。

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:区分不同轨迹段
小技巧:如果你后面要用滑动窗口提取特征,建议在预处理阶段就把数据按轨迹段分组存储好。这样后续计算窗口特征时,不需要跨段滑动,避免混入错误上下文。