ARTICLE DETAIL

资讯详情

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

ST-DBScan时空聚类详解:双阈值密度聚类原理与Python实现

ST-DBScan时空聚类详解:双阈值密度聚类原理与Python实现 简介一份基于Python语言实现的ST-DBScan时空密度聚类算法代码包专为需要处理空间聚类与噪声场景的数据分析、机器学习开发者提供参考。该算法的核心在于无需预先指定簇数量仅需设定半径与最小邻居数两个参数即可基于空间密度自动识别高密度连通区域并有效区分噪声点适合森林砍伐区域划定、肿瘤影响器官识别等空间特征聚类应用。压缩包共8个文件包含3个Python脚本算法主体、辅助函数与演示调用、1个CSV数据文件用于测试的样本集、1张效果示意图、1份Markdown说明文档以及许可证和忽略规则文件整体仅660KB结构紧凑便于逐文件阅读与二次开发。目前已有708人学习下载读者可结合源码中的注释和示例数据快速理解ST-DBScan的迭代过程并根据自身数据调整半径与最小邻居参数直接复用或扩展至遥感、医疗影像等真实项目节省从零实现算法的调研和编码时间。1. 什么是ST-DBScan普通DBSCAN聚不动带时间戳的点ST-DBScan时空DBSCAN在Python里跑通并不难网上能搜到很多源码片段但难点在于搞清楚它跟普通DBSCAN的差别到底在哪——差的不只是多了一列时间戳而是整个邻域判定逻辑都变了。给你一条网约车的GPS轨迹点集每一行是经度、纬度、时间戳你真正关心的问题是“哪个区域在哪个时间段是热点”晚高峰的商圈、午间的园区、深夜的酒吧街。普通DBSCAN只算空间距离会把凌晨3点和晚上8点的同一片街区揉进一个簇手动按小时切片再做DBSCAN又会在时间边界上产生断裂。ST-DBScan做的就是把“空间邻域”和“时间邻域”同时作为聚类的必要条件适合轨迹分析、犯罪热点检测、POI挖掘这类既看位置又看时段的场景。新手照着下面的最小实现可以当天跑通熟手则可以重点关注第5章的参数坑和索引优化。2. 双阈值密度聚类的原理空间邻域和时间邻域必须同时命中网上很多ST-DBScan文章把原理讲得很玄实际上就是一句话两个点要成为“邻居”不光距离要足够近发生时间也要足够接近。这一章把DBSCAN的三个核心概念迁移到时空场景顺便解释为什么“同时满足”比“加权距离”更实用以及为什么直接按时间切片聚类会出问题。2.1 核心点、边界点与噪声点DBSCAN的三个角色在时空场景下的迁移经典DBSCAN里只有空间坐标两个点之间的距离就是欧氏距离或球面距离。ST-DBScan把每个点扩展成三维结构x坐标或经纬度、y坐标、时间t但注意空间和时间不是拼成一个向量去算“总距离”而是分别计算、求交集。用学术点的说法邻域定义从“半径eps的圆”变成了“半径eps的圆且时间差≤eps_time的柱体”。在这个定义下点的角色划分跟原始DBSCAN完全一致。如果点p的时空邻域内空间≤eps、时间差≤eps_time包含至少min_pts个点p就是核心点不属于任何簇的孤立点是噪声点剩下的被核心点“捞”进来但自己不满足核心点条件的就是边界点。核心点是簇的骨架边界点决定簇的形状噪声点则被直接丢弃。这个设计有一个非常直接的好处参数对应业务口径。你不需要思考“时间当量换算成多少米”这种抽象问题只需要回答“我关心多大空间范围和多长时间窗口”——比如300米内、5分钟内至少出现10个点就算一个热点簇。2.2 为什么是“同时满足”而不是加权距离参数物理意义的取舍这是ST-DBScan最常见的认知分歧。有人会问把时间和空间拼成一个三维向量算加权欧氏距离再设一个eps不也能聚类吗能但有两个致命问题。第一权重λ的物理意义模糊。λ0.5表示“1秒的时间差等同于0.5米的空间距离”这个换算关系在业务上根本说不通换一个数据集就得重新标定跟猜参数没什么区别。第二加权距离会把时间相关性稀释。两个点相距50米但时间差2小时加权距离可能仍然很小于是被划进同一个簇这恰恰是ST-DBScan要避免的情况。双阈值交集的做法则不存在这两个问题。判定逻辑是纯量化的点p和点q互为时空邻居当且仅当空间距离dist(p, q) ≤ eps 且 时间绝对差 |t_p - t_q| ≤ eps_time。下面是两种判定的直观对比表判定维度普通DBSCANST-DBScan输入数据[x, y] 或 [经纬度][x, y, t] 或 [经纬度, t]邻居条件空间距离 ≤ eps空间距离 ≤ eps 且 时间差 ≤ eps_time核心点判定邻居数 ≥ min_pts邻居数 ≥ min_pts同时满足双阈值典型盲区昼夜同址混为一簇空间近但时间远的簇被正确拆开这个表格里的最后一行就是ST-DBScan存在的全部理由它筛掉的不是“离得远的点”而是“虽然离得近但根本不是同一个时段事件”的点。识别同一片街区里白天和夜晚的两个热点这种能力是普通DBSCAN给不了的。2.3 和按时间切片聚类的对比边界断裂问题的由来一个朴素的替代方案是先把数据按小时或半小时切片每个切片单独跑一次DBSCAN最后再把结果合并。这种办法看似简单实际用起来非常痛苦。首先切片边界必然产生断裂。假设你按小时切片22:55和23:05的两个点其实是同一波晚高峰人群但被切到两个切片里各自都因为邻居数量不足min_pts而被判成噪声。想缓解这个问题就得让切片重叠比如用滑动窗口但滑动窗口会让同一个点出现在多个切片里聚出的簇在两个窗口里还会打架——计数口径到底以哪个为准其次参数被乘以切片数量。每个切片都要单独调一遍eps和min_pts而且夜间切片点稀疏、白天切片点密集min_pts对两个切片根本不好取同一个值。ST-DBScan把时间维度显式建模在邻域判定里时间边界不再由人为切片决定而是由eps_time参数决定本质上是一个“滑动的、数据驱动的窗口”不存在边界断裂问题。这也是它在轨迹聚类和时空事件检测里成为默认选项的原因。3. 用Python写一个最小ST-DBScan核心代码与运行效果这一章直接给可以抄走的代码。我不会把整个文件铺出来吓唬人而是按“输入约定 → 核心算法 → 构造测试数据跑通 → 评估这段代码能用在哪”四步走完整。前置依赖只有numpy环境里没有的话先执行pip install numpyPython 3.8以上即可不需要sklearn。3.1 输入数据约定每个点只有三个数字算法输入是一个形状为(n, 3)的NumPy数组每一行是[x, y, t]。x和y代表平面坐标单位建议统一成“米”t是数值时间戳单位建议统一成“秒”Unix时间戳。为什么建议而不是强制因为ST-DBScan的时间阈值eps_time直接跟t的单位挂钩你如果用毫秒时间戳eps_time3600就会等同于只筛了3.6秒直接崩掉。关于经纬度怎么换算成米制坐标第4章会细说这里先默认拿到的是已经投影好的坐标。一个简单的读入示例import numpy as np # data.csv: 每行一个点位前三列分别是 x(m), y(m), ts(秒级) raw np.loadtxt(data.csv, delimiter,, usecols(0, 1, 2)) print(raw.shape) # (n, 3) print(raw[:5])这段代码跳过文件头、只读取前三列numpy会自动把数据转换成float64数组。如果你的csv是字符串时间戳不能直接用loadtxt需要先用pandas解析这个处理也留到第4章。3.2 最小实现向量化邻居查询与簇扩展用纯Python双层循环写邻居查询在几千点的规模上还勉强能跑上万点就慢得让人怀疑人生。我这里用NumPy做向量化对每个点一次性计算它跟全量点集的空间距离和时间差再通过布尔掩码取交集。后续拿到邻居列表后用队列做广度优先的簇扩展。完整函数如下import numpy as np from collections import deque def st_dbscan(points, eps, eps_time, min_pts): ST-DBScan最小实现。 points: (n, 3) 数组列依次为 x, y, t eps: 空间邻域半径跟x/y单位一致 eps_time: 时间邻域半径跟t单位一致 min_pts: 核心点最少邻居数 返回: labels-1表示噪声0表示未访问(调用后不会出现)1为簇ID n points.shape[0] labels np.zeros(n, dtypeint) # 0未访问, -1噪声, 0簇ID cluster_id 0 def _neighbors(idx): 向量化计算点idx的时空邻居下标列表 # 空间距离逐行计算二阶范数得到(n,)数组 spatial np.linalg.norm(points[:, :2] - points[idx, :2], axis1) # 时间差绝对值 time_diff np.abs(points[:, 2] - points[idx, 2]) # 双阈值交集 return np.where((spatial eps) (time_diff eps_time))[0] for i in range(n): if labels[i] ! 0: continue seeds _neighbors(i) # 邻居列表包含点i自身去掉避免它干扰min_pts统计 seeds seeds[seeds ! i] if len(seeds) min_pts: labels[i] -1 continue # 点i是核心点以它为中心扩展一个新簇 cluster_id 1 labels[i] cluster_id queue deque(seeds.tolist()) while queue: q queue.popleft() # 未访问的点归入当前簇 if labels[q] 0: labels[q] cluster_id q_nbrs _neighbors(q) q_nbrs q_nbrs[q_nbrs ! q] # 如果q也满足核心点条件把它邻域内尚未访问的 # 邻居入队继续扩展 if len(q_nbrs) min_pts: for nq in q_nbrs: if labels[nq] 0 or labels[nq] -1: queue.append(nq) # 边界点q之前被标为-1但位于当前核心点邻域内 elif labels[q] -1: labels[q] cluster_id return labels代码的逻辑分三层。第一层主循环逐点扫描未访问的点计算它的时空邻居数少于min_pts直接标噪声。第二层对于满足核心点条件的点开辟新簇并把它所有邻居塞进队列。第三层从队列弹出点做扩展未访问点被标记入簇然后检查它是否也是核心点是的话继续把它的邻居入队直到队列为空。边界点通过labels[q] -1这个分支被收编进簇这是DBSCAN系列跟K-Means最大的区别——簇不要求是凸的形状完全由密度决定。参数上要注意几点min_pts在时空场景里建议不要设太小小于5时噪声对结果的影响会明显放大eps的单位跟坐标单位一致如果x/y是米eps就是米eps_time的单位跟t一致t是秒eps_time就填秒。这三个参数的联动会在第5章专门踩坑。3.3 用模拟数据跑通完整的测试脚本与输出解读写算法最容易犯的错是“代码看起来对但跑出来不知道对不对”。我构造一组带明显时空结构的模拟数据两个簇在空间上离得很近、几乎重叠但发生时段完全不同第三个簇在远处、独立成簇。这样能直观检验时间维度是否真的在起作用。rng np.random.default_rng(2024) # 三个活动簇的中心与时间基准 centers { A: (0.0, 0.0, 100.0), # 上午10:00前后 B: (0.8, 0.0, 1000.0), # 中午12:00前后空间上离A很近 C: (-4.0, 2.0, 100.0), # 远处另一个点簇 } points [] for cx, cy, ct in centers.values(): for _ in range(300): x cx rng.normal(0, 0.15) y cy rng.normal(0, 0.15) t ct rng.normal(0, 10) points.append([x, y, t]) # 均匀噪声60个 noise rng.uniform([-5, -3, 50], [5, 3, 1150], size(60, 3)) points np.vstack([np.array(points), noise]) rng.shuffle(points) # 打乱顺序避免输入有序带来的假象 labels st_dbscan(points, eps1.0, eps_time120, min_pts10) n_clusters labels.max() n_noise (labels -1).sum() print(f簇数量: {n_clusters}, 噪声点数: {n_noise}) # 期望输出接近: 簇数量 3, 噪声点数 约 50-60为什么“期望接近3个簇”A和B两个簇的空间中心只差0.8米但eps1.0意味着只看空间距离A和B会被连成同一个簇——这正是普通DBSCAN会犯的错。但因为A的时间基准是100秒、B是1000秒eps_time120秒把两者彻底隔开于是算法输出3个簇。C虽然时间和A相近但空间距离超过eps不会合并。跑完这个脚本建议再做一件事把eps_time改成100000再跑一次你会发现簇数量变成2甚至1。这就直接验证了eps_time的作用。如果输出跟预期不符先检查数据里有没有NaN或Inf再看时间戳量纲是不是秒。3.4 这段代码的边界能跑什么体量、不能跑什么体量上面这份实现在数千到一两万点的规模上足够应付学习和中等体量的实验。它的主要瓶颈在_neighbors的全表扫描对每个点都要跟全体点算一次距离整体复杂度是O(n²)。以我自己的实测经验n2万时单次全量运行在普通笔记本上大概是几十秒量级如果还要做参数网格扫描体验会非常糟糕。所以明确一下边界5万点以下、跑几次调参这份实现够用5万点以上或者要频繁调参请直接跳到第5章换空间索引版本。不要试图在这个基础上加tqdm进度条硬等那是浪费生命。另外这份代码没做并行化也没有做内存优化如果点集超过10万建议先用第6章的网格扫描确定参数再用索引版跑全量。4. 把原始数据喂给算法时间戳与坐标单位的统一真实场景的数据不会像模拟数据那样干净。这一章处理两个最头疼的环节把“2025-03-01 08:15:00”这种字符串变成数值时间戳以及把经纬度换算成以米为单位的平面坐标。这两个环节出问题算法本身再对也是白搭。4.1 CSV到数组时间字符串与经纬度列的处理假设你手里是一份典型的轨迹CSV列结构类似device_id, timestamp, latitude, longitude。首先要做的是把时间列转成秒级Unix时间戳同时把经纬度列拎出来。推荐用pandas处理一步到位import pandas as pd import numpy as np df pd.read_csv(tracks.csv) print(df.dtypes) # 统一转成秒级Unix时间戳 df[ts] pd.to_datetime(df[timestamp]).astype(int64) // 10**9 print(df[ts].min(), df[ts].max()) # 检查时间范围确认不是毫秒 # 组织成算法输入 points df[[latitude, longitude]].to_numpy()代码里最关键的一行是.astype(int64) // 10**9pandas把datetime转为int64时单位是纳秒除以10^9才是秒。如果漏掉这一步你的时间戳会变成1.7e18这种天文数字eps_time无论填什么都等于没约束。检查df[ts].min()和max()是必要的自检动作看到“1690000000”量级的数字才说明转对了。4.2 经纬度距离换算eps用“米”而不是“度”经纬度坐标直接揉进算法有两个问题。第一1度经度的实际长度在赤道约111公里在纬度60度的地方只有约55公里直接用度数算欧氏距离eps在物理上就不是恒定的。第二1度纬度约111公里1度经度在多数地区小于这个值导致x和y轴的尺度不一致距离计算会被经度方向扭曲。常见做法是做一次等距圆柱投影的近似换算把经纬度转成米制坐标。对城市尺度的轨迹分析这个近似误差在可接受范围R_EARTH 6371000.0 def project(lat: np.ndarray, lon: np.ndarray) - np.ndarray: 简化等距投影城市级分析够用跨省级数据建议用UTM lat_rad np.deg2rad(lat) # 用样本平均纬度做参考减少经度方向的变形 ref_lat np.deg2rad(lat.mean()) x lon * np.pi / 180.0 * R_EARTH * np.cos(ref_lat) y lat * np.pi / 180.0 * R_EARTH return np.column_stack([x, y]) proj_xy project(points[:, 0], points[:, 1]) points_3d np.column_stack([proj_xy, df[ts].to_numpy()])投影后x和y的单位是米eps和eps_time的物理含义就清晰了eps300表示300米eps_time1800表示半小时。这里有一个细节值得注意ref_lat用的是整批数据的平均纬度它决定了投影的参考线。如果数据跨度超过两三度约两三百公里这份近似误差会变大届时应该换用更严格的UTM投影专业做法是用pyproj库按数据所在分带选择投影但一般轨迹分析很少跨这么大范围。4.3 数据清洗基操无效时间戳与重复点轨迹数据最常见的脏点有三种。第一种是时间字符串格式混杂比如说有的行是“2025/03/01 08:15”有的是“2025-03-01 08:15:00”pd.to_datetime默认能解析大部分常见格式遇到解析不了的会抛异常或者变成NaT处理方式是使用errorscoerce并丢弃df[ts] pd.to_datetime(df[timestamp], errorscoerce).astype(int64) // 10**9 df df.dropna(subset[ts])第二种是经纬度越界或为0的脏点比如经纬度等于(0,0)的往往是设备异常默认值直接过滤掉。第三种是时间上完全重复的点——同一个设备在同一个秒级时间戳上报了多次这种点会作为重复点抬高局部密度导致min_pts判定失真# 彻底去重同一位置同一秒只保留一个点 df df.drop_duplicates(subset[latitude, longitude, ts], keepfirst)去重操作的逻辑很简单但它在聚类里的影响比大多数人预期的大。一个热点区域如果有设备反复上报同一点会在邻域统计里制造“密度泡沫”让核心点判定失去代表性。清洗完再进行投影和转换结果会稳定很多。4.4 为什么不需要归一化双阈值判断的天然解耦不少做机器学习出身的读者会本能地对x、y、t做MinMax归一化把三个维度压到[0,1]区间再设一个统一的eps。这是ST-DBScan最容易踩的陷阱之一原因在于算法根本不计算“三维综合距离”归一化反而会把物理含义摧毁。假设x和y都是米制坐标范围在几万米量级t在秒级、范围在86400左右。做MinMax归一化后x、y、t都在[0,1]里eps0.05看似合理但它对应的物理含义成了“坐标全范围的5%”既不是米也不是秒完全说不清。而且归一化压缩了x和y的相对尺度后空间上相距几百米的点可能被算作“很近”时间上差几个小时的点也被算作“很近”聚类结果会严重偏离业务认知。正确姿势是保持原始物理单位让eps以米为单位、eps_time以秒为单位。这样你在调参时的每一个数都是可以直接跟业务对口径的“半径300米、时间窗口5分钟”。如果觉得x/y量级太大影响直觉可以把坐标从米换算成公里x/1000但eps也要对应填0.3表示300米——保持单位一致即可不要做归一化。5. ST-DBScan避坑指南5个会让聚类翻车的问题这一章全是我实际跑数据时踩过的坑按“现象 → 原因 → 解决”写。每一个坑都对应一种“代码没报错、结果却明显不对”的场景属于没有报错信息最难排查的那类问题。希望你能避开这些血泪经验。5.1 时间单位不统一聚出来全是噪声现象数据量明明很大但跑完ST-DBScan后噪声占比超过80%有效簇寥寥无几。原因时间戳量纲混乱。最常见的是同一份数据里既有秒级时间戳又有毫秒级时间戳或者你处理时漏了// 10**9这一步。此时如果eps_time设成3600秒毫秒时间戳的点之间差值只有几十到几百看似宽松的阈值实际上筛掉了绝大多数跨时段邻居。解决在喂给算法之前先检查时间戳范围图省事的话直接用pandas统一转换并打印min-max做自检ts pd.to_datetime(df[timestamp], errorscoerce).astype(int64) # 如果min-max在1e18量级说明是纳秒1e12是微秒1e9才是秒 print(ts.min(), ts.max()) df[ts_sec] ts // 10**95.2 eps和eps_time比例失衡要么一坨、要么全散现象调参时发现eps稍微调大一点所有簇就连成一片稍微调小一点又全是噪声几乎找不到中间地带。原因eps和eps_time两个阈值是求交集的关系较大的一侧约束实际上失效。比如点集覆盖范围是5公里×5公里你设eps5000那空间约束等于不存在聚类退化成纯按时间分簇反过来eps_time设成一天时间约束也等于不存在聚类退化成纯空间DBSCAN。很多人只盯eps调参忘了eps_time可能在拖后腿。解决先解耦调参。第一步忽略时间列用普通DBSCAN或ST-DBScan把eps_time设成极大值如10**9单独定出合理的eps区间第二步固定eps扫描eps_time从分钟级到小时级的几个档位观察簇数量变化曲线。按我的经验eps_time取数据时间跨度的1%到5%通常是比较合理的起点比如数据跨度24小时eps_time先试1440秒到4320秒。5.3 五万条以上数据卡死从O(n²)到空间索引现象数据量到5万以上跑一次要几分钟到十几分钟调参根本没法进行。原因第3章的最小实现里_neighbors是全表扫描每查一个点的邻居都要算一次n维距离整体O(n²)。这不是Python慢复杂度决定了它不可能快。解决用scipy.spatial.cKDTree先做空间近邻检索把候选集缩小再在候选集里筛时间。ST-DBScan里最耗时的永远是“找空间邻居”而空间KD树正好解决这个问题时间维度的筛选因为候选集已经很小纯Python循环就可以from scipy.spatial import cKDTree # 建树一次整个聚类过程复用 tree cKDTree(points[:, :2]) def neighbors_fast(idx, eps, eps_time): 先空间粗筛再时间精筛替代3.2节的全表_neighbors cand tree.query_ball_point(points[idx, :2], reps) t0 points[idx, 2] # 候选里去掉自身再按时间阈值过滤 return [c for c in cand if c ! idx and abs(points[c, 2] - t0) eps_time]把3.2节st_dbscan里的_neighbors替换成neighbors_fast其余逻辑不动。注意空间建树用的points[:, :2]必须是投影后的米制坐标否则eps的物理单位又会出错。效果方面5万点从几分钟降到了一两秒这是ST-DBScan落地时最值得做的一次优化。5.4 盲目归一化让eps失去物理含义现象对x、y、t做MinMax归一化后聚类结果出现“空间上挨着但时间上差半天”的簇完全不符合业务直觉。原因如4.4节所述归一化后eps不再对应任何物理单位空间和时间尺度之间形成了不可控的扭曲。解决不要对ST-DBScan的输入做归一化。坐标保持米、时间保持秒必要时只做单位换算米转公里、秒转分钟保证有明确的物理语义。如果你是因为坐标和时间量级差异太大而不适应记住算法本来就不是靠“三维距离”判定的不存在量级不平衡的问题。5.5 边界点归属不稳定不是bug是特性现象同一份数据改成从某个点开始聚类或者换了遍历顺序部分边界点的簇归属变了簇的个数也可能出现微妙变化。原因DBSCAN系列对边界点的归属没有唯一性保证。一个边界点如果同时落在两个核心点的邻域内最终归入哪个簇取决于哪个簇先扩展到这里。这是算法本性不是实现缺陷。核心点和噪声点的划分是稳定的不稳定的是边界点。解决如果业务上对边界点敏感比如“这个用户到底属于哪个热点区域”就不要让边界点承担关键决策。做法是聚类后只保留核心点作为簇的代表边界点单独标记为“边缘活跃点”不进簇内做统计口径。而且要接受一个事实同一份数据用不同实现跑边界点归属不同很正常报告的指标应基于核心点集合。6. 让聚类结果可解释双视图可视化与参数网格扫描算法跑通只是第一步怎么说服业务方“这3个簇确实是热点”才是落地的关键。推荐同时画两张图空间俯视图负责看“在哪聚成了团”时间-空间剖面图负责看“这些簇是不是确实分布在不同的时间段”。6.1 双视图可视化空间俯视图与时间-空间剖面图import matplotlib.pyplot as plt fig, axes plt.subplots(1, 2, figsize(13, 5)) # 左图空间俯视颜色簇ID白色噪声 ax1 axes[0] sc1 ax1.scatter(points[:, 0], points[:, 1], clabels, cmaptab10, s6, alpha0.9) ax1.set_xlabel(x (m)) ax1.set_ylabel(y (m)) ax1.set_title(Spatial Clusters) # 右图横轴时间(小时)纵轴x坐标颜色簇ID ax2 axes[1] sc2 ax2.scatter(points[:, 2] / 3600.0, points[:, 0], clabels, cmaptab10, s6, alpha0.9) ax2.set_xlabel(Time (hours)) ax2.set_ylabel(x (m)) ax2.set_title(Time-Space Profile) plt.tight_layout() plt.savefig(st_dbscan_result.png, dpi150)右图的作用在于验证时间维度是否生效如果两个簇的空间中心几乎重叠但时间剖面图上分居左右两侧就说明eps_time的约束在正确工作。如果右图所有点混成一团没有明显分层先怀疑eps_time设得太大。这两张图比任何聚类评价指标都直观也是我每次调参必看的后悔药——参数错了图一眼就能暴露。6.2 参数网格扫描用聚类数曲线找合理阈值由于ST-DBScan调参是二维的逐个人工试非常低效。简单做法是固定min_pts扫描eps和eps_time的网格画出簇数量随参数变化的曲线寻找“平台区”——即参数在一定范围内变化、簇数量基本稳定的区域取该区域的中心值作为最终参数eps_grid [200, 300, 400, 500] time_grid [600, 1200, 1800] for et in time_grid: counts [] for e in eps_grid: lab st_dbscan(points, epse, eps_timeet, min_pts10) counts.append(lab.max()) plt.plot(eps_grid, counts, markero, labelfeps_time{et}s) plt.xlabel(eps (m)) plt.ylabel(cluster count) plt.legend() plt.show()网格扫描很费算力如果点集超过两万先用5.3节的索引版实现或者从全量数据里随机抽1万点来扫描参数定参后再全量运行。扫描时重点看曲线从“一坨大簇”到“簇数量骤增”的拐点往往就是合理参数的位置如果簇数量随参数剧烈震荡说明数据本身缺乏清晰的密度结构此时换个聚类思路比继续硬调参数更明智。作为一个跟时空数据打过不少交道的工程师我的习惯是先把可视化跑通再谈参数再复杂的算法在图上都藏不住问题。希望这篇笔记能让你在ST-DBScan上少走一些弯路如果照着跑通了你手头的数据集后面想从纯聚类往轨迹语义分析走顺着热点簇做时序统计就是很自然的下一步了。本文还有配套的精品资源点击获取
返回列表