简介:TrajectoryClustering-master 是一份面向数据挖掘与地理空间分析学习者的 Python 轨迹聚类实战源码,适合希望理解 GPS 轨迹、移动设备位置记录等时空数据聚类流程的初学者与数据科学从业者。项目围绕轨迹数据预处理、距离度量、聚类算法实现与结果可视化展开,可用于交通流量分析、用户行为研究、动物迁徙模式识别等场景。压缩包共 7 个文件,约 998KB,包含 4 个 py 源码文件、1 个 md 说明文档、1 个 png 效果图与 1 个 gif 动态演示,源码分别承担轨迹读取、聚类计算与公共工具函数等职责,图片则直观呈现聚类中心与轨迹分布。目前已有 1216 人学习下载。通过研读源码,读者可掌握 NumPy、Pandas 数据清洗与 Matplotlib 可视化方法,理解 K-means、DBSCAN 等算法在轨迹场景中的参数调整与评估思路,并借助动态图快速验证聚类效果,为后续处理真实时空数据积累可复用的工程经验。
1. 拿到 TrajectoryClustering-master 之后,先搞清楚它能替你省掉哪段活
如果你手头正压着一批 GPS 点位、车辆行驶记录或者移动设备的定位日志,想从里面看出「哪些轨迹其实走的是同一条路」「哪个路口是主要通行方向」,那 TrajectoryClustering-master 这个包值得先跑一遍再决定要不要自己重写。它是一份 Python 实现的轨迹聚类源码,目录里就 trajectory_clustering.py、trajectory.py、clustering.py、common.py 几个核心文件,外加 img 目录下的 trajclus.png 和 roundabout.gif 两张结果图。结构不复杂,但把「读轨迹 → 算相似度 → 聚类 → 画图」这条链路完整串了起来,适合做交通流量分析、用户行为分组、动物迁徙模式归纳这类活。新手能照着跑通看效果,熟手能直接拆开换距离度量和聚类算法。下面按「它怎么算 → 怎么跑 → 坑在哪 → 怎么改」的顺序拆。
2. 轨迹聚类的核心:距离度量与聚类算法怎么选
2.1 为什么轨迹聚类不能直接套普通 K-means
普通 K-means 处理的是无序的点集,两个样本换个顺序还是同一个样本。轨迹不一样,它是有时序的点序列,点与点之间的先后关系本身就是信息。你把一条轨迹的点打乱,它代表的运动模式就变了。所以轨迹聚类的第一步不是选聚类算法,而是先定义「两条轨迹有多像」。
常见做法有三类。第一类是逐点欧氏距离,要求两条轨迹采样点数量一致、时间对齐,实现最简单,但对采样频率和起点敏感,稍微错位距离就飙上去。第二类是对轨迹重采样后算平均距离,缓解点数不一致的问题,代价是丢失部分细节。第三类是考虑形状和方向的度量,比如动态时间规整(DTW)或者弗雷歇距离,能容忍速度差异和局部错位,但计算量明显上升。
TrajectoryClustering-master 走的是「先把轨迹整理成统一表示,再算距离矩阵,最后丢给聚类算法」的路子。trajectory.py 负责轨迹对象的读取和预处理,clustering.py 负责聚类逻辑,common.py 放公共函数,trajectory_clustering.py 是入口。这个分层的好处是你想换距离度量,只动 common.py 或 trajectory.py 里的距离函数就行,不用碰聚类主流程。
提示:如果你的轨迹采样频率差异很大,先做重采样再算距离,否则聚类结果基本是采样频率的聚类,不是运动模式的聚类。
2.2 聚类算法选型:K-means、DBSCAN 还是层次聚类
选哪个算法,取决于你对「簇」的预期。K-means 要求你事先指定簇数量 K,适合你已经知道大概有几类运动模式的场景,比如一个路口左转、右转、直行三种流向,K 设 3 就够。它的缺点是假设簇是球形且大小相近,轨迹形状复杂时容易切错。
DBSCAN 不需要指定簇数量,靠邻域半径 eps 和最小样本数 min_samples 控制,能识别噪声点,适合轨迹分布不均匀、有大量离群轨迹的情况。缺点是参数敏感,eps 差一点结果差很多,而且高维距离下邻域判断会退化。
层次聚类输出树状结构,不用一开始定 K,可以看树状图再决定切几刀,适合探索性分析。代价是计算复杂度高,轨迹数量上千后内存和时间都吃紧。
TrajectoryClustering-master 的 clustering.py 里封装了聚类调用,具体用哪个算法要看源码里的实现。我一般会先用层次聚类跑一遍小样本看分布,确定大致簇数后再用 K-means 做正式聚类,DBSCAN 留给噪声多的数据集。这个组合不是唯一解,但能让你在参数没底的时候先有个参照。
2.3 从原始轨迹到距离矩阵的完整流程
不管最后用哪个算法,前面几步是共通的。下面这段代码演示了从轨迹列表构造距离矩阵的标准做法,你可以对照项目里的 common.py 看它实际怎么实现的。
import numpy as np from scipy.spatial.distance import cdist def resample_trajectory(traj, n_points=50): """把一条轨迹重采样成固定点数,消除采样频率差异""" traj = np.array(traj) # 按累计弧长做线性插值,保证重采样后点的间距均匀 dists = np.sqrt(((np.diff(traj, axis=0)) ** 2).sum(axis=1)) cumdist = np.concatenate([[0], np.cumsum(dists)]) if cumdist[-1] == 0: return np.tile(traj[0], (n_points, 1)) sample_points = np.linspace(0, cumdist[-1], n_points) x = np.interp(sample_points, cumdist, traj[:, 0]) y = np.interp(sample_points, cumdist, traj[:, 1]) return np.column_stack([x, y]) def build_distance_matrix(trajectories, n_points=50): """构造轨迹两两之间的距离矩阵""" resampled = [resample_trajectory(t, n_points) for t in trajectories] n = len(resampled) dist_mat = np.zeros((n, n)) for i in range(n): for j in range(i + 1, n): # 逐点欧氏距离取均值,作为两条轨迹的距离 d = cdist(resampled[i], resampled[j]) dist_mat[i, j] = dist_mat[j, i] = d.min(axis=1).mean() return dist_mat这段代码有两个关键参数。n_points 控制重采样后的点数,设太小会抹平轨迹形状,设太大对距离计算没帮助还拖慢速度,一般 30 到 100 之间试。距离计算里用了d.min(axis=1).mean(),意思是每个点找另一条轨迹上最近的点,取这些最近距离的均值,这样对局部错位有一定容忍度。如果你要严格逐点对齐,直接换成np.linalg.norm(resampled[i] - resampled[j], axis=1).mean()。
拿到距离矩阵后,聚类就是标准操作了。K-means 不能直接吃距离矩阵,需要先做多维缩放或者用 KMedoids;DBSCAN 和层次聚类可以直接用预计算的距离矩阵。项目里如果用的是 sklearn,注意metric='precomputed'这个参数,传错会报错或者算出莫名其妙的结果。
3. 把项目跑起来:环境、入口与可视化输出
3.1 环境准备与依赖安装
这个项目是纯 Python 实现,依赖不会太重。常见做法是建一个虚拟环境再装包,避免和系统里的其他版本打架。
python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install numpy scipy matplotlib scikit-learnnumpy 和 scipy 负责数值计算和距离矩阵,matplotlib 负责画轨迹图和聚类中心,scikit-learn 提供聚类算法和评估指标。如果你用的是较新的 Python 版本,注意 scipy 和 sklearn 的版本匹配,装完先python -c "import sklearn; print(sklearn.__version__)"确认能导入。项目里如果用了 pandas 读数据,再补一个pip install pandas。
注意:不要用系统自带的 Python 直接装包,权限问题和版本冲突会让你在排错上花掉比写代码还多的时间。
3.2 入口脚本与数据格式
trajectory_clustering.py 是入口,跑之前先确认它读的数据长什么样。轨迹数据常见的存储格式有两种:一种是每行一个点,字段是traj_id, x, y(或者traj_id, lat, lon);另一种是每条轨迹一行,坐标序列用分号或 JSON 串起来。项目里的 trajectory.py 应该有对应的读取函数,你打开看一眼它期望的列名和分隔符。
如果数据格式对不上,不用改源码,在入口脚本前面加一段适配就行:
import pandas as pd def load_trajectories(csv_path): """把长表格式的轨迹数据转成 {traj_id: [(x,y), ...]} 字典""" df = pd.read_csv(csv_path) # 按轨迹 ID 分组,按时间或序号排序后取出坐标序列 df = df.sort_values(['traj_id', 'timestamp']) trajs = {} for tid, group in df.groupby('traj_id'): trajs[tid] = group[['x', 'y']].values.tolist() return trajs这里 sort_values 那一步不能省。很多 GPS 日志是按写入顺序存的,不是按时间顺序,不排序直接聚类,轨迹形状全是乱的。字段名按你实际数据的列名改,x/y 换成 lat/lon 也一样,只要保证是数值列。
3.3 可视化:看懂 trajclus.png 和 roundabout.gif
img 目录下的 trajclus.png 是静态聚类结果图,通常画的是所有轨迹叠在一起,不同簇用不同颜色,聚类中心用粗线或标记点标出。roundabout.gif 是环岛场景的动态展示,能看出轨迹随时间的走向和聚类归属变化。这两个图不是装饰,是你验证聚类有没有跑对的参照。
自己画图时,重点看三件事:同一簇的轨迹是不是走向一致,聚类中心是不是落在轨迹密集的通道上,有没有明显该分到一类却被拆开的轨迹。如果图上看不出结构,先别调聚类参数,回头检查距离矩阵是不是算错了。常见错误是坐标没做投影直接用经纬度算欧氏距离,纬度方向一度和一经度一度的实际距离不一样,结果会被纬度差异主导。数据量小的时候用 UTM 投影,数据量大或者跨区域就用 Haversine 距离。
import matplotlib.pyplot as plt def plot_clusters(trajectories, labels, centers=None): """按簇标签画轨迹,中心用加粗线标出""" plt.figure(figsize=(10, 8)) for traj, lab in zip(trajectories, labels): traj = np.array(traj) plt.plot(traj[:, 0], traj[:, 1], color=plt.cm.tab10(lab), alpha=0.5, linewidth=1) if centers is not None: for c in centers: c = np.array(c) plt.plot(c[:, 0], c[:, 1], color='black', linewidth=3) plt.axis('equal') plt.savefig('trajclus_result.png', dpi=150) plt.show()axis('equal')要加上,不然 x 和 y 轴比例不一致,轨迹形状会被拉伸,看起来像另一条路。alpha 设 0.5 是为了轨迹重叠时还能看出密度。centers 传聚类中心轨迹,没有就传 None。
4. 避坑与排查:轨迹聚类里最容易翻车的五件事
4.1 现象:聚类结果每次跑都不一样
原因:K-means 初始化是随机的,不固定 random_state 的话每次初始中心不同,结果自然不同。另外如果用了 DBSCAN,数据顺序也会影响边界点的归属。
解决:K-means 加random_state=42,DBSCAN 前先对数据做固定顺序的排序。如果换了 random_state 结果差异很大,说明数据本身簇结构不明显,不是算法问题。
4.2 现象:所有轨迹被分到一簇,或者每条轨迹自成一簇
原因:距离矩阵的量纲不对。所有轨迹被分一簇通常是距离值整体偏小,聚类算法分不开;每条自成一簇是距离值整体偏大,邻域半径够不到任何邻居。
解决:先打印距离矩阵的统计量,看 min、max、mean 和分位数。正常情况距离分布应该有明显的低值聚集区。如果 max 和 min 差几个数量级,先做归一化再聚类。DBSCAN 的 eps 可以取距离矩阵的 10% 到 20% 分位数作为起点试。
4.3 现象:轨迹形状明显不同却被聚到一起
原因:距离度量只看了位置没看方向,或者重采样点数太少把形状差异抹平了。
解决:换带方向的度量,比如在距离里加入航向角差异,或者用 DTW 替代逐点欧氏距离。重采样点数从 50 提到 100 再试。如果还是不行,检查是不是坐标投影有问题,经纬度直接算距离会让南北向和东西向的轨迹距离失真。
4.4 现象:程序跑得极慢,几千条轨迹就卡死
原因:距离矩阵是 O(n²) 的,逐对计算加上 DTW 这种高复杂度度量,n 到几千就撑不住。另外 Python 循环算距离没有向量化,白白浪费算力。
解决:用scipy.spatial.distance.cdist或者 sklearn 的pairwise_distances做向量化计算。轨迹数量超过五千时,先用 MiniBatchKMeans 或者对轨迹做分段代表点降维,再聚类。层次聚类在 n 大于两千时基本不要考虑。
4.5 现象:可视化图上轨迹叠成一团,看不出聚类效果
原因:轨迹没有按簇着色,或者所有轨迹用了同一个颜色和线宽,重叠后就是一团黑。
解决:按 labels 给每条轨迹分配颜色,用plt.cm.tab10或tab20色板。轨迹多的时候降低 alpha 到 0.3,聚类中心用黑色粗线单独画。如果还是看不清,分簇画子图,每簇一张,比全叠在一起有用得多。
5. 进阶:换距离度量与用轮廓系数验证聚类质量
跑通默认流程之后,真正决定这个项目对你有没有用的,是你能不能把距离度量换成贴合自己数据的版本。项目里 common.py 和 trajectory.py 是动刀的地方。我一般会先把距离函数抽出来单独测:拿两条已知相似的轨迹和两条已知不相似的轨迹,看算出来的距离能不能把相似的对排前面。这一步过了再进聚类,否则后面调参全是玄学。
换度量的常见方向有三个。一是加入速度或时间维度,把(x, y)扩成(x, y, t)或者(x, y, v),距离里给时间差一个权重。二是用 DTW 处理速度不一致的轨迹,代价是计算慢,可以用 FastDTW 做近似。三是分段后再算距离,把长轨迹切成等长的段,段内算距离再聚合,适合轨迹长度差异大的场景。
换完度量怎么知道聚类变好了?轮廓系数(Silhouette Score)是最常用的内部指标,取值 -1 到 1,越接近 1 说明簇内越紧、簇间越远。用预计算距离矩阵时直接传给 sklearn:
from sklearn.metrics import silhouette_score # dist_mat 是前面算好的距离矩阵,labels 是聚类结果 score = silhouette_score(dist_mat, labels, metric='precomputed') print(f'Silhouette Score: {score:.3f}')注意轮廓系数对簇的形状有假设,DBSCAN 这种非凸簇用它会偏低,这时候看 Calinski-Harabasz 指数或者直接看可视化更靠谱。我的习惯是每次改完距离度量或聚类参数,都把轮廓系数和可视化图一起存下来,文件名带上参数,比如sil_0.42_eps0.8.png,回头对比不用重新跑。
还有一个容易忽略的点:聚类数量不是越多越好,也不是轮廓系数越高越好。交通场景里,簇太多意味着你把同一种流向拆成了好几类,业务上没法解释。我一般会限制簇数不超过实际可能流向数的两倍,超过就回头检查距离度量是不是把噪声放大了。
从那以后我每次拿到新的轨迹数据,都强制先跑一遍重采样加距离矩阵统计,确认距离分布合理再进聚类,省掉了很多事后返工。希望帮到你。
本文还有配套的精品资源,点击获取