ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python轨迹聚类实战:从距离度量到聚类算法与可视化

Python轨迹聚类实战:从距离度量到聚类算法与可视化 简介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_points50): 把一条轨迹重采样成固定点数消除采样频率差异 traj np.array(traj) # 按累计弧长做线性插值保证重采样后点的间距均匀 dists np.sqrt(((np.diff(traj, axis0)) ** 2).sum(axis1)) 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_points50): 构造轨迹两两之间的距离矩阵 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(axis1).mean() return dist_mat这段代码有两个关键参数。n_points 控制重采样后的点数设太小会抹平轨迹形状设太大对距离计算没帮助还拖慢速度一般 30 到 100 之间试。距离计算里用了d.min(axis1).mean()意思是每个点找另一条轨迹上最近的点取这些最近距离的均值这样对局部错位有一定容忍度。如果你要严格逐点对齐直接换成np.linalg.norm(resampled[i] - resampled[j], axis1).mean()。拿到距离矩阵后聚类就是标准操作了。K-means 不能直接吃距离矩阵需要先做多维缩放或者用 KMedoidsDBSCAN 和层次聚类可以直接用预计算的距离矩阵。项目里如果用的是 sklearn注意metricprecomputed这个参数传错会报错或者算出莫名其妙的结果。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.gifimg 目录下的 trajclus.png 是静态聚类结果图通常画的是所有轨迹叠在一起不同簇用不同颜色聚类中心用粗线或标记点标出。roundabout.gif 是环岛场景的动态展示能看出轨迹随时间的走向和聚类归属变化。这两个图不是装饰是你验证聚类有没有跑对的参照。自己画图时重点看三件事同一簇的轨迹是不是走向一致聚类中心是不是落在轨迹密集的通道上有没有明显该分到一类却被拆开的轨迹。如果图上看不出结构先别调聚类参数回头检查距离矩阵是不是算错了。常见错误是坐标没做投影直接用经纬度算欧氏距离纬度方向一度和一经度一度的实际距离不一样结果会被纬度差异主导。数据量小的时候用 UTM 投影数据量大或者跨区域就用 Haversine 距离。import matplotlib.pyplot as plt def plot_clusters(trajectories, labels, centersNone): 按簇标签画轨迹中心用加粗线标出 plt.figure(figsize(10, 8)) for traj, lab in zip(trajectories, labels): traj np.array(traj) plt.plot(traj[:, 0], traj[:, 1], colorplt.cm.tab10(lab), alpha0.5, linewidth1) if centers is not None: for c in centers: c np.array(c) plt.plot(c[:, 0], c[:, 1], colorblack, linewidth3) plt.axis(equal) plt.savefig(trajclus_result.png, dpi150) plt.show()axis(equal)要加上不然 x 和 y 轴比例不一致轨迹形状会被拉伸看起来像另一条路。alpha 设 0.5 是为了轨迹重叠时还能看出密度。centers 传聚类中心轨迹没有就传 None。4. 避坑与排查轨迹聚类里最容易翻车的五件事4.1 现象聚类结果每次跑都不一样原因K-means 初始化是随机的不固定 random_state 的话每次初始中心不同结果自然不同。另外如果用了 DBSCAN数据顺序也会影响边界点的归属。解决K-means 加random_state42DBSCAN 前先对数据做固定顺序的排序。如果换了 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 说明簇内越紧、簇间越远。用预计算距离矩阵时直接传给 sklearnfrom sklearn.metrics import silhouette_score # dist_mat 是前面算好的距离矩阵labels 是聚类结果 score silhouette_score(dist_mat, labels, metricprecomputed) print(fSilhouette Score: {score:.3f})注意轮廓系数对簇的形状有假设DBSCAN 这种非凸簇用它会偏低这时候看 Calinski-Harabasz 指数或者直接看可视化更靠谱。我的习惯是每次改完距离度量或聚类参数都把轮廓系数和可视化图一起存下来文件名带上参数比如sil_0.42_eps0.8.png回头对比不用重新跑。还有一个容易忽略的点聚类数量不是越多越好也不是轮廓系数越高越好。交通场景里簇太多意味着你把同一种流向拆成了好几类业务上没法解释。我一般会限制簇数不超过实际可能流向数的两倍超过就回头检查距离度量是不是把噪声放大了。从那以后我每次拿到新的轨迹数据都强制先跑一遍重采样加距离矩阵统计确认距离分布合理再进聚类省掉了很多事后返工。希望帮到你。本文还有配套的精品资源点击获取
返回列表