第 2 章:轨迹数据基础——坐标系统、时间戳、轨迹点、轨迹段、轨迹ID
做轨迹分析,首先得把数据的基本构成搞明白。说白了,一条轨迹就是一大堆点按照时间顺序串起来的集合。但这里面每个点的“身份”是什么?点跟点之间怎么组织成轨迹?这里面的门道,咱们今天捋一遍。
我个人习惯把轨迹数据拆成五个最基础的概念:坐标系统、时间戳、轨迹点、轨迹段,还有轨迹ID。这五样东西,你理解透了,后面那些花哨的指标算法才接得住。
坐标系统——轨迹的骨架
你想想看,一个轨迹点落在哪儿,总得有个参照吧?坐标系统就是这个参照标准。不同的系统,坐标值的含义完全不同。搞混了,算出来的距离能偏出几公里。
实际项目中,我最常见到的是下面这几种:
| 坐标系统 | 简称 | 常见用途 | 技术特点 |
|---|---|---|---|
| WGS 84 | 经纬度 | GPS 原始输出 | 全球统一,桌面应用直接使用 |
| GCJ-02 | 火星坐标 | 国内地图服务 | WGS 84 偏移加扰,约 100~700m |
| BD-09 | 百度坐标 | 百度地图产品 | GCJ-02 二次加密 |
| Cartesian / UTM | 投影平面 | 局部高精度计算 | 米单位,适合速度 / 距离计算 |
| CGCS2000 | 国家 2000 | 测绘 / 自动驾驶 | 国内水准原点,精度高于 WGS 84 |
我个人最常打交道的是 WGS-84 和 GCJ-02。曾经有个项目,我们直接拿 GPS 经纬度往高德地图上打点,结果发现所有轨迹都偏了 500 多米。后来才意识到——GPS 吐的是 WGS-84,高德用的是 GCJ-02,必须转一次再渲染。
代码层面上,常见的坐标转换就是在一个工具类里内置偏移算法。比如 GCJ-02 转 WGS-84:
def gcj02_to_wgs84(lng, lat):
# 使用迭代法逼近真实坐标
# 偏移量取决于经纬度所在位置
dx = lng - 105.0
dy = lat - 35.0
radlat = lat / 180.0 * pi
magic = sin(radlat)
magic = 1 - ee * magic * magic
sqrtmagic = sqrt(magic)
dlng = (dx * 100.0 + 0.0) / (sqrtmagic * cos(radlat) * pi / 180.0)
dlat = (dy * 100.0 + 0.0) / (sqrtmagic * pi / 180.0)
return lng - dlng, lat - dlat
千万别在短距离里混用不同坐标系统的数据做速度计算。平面距离公式(半正矢公式)只适用于经纬度,而平面速度计算必须用 UTM/Cartesian 坐标。我见过有人用经纬度差直接除时间算速度,结果可想而知……
时间戳——轨迹的时间轴
坐标决定了“点在哪”,时间戳决定了“点什么时候在”。没有时间戳的轨迹点,就像没有时针的钟——根本没法用。
时间戳一般有两种格式:
- Unix 时间戳:从 1970-01-01 00:00:00 UTC 到现在的秒(或毫秒)数。整数,好传输好比较。
- ISO 8601 字符串:比如 "2024-03-15T08:30:00Z"。可读性强,但解析成本高一点。
我记得有一次排查一个车辆漂移问题。日志里看到两个轨迹点,坐标完全没变,但时间戳差了 40 分钟。解析发现是设备的时间戳格式用错了——Unix 毫秒级当成了秒级传到了服务器。你看,一个格式错误就够你头疼半天。
我的建议是:数据链路内部统一用 Unix 毫秒时间戳(int64),只在展示层转成用户可读的时间字符串。一来避免解析歧义,二来排序、求差值都极其方便。
轨迹点——最小的数据单元
一个轨迹点就是一条带有位置和时间的记录。它在代码里通常长这样:
{
"lat": 39.9042,
"lng": 116.4074,
"timestamp": 1710484200000,
"speed": 12.5,
"bearing": 45
}
当然,实际生产环境里点里还可能带 altitude(海拔)、accuracy(定位精度)、extras(自定义属性)等字段。但无论如何,坐标 + 时间戳是一组轨迹点的最基本门槛,缺一不可。
我刚入行那会儿,总觉得“点”嘛,差不多就行了。后来被数据清洗折磨过才明白——轨迹点的质量直接影响后续所有指标的准确性。比如速度异常大,往往是因为“点”的坐标跳到了几百米外,这种野点必须先过滤掉。
轨迹段——把点串成线
单个轨迹点能传递的信息有限。把同一设备、连续时间内的点按时间排序连起来,就构成了轨迹段。
轨迹段也有自己的属性:
- 起止时间:第一个点和最后一个点的时间戳
- 长度:所有相邻点之间的距离累加
- 持续时间:最后一个点时间戳 - 第一个点时间戳
- 采样率:例如 1 秒一个点,2 秒一个点
现实中,我们经常把一条完整行程分成多个段来处理。比如车辆熄火后重新启动,时间间隔超过几分钟,通常就认为是一个新段的开始。为什么?因为熄火期间的轨迹是断的,强行连起来会算出不合理的静止或跳跃。
# 按时间间隔切割轨迹段
def split_track_by_gap(points, max_gap_ms=300000):
segments = []
current_segment = [points[0]]
for p in points[1:]:
gap = p['timestamp'] - current_segment[-1]['timestamp']
if gap > max_gap_ms:
segments.append(current_segment)
current_segment = [p]
else:
current_segment.append(p)
segments.append(current_segment)
return segments
我个人经验:合理的切割阈值(max_gap)取决于业务场景。物流配送一般是 5~10 分钟;人步行轨迹可能用 30 秒;自动驾驶数据则通常用 2~3 秒。别偷懒,要根据业务数据分布去定这个值。
轨迹ID——给每段轨迹一个身份
最后,咱们得给每段轨迹一个唯一标识。否则,多个设备的轨迹在数据表里混在一起,根本分不清谁是谁。
轨迹ID 通常由两部分组合而成:
- 设备标识:IMEI、设备 UUID、车辆车牌号等
- 段序号或起始时间:保证同一设备的不同段不会重复
例如:truck_A_20240315_001,一看就知道是 A 车在 2024 年 3 月 15 日的第 1 段轨迹。
我曾经踩过一个坑:用设备 ID + 当天日期拼成 ID,但同一个设备在同一天跑了多趟车(比如出租车),结果后面几趟的数据把前面的覆盖了。后来改成 "设备 ID + 起始 Unix 时间戳",才彻底解决唯一性问题。轨迹ID 必须是全局唯一的,不能有模糊空间。
这五个概念之间的关系,我用一张图来总结:
你看,从坐标系统确定位置参照,到时间戳赋予每一点时间属性,再到轨迹点、轨迹段、轨迹ID依次串起来,一条可计算、可分析的轨迹就成形了。后面章节里所有的历史轨迹、实时轨迹、高级指标——都是在这些基础元件上搭起来的。