
简介TrajectoryClustering-master 是一份面向数据挖掘与地理空间分析学习者的 Python 轨迹聚类实战源码适合具备一定 Python 基础、希望理解 GPS 轨迹与移动位置数据聚类流程的开发者与数据科学从业者。资源围绕轨迹数据预处理、距离度量、聚类算法实现与结果可视化展开可用于交通流量分析、用户行为研究、动物迁徙模式识别等场景。压缩包共 7 个文件约 998KB以 4 个 py 脚本为核心辅以 1 个 md 说明文档、1 个 png 聚类效果图与 1 个 gif 动态演示脚本分别承担轨迹读取、聚类计算与公共工具函数等职责结构紧凑便于按模块阅读。目前已有 1216 人学习下载。通过研读源码读者可掌握轨迹清洗、相似性度量、K-means 或 DBSCAN 等聚类算法的落地方式并借助可视化直观查看聚类中心与轨迹分布同时理解轮廓系数等评估思路为后续项目实践提供可复用的代码参考与排错经验。1. 轨迹聚类到底在聚什么从 GPS 点到行为模式的第一次跳跃轨迹聚类这个词第一次听容易以为是给地图上的线做分组。实际做过就知道它处理的是带时间戳的坐标序列目标是把“走得像”的轨迹归到一起——通勤路线、配送绕行、渔船作业区、车辆异常变道都是它的射程。TrajectoryClustering 这类 Python 项目通常解决三件事把原始 GPS 点清洗成可比序列、定义两条轨迹之间的距离、用聚类算法把距离近的归堆。它适合手里有轨迹数据、想做模式发现却卡在“点太多、长度不一、噪声大”的工程师。难点不在调 sklearn 的聚类函数而在前面那步怎么让两条采样频率不同、起点不同、长度不同的轨迹变得可比较。这一步没做对后面聚类出来的结果基本是玄学。2. 轨迹相似度怎么算从欧氏距离到 DTW 与 Hausdorff2.1 为什么不能直接对坐标点做欧氏聚类很多人第一次上手会把每条轨迹的所有点拉平成一个长向量然后直接丢进 KMeans 或 DBSCAN。这个做法在轨迹长度一致、采样频率固定、起点对齐时勉强能用但现实数据几乎不满足这三个条件。一条轨迹 200 个点另一条 80 个点拉平后维度不同欧氏距离根本算不了。就算强行重采样到同一长度起点错位也会让距离虚高——两条完全相同的路线一条从 A 出发、一条从 B 出发欧氏距离会告诉你它们差很远。常见做法是先做重采样把每条轨迹统一到固定点数再做归一化减去起点或减去质心最后才谈距离。但重采样会丢时间信息对速度变化敏感的场景不合适。所以选距离度量之前先问自己我关心的是形状相似还是时空都相似2.2 DTW、Hausdorff 和欧氏距离的选型对照距离度量适用场景对长度不齐对时间偏移计算成本欧氏距离已对齐、等长、采样一致不支持敏感低DTW形状相似、允许时间伸缩支持鲁棒中高Hausdorff关注最大偏离、形状包络支持不敏感中编辑距离离散化后的轨迹序列支持部分支持中DTW 是轨迹聚类里最常被提到的但它有个坑两条轨迹只要有一小段绕路DTW 会把这段的代价摊到整条路径上导致距离被拉高。Hausdorff 只看最大偏离点对局部噪声敏感。实际项目里我一般先用 DTW 做基线如果发现聚类结果被少数绕路轨迹带偏再换 Hausdorff 或加窗口约束的 DTW。2.3 用 Python 算两条轨迹的 DTW 距离import numpy as np from scipy.spatial.distance import cdist def dtw_distance(traj_a, traj_b, windowNone): traj_a, traj_b: shape (n, 2) 或 (n, 3) 的坐标序列 window: Sakoe-Chiba 窗口半径None 表示无约束 返回归一化后的 DTW 距离 n, m len(traj_a), len(traj_b) # 代价矩阵每对点的欧氏距离 cost cdist(traj_a, traj_b, metriceuclidean) # 累积代价矩阵初始化为无穷大 acc np.full((n 1, m 1), np.inf) acc[0, 0] 0.0 for i in range(1, n 1): # 窗口约束只计算对角线附近的范围 j_start max(1, i - window) if window else 1 j_end min(m, i window) if window else m for j in range(j_start, j_end 1): acc[i, j] cost[i-1, j-1] min( acc[i-1, j], # 插入 acc[i, j-1], # 删除 acc[i-1, j-1] # 匹配 ) return acc[n, m] / max(n, m) # 按路径长度归一化这段代码的核心是累积代价矩阵的递推。acc[i, j]表示前 i 个点与前 j 个点的最小累积代价三个转移分别对应插入、删除、匹配。window参数控制 Sakoe-Chiba 窗口限制对齐路径不能偏离对角线太远既能加速又能防止病态对齐。最后除以max(n, m)做归一化否则长轨迹天然距离更大聚类时会偏向把短轨迹归一堆。参数上window一般取轨迹长度的 10% 到 20%。太小会漏掉合理的时间偏移太大等于没约束。如果轨迹点特别密比如每秒一个点建议先降采样到 5 到 10 秒一个点再算 DTW否则 O(n²) 的复杂度会让聚类跑不动。3. 聚类算法怎么选DBSCAN、层次聚类和 KMeans 在轨迹上的真实表现3.1 轨迹聚类为什么常选 DBSCAN轨迹数据的第一个特点是簇形状不规则第二个特点是噪声多。KMeans 假设簇是球形且数量已知这两条都不满足。DBSCAN 基于密度不需要预设簇数量还能把低密度区域的轨迹标为噪声——这对轨迹数据很关键因为总有一些零散的、不构成模式的轨迹。但 DBSCAN 在轨迹上的直接应用有个前提你得先有距离矩阵。它不接受原始坐标序列只接受样本间的距离。所以流程是先算 N 条轨迹两两之间的 DTW 距离得到 N×N 矩阵再把矩阵喂给 DBSCAN。from sklearn.cluster import DBSCAN import numpy as np def cluster_trajectories(trajectories, eps0.5, min_samples3, window10): trajectories: list of np.array, 每条 shape (n_i, 2) eps: DBSCAN 邻域半径需根据距离分布调整 min_samples: 核心点的最小邻居数 n len(trajectories) dist_matrix np.zeros((n, n)) for i in range(n): for j in range(i 1, n): d dtw_distance(trajectories[i], trajectories[j], window) dist_matrix[i, j] d dist_matrix[j, i] d # 预计算距离矩阵传入 DBSCAN db DBSCAN(epseps, min_samplesmin_samples, metricprecomputed) labels db.fit_predict(dist_matrix) return labels, dist_matrixmetricprecomputed是关键告诉 DBSCAN 输入已经是距离矩阵。eps的设定不能拍脑袋要先看距离矩阵的分布把所有非对角元素排序画个直方图找第一个谷底位置。min_samples一般取 3 到 5轨迹数据本身样本量不会太大设太高会导致大量轨迹被判为噪声。3.2 层次聚类在轨迹上的适用边界层次聚类的好处是能输出树状结构方便按业务需要切不同粒度的簇。轨迹数据里如果想知道“哪些路线属于同一大类、哪些是细分变体”层次聚类比 DBSCAN 更直观。缺点是 O(n²) 到 O(n³) 的复杂度轨迹条数超过几千条就基本跑不动。用层次聚类时linkage 方法的选择比距离度量还重要。single容易产生链式效应一条轨迹接一条轨迹全串起来complete对噪声敏感average是比较稳的默认选择。如果距离矩阵已经算好直接传进去from scipy.cluster.hierarchy import linkage, fcluster def hierarchical_cluster(dist_matrix, n_clusters5, methodaverage): # condensed 形式取上三角 condensed dist_matrix[np.triu_indices_from(dist_matrix, k1)] Z linkage(condensed, methodmethod) labels fcluster(Z, tn_clusters, criterionmaxclust) return labels, Zmethodaverage是 UPGMA对轨迹长度差异的容忍度比complete好。criterionmaxclust表示按最大簇数切也可以换成distance按距离阈值切。实际用的时候我一般先跑average看树状图再决定切几刀。3.3 KMeans 在轨迹上的正确打开方式KMeans 不是不能用但必须满足两个条件轨迹已经对齐到相同长度且距离用欧氏。做法是先把所有轨迹重采样到固定点数再做归一化然后拉平。这样 KMeans 的质心才有物理意义——它代表一条“平均轨迹”。from sklearn.cluster import KMeans from scipy.interpolate import interp1d def resample_trajectory(traj, n_points50): 把轨迹重采样到固定点数 t np.linspace(0, 1, len(traj)) t_new np.linspace(0, 1, n_points) f_x interp1d(t, traj[:, 0], kindlinear) f_y interp1d(t, traj[:, 1], kindlinear) return np.column_stack([f_x(t_new), f_y(t_new)]) def kmeans_trajectories(trajectories, n_clusters5, n_points50): resampled np.array([resample_trajectory(t, n_points) for t in trajectories]) # 减去每条轨迹的起点消除绝对位置影响 resampled resampled - resampled[:, 0:1, :] X resampled.reshape(len(trajectories), -1) km KMeans(n_clustersn_clusters, n_init10, random_state42) labels km.fit_predict(X) return labels, kmn_init10是必须的KMeans 对初始质心敏感单次运行容易陷局部最优。减去起点这步看场景如果关心的是绝对路线不能减如果关心的是形状减掉更合理。重采样点数n_points一般取 30 到 100太少丢形状细节太多维度爆炸。4. 避坑与排查轨迹聚类翻车的五个真实场景4.1 聚类结果全是噪声labels 里 -1 占了一大半现象DBSCAN 跑完大部分轨迹被标为 -1只有零星几个簇。原因eps设得太小或者距离矩阵没有归一化导致所有轨迹之间的距离都大于eps。解决先打印距离矩阵的统计量看中位数和四分位数。eps一般取距离分布的 60% 到 80% 分位数。如果距离值普遍很大检查 DTW 是否做了长度归一化——没归一化的话长轨迹之间的距离会天然偏大。4.2 两条肉眼看着一样的轨迹距离却很大现象两条路线几乎重合但 DTW 距离排在前几名。原因坐标系问题。GPS 原始坐标是经纬度纬度方向 1 度约 111 公里经度方向随纬度变化。直接算欧氏距离在高纬度地区经度方向的差异会被低估或高估。解决先投影到平面坐标比如 UTM 或 Web Mercator。Python 里用pyproj做转换或者简单点把经度乘以cos(纬度)再算距离。这个坑在跨纬度数据上特别明显低纬度数据可能感觉不到。4.3 聚类数量每次跑都不一样现象同样的数据换个随机种子或换个顺序簇的数量和成员都变了。原因如果用的是 KMeans初始质心随机导致如果用的是 DBSCAN轨迹输入顺序会影响边界点的归属。解决KMeans 设random_state并增大n_init。DBSCAN 的边界点归属本身就有歧义如果业务上不能接受改用层次聚类或 HDBSCAN。另外距离矩阵的计算顺序不影响结果但浮点误差累积可能导致微小差异建议对距离矩阵做四舍五入到小数点后 6 位。4.4 轨迹点太多聚类跑了一晚上没跑完现象几千条轨迹每条几百个点DTW 距离矩阵计算卡死。原因DTW 是 O(n²) 每条轨迹对N 条轨迹就是 O(N²·n²)计算量爆炸。解决三个方向。第一降采样把每条轨迹的点数降到 50 以内。第二用 FastDTW 或加 Sakoe-Chiba 窗口把复杂度降到 O(n·window)。第三先用欧氏距离或 Hausdorff 做粗筛只对距离较近的轨迹对算 DTW。实际项目里我一般先降采样到 30 个点再开窗口几千条轨迹几分钟能跑完。4.5 聚类出来的簇在业务上没法解释现象算法说这是三类但业务方看了说“这三类混在一起没有区分度”。原因距离度量没有对齐业务目标。比如业务关心的是“走哪条路”但 DTW 对速度变化也敏感导致同一路线不同速度的轨迹被分到不同簇。解决把业务约束加进距离计算。如果只关心路径形状先对轨迹做空间重采样按距离而不是按时间再算 DTW。如果关心时间就在距离里加时间维度的权重。聚类不是纯算法问题距离定义才是核心。5. 让轨迹聚类真正可用的三个进阶技巧5.1 用轮廓系数和业务指标双重验证簇质量轮廓系数是聚类里最常用的内部指标但它对轨迹聚类有个陷阱它假设簇是凸的而轨迹簇往往不是。所以轮廓系数只能当参考不能当唯一标准。我一般同时看两个东西一是簇内距离的均值和簇间距离的均值之比比值小于 0.5 说明分离度还行二是随机抽几条轨迹人工看它们是否真的属于同一模式。from sklearn.metrics import silhouette_score def evaluate_clustering(dist_matrix, labels): # 排除噪声点 mask labels ! -1 if len(set(labels[mask])) 2: return None sil silhouette_score(dist_matrix[mask][:, mask], labels[mask], metricprecomputed) # 簇内/簇间距离比 unique_labels set(labels[mask]) intra, inter [], [] for k in unique_labels: idx_k np.where(labels k)[0] for i in idx_k: for j in idx_k: if i j: intra.append(dist_matrix[i, j]) for k2 in unique_labels: if k2 k: continue idx_k2 np.where(labels k2)[0] for i in idx_k: for j in idx_k2: inter.append(dist_matrix[i, j]) ratio np.mean(intra) / np.mean(inter) if inter else float(inf) return sil, ratiosilhouette_score传metricprecomputed时输入必须是距离矩阵而不是特征矩阵。ratio小于 0.5 是我个人的经验阈值不是绝对标准但比单看轮廓系数靠谱。5.2 用 HDBSCAN 替代 DBSCAN 处理密度不均的轨迹DBSCAN 的eps是全局的但轨迹数据的密度往往不均匀——市区轨迹密集郊区稀疏。HDBSCAN 不需要设eps它通过层次密度估计自动适应不同密度的簇。安装pip install hdbscan用法和 sklearn 接口类似import hdbscan def hdbscan_cluster(dist_matrix, min_cluster_size5): clusterer hdbscan.HDBSCAN( min_cluster_sizemin_cluster_size, metricprecomputed ) labels clusterer.fit_predict(dist_matrix) return labelsmin_cluster_size是最小簇的轨迹条数比 DBSCAN 的min_samples更直观。HDBSCAN 在轨迹数据上的表现通常比 DBSCAN 稳代价是计算稍慢但省去了调eps的痛苦。5.3 把聚类结果落成可查询的轨迹模式库聚类跑完不是终点把结果存下来、能按模式查询才有价值。我一般会把每条轨迹的簇标签、代表轨迹簇内距离中位数最小的那条、簇的统计信息轨迹数、平均长度、时间范围存成一张表。代表轨迹特别有用业务方不需要看几百条轨迹看几条代表就能理解每个簇的含义。import pandas as pd def build_pattern_library(trajectories, labels, dist_matrix): records [] for k in set(labels): if k -1: continue idx_k np.where(labels k)[0] # 找簇内到其他轨迹距离和最小的那条作为代表 sub_matrix dist_matrix[np.ix_(idx_k, idx_k)] rep_idx idx_k[np.argmin(sub_matrix.sum(axis1))] records.append({ cluster_id: k, trajectory_count: len(idx_k), representative_idx: rep_idx, avg_length: np.mean([len(trajectories[i]) for i in idx_k]), intra_distance_mean: sub_matrix[np.triu_indices_from(sub_matrix, k1)].mean() }) return pd.DataFrame(records)这张表可以直接写进数据库或存成 CSV后续做可视化、做异常检测、做路线推荐都从这张表出发。代表轨迹的选择用“到簇内其他轨迹距离和最小”这条规则比随机选或选第一条稳定得多。做轨迹聚类这几年最大的教训是距离定义决定了聚类结果的上限算法只是在下限上做优化。我见过太多人花一周调 DBSCAN 参数却没花半天想清楚两条轨迹到底该怎么比。先把距离矩阵算对再谈聚类顺序反了就是白干。希望帮到你。本文还有配套的精品资源点击获取