3. 时间序列处理

大家好,我是老张。今天咱们聊聊时间序列处理。说实话,做轨迹回放这么多年,我踩过最大的坑就是时间数据不干净。你拿到的GPS数据、传感器数据,时间戳格式五花八门,有的用毫秒,有的用秒,有的干脆是字符串。不处理?那后面的分析全是错的。

3.1 时间戳标准化——把数据对齐到同一把尺子上

我刚开始做项目时,接过一批车载GPS数据。打开一看,好家伙,时间戳有Unix时间戳、有ISO 8601字符串、还有Excel序列号。我当时就懵了。后来我养成了一个习惯:所有时间戳,统一转成Unix毫秒时间戳。为什么?因为计算方便,排序、差值、切片都省心。

核心原则:时间戳标准化就是给所有数据配一把统一的尺子。我建议用毫秒级Unix时间戳,精度够用,兼容性好。

具体怎么做?我一般分三步:

  1. 识别原始格式——是整数、浮点数还是字符串?
  2. 统一时区——所有时间转成UTC,避免夏令时、时区偏移的坑
  3. 转成数值——最终得到毫秒级整数,方便后续计算

给你看个代码示例,这是我常用的标准化函数:

def standardize_timestamp(ts, fmt='unix_ms'):
    """
    将各种时间戳统一为毫秒级Unix时间戳
    """
    import pandas as pd
    from datetime import datetime
    
    if fmt == 'iso_string':
        # 处理 '2024-01-15T10:30:00Z' 这种格式
        dt = pd.to_datetime(ts)
        return int(dt.timestamp() * 1000)
    elif fmt == 'unix_sec':
        # 秒级时间戳转毫秒
        return int(ts * 1000)
    elif fmt == 'unix_ms':
        # 已经是毫秒,直接返回
        return int(ts)
    else:
        raise ValueError(f'不支持的格式: {fmt}')

小技巧:我习惯用pandas的to_datetime()做解析,它能自动识别大部分常见格式。但注意,它处理不了Excel序列号,那个得单独写逻辑。

3.2 插值算法——填补数据空洞的艺术

数据采集过程中,丢包、信号遮挡、设备休眠,都会导致时间序列出现空洞。你想想看,轨迹回放时突然跳过一个点,画面会卡顿甚至错位。这时候就需要插值。

我常用的插值算法就两种:线性插值和样条插值。别被名字吓到,说白了就是补点。

3.2.1 线性插值——简单粗暴,但够用

线性插值假设两个已知点之间是直线运动。对于匀速运动的轨迹,这个假设基本成立。我做过一个物流车辆监控项目,GPS每10秒上报一次,但客户要求每秒显示位置。线性插值一上,效果很好。

def linear_interpolate(t, t0, t1, v0, v1):
    """
    线性插值:已知(t0, v0)和(t1, v1),求t时刻的值
    """
    ratio = (t - t0) / (t1 - t0)
    return v0 + ratio * (v1 - v0)

嗯,这里要注意:线性插值对加速度变化大的场景不友好。比如急刹车、急转弯,插出来的点会偏离实际路径。我曾经在一个赛车轨迹回放项目里用过,结果插出来的轨迹把弯道拉直了,客户直接投诉。

3.2.2 样条插值——平滑,但别滥用

样条插值用分段多项式拟合数据点,生成一条平滑曲线。说白了,它比线性插值更「聪明」,能模拟出加速度变化。

我推荐用scipy的CubicSpline,三次样条插值效果最好:

from scipy.interpolate import CubicSpline
import numpy as np

# 已知数据点
t_known = np.array([0, 1, 2, 3, 4])  # 时间
x_known = np.array([0, 2, 1, 3, 5])  # 位置

# 创建样条插值函数
cs = CubicSpline(t_known, x_known)

# 在任意时间点插值
t_new = np.array([0.5, 1.5, 2.5, 3.5])
x_new = cs(t_new)

避坑指南:我曾经在一个无人机轨迹项目里滥用样条插值,结果插出了「超物理」的轨迹——无人机在两点之间画了个大弧线,实际根本飞不出来。记住:样条插值只适用于平滑运动,对剧烈变化的数据要谨慎。

3.3 时间窗口切片技术——把长序列切成可管理的片段

拿到标准化的时间序列后,下一步就是切片。为什么?因为一整天的轨迹数据可能有几十万个点,你不可能一次性处理。切片之后,可以逐段分析、逐段回放。

我常用的切片方式有三种:

切片方式 说明 适用场景
固定时间窗口 按固定时长切,比如每5分钟一段 常规轨迹回放、监控
滑动时间窗口 窗口滑动,有重叠 实时分析、异常检测
事件驱动窗口 按事件触发切分,比如停车、转弯 行为分析、场景复现

给你看个固定时间窗口的代码:

def slice_by_time_window(df, window_size_ms=300000):
    """
    按固定时间窗口切片,默认5分钟
    df: 包含 'timestamp_ms' 列的DataFrame
    """
    df = df.sort_values('timestamp_ms')
    start_time = df['timestamp_ms'].min()
    end_time = df['timestamp_ms'].max()
    
    windows = []
    current_start = start_time
    while current_start < end_time:
        current_end = current_start + window_size_ms
        window_data = df[(df['timestamp_ms'] >= current_start) & 
                         (df['timestamp_ms'] < current_end)]
        if not window_data.empty:
            windows.append(window_data)
        current_start = current_end
    
    return windows

我的经验:滑动窗口适合做实时回放。比如每100毫秒滑动一次,每次取前5秒的数据,这样回放画面既流畅又不会漏掉细节。但注意,重叠率太高会浪费计算资源,我一般设50%重叠。

3.4 知识体系总览

说了这么多,我画了张图帮你理清思路。时间序列处理的核心就这三块:标准化是基础,插值是补洞,切片是管理。三者缺一不可。

时间序列处理知识体系 时间戳标准化 • 统一格式 • 时区转换 • 精度对齐 • 异常检测 • 缺失处理 插值算法 • 线性插值 • 样条插值 • 多项式插值 • 最近邻插值 • 插值误差评估 时间窗口切片 • 固定窗口 • 滑动窗口 • 事件驱动窗口 • 窗口重叠策略 • 性能优化 标准化 → 插值 → 切片:时间序列处理的完整流水线

最后说一句,时间序列处理没有银弹。你拿到的数据质量不同,处理策略就得调整。我个人的建议是:先标准化,再插值,最后切片。顺序别搞反,否则你会后悔的。

可可精品资料库