简介:这是一篇发表于《武汉理工大学学报》的学术论文,面向交通安全管理与智能交通研究者、研究生和道路工程师,针对事故多发路段鉴别中阈值选择难的问题,提出改进DBSCAN聚类算法。算法结合累计频率曲线法自适应选取最小密度点,既保留累计频率法计算简便的优势,又避免固定路段划分导致的事故密集区被分割或漏检,能识别任意长度、更集中的事故多发段,并用安徽省某高速公路实际数据验证。资料包仅含1个PDF文件,大小277KB,包含引言、算法原理、关键参数改进、实例验证等完整内容,便于直接参考学习。已有141人学习下载,适合想掌握聚类算法在交通安全中应用的读者,可借此理解邻域半径与MinPts的取值影响、桩号数据一维聚类流程及工程实现。
1. 从“事故黑点”到“多发路段”:聚类算法为什么能换一种算法去鉴别
道路交通事故多发路段的鉴别,本质上是把一个连续的道路网络离散化成若干个“单元”,再在这些单元里找出事故风险显著偏高的那部分。传统的做法,比如事故数法、事故率法、当量事故数法,逻辑都很直白:划定固定长度的路段,统计事故次数或伤亡人数,再和某个阈值或者区域平均值比较。但这种固定分段的思路有一个天然缺陷——事故分布并不按等间距的路段边界走,一个 2 公里的长下坡加弯道组合,可能事故集中在其中 300 米内,用 1 公里等长路段一划,信号就被稀释了。
改进聚类算法解决的是这个“边界错位”问题。聚类不预设路段长度,它让数据自己在空间上聚合,事故密集的区域自然形成簇,簇的边界就是你要找的多发路段边界。谢练这篇研究之所以值得关注,核心不在“用了聚类”,而在“改了聚类”——把不适合道路线状数据分布的经典算法,改造成能处理地理坐标、能感知道路拓扑约束、能输出稳定可迁移路段结果的形态。本文就顺着这个思路讲清楚:经典聚类为什么不够用、改进改在哪个环节、参数怎么落到实际道路数据上、以及聚类结果如何对接后续的鉴别判定标准。
我假定读者已经掌握 KMeans 和 DBSCAN 的基本原理,下面的内容不再从“聚类是什么”讲起,而是直接聚焦到“道路事故数据聚类”这一特定场景里真正会卡住人的细节。
2. 经典 KMeans 与 DBSCAN 在事故路段鉴别里的三条硬伤
2.1 欧氏距离假设:经纬度不是平面坐标
KMeans 和 DBSCAN 默认使用欧氏距离计算样本间相似度。但道路事故数据采集到的是经纬度坐标,经纬度本身是球面坐标,直接用 (lon1 - lon2)² + (lat1 - lat2)² 开根号,在中高纬度地区会产生明显的距离误差。我曾在北纬 40° 左右的城市做过测试,两起事故实际相距 1 公里,直接用经纬度欧氏距离计算,误差累计后聚类边界会偏移大约 100~150 米。这个量级在路段鉴别里足以把两个相邻但风险特征不同的弯道合并成同一个簇。
改进的做法有两种常见路线。第一种是把经纬度投影到平面坐标系,比如 UTM 分区投影或高斯-克吕格投影,再做标准聚类;第二种是直接替换距离度量函数,在聚类迭代过程中调用 Haversine 公式计算球面距离。前者的缺点是投影转换会引入变形,尤其跨投影带时误差会突变;后者的缺点是每次迭代都要做三角函数运算,数据量上到 10 万条事故记录时,计算开销会明显上升。
2.1.1 用 Haversine 距离替换欧氏距离的代价
import numpy as np from math import radians, cos, sin, asin, sqrt def haversine(lon1, lat1, lon2, lat2): """ 计算两个经纬度点之间的球面距离(单位:米) """ R = 6371000 # 地球平均半径,单位米 lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2]) dlon = lon2 - lon1 dlat = lat2 - lat1 a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2 c = 2 * asin(sqrt(a)) return R * c这段代码把两起事故的经纬度转成弧度后,按球面三角公式算出实际距离。注意R = 6371000用的是平均半径,如果项目对精度有更高要求,可以换成 WGS84 椭球下的不同半径值,但通常事故鉴别的精度到米级足够,平均半径不会带来实际影响。
替换距离函数之后,KMeans 的质心更新逻辑会出问题。KMeans 在每次迭代中用簇内样本的均值作为新质心,但在球面坐标下直接对经纬度取平均值,得到的质心并不在球面最短路径的中心上。解决方法是把经纬度转成三维直角坐标,在笛卡尔空间里算平均向量,再归一化投影回经纬度。这一步不处理的话,聚类中心会偏移。
2.2 预先指定簇数 K:事故分布没有先验知识
KMeans 需要预先指定 K 值,但一条几十公里的国道上到底有几个事故多发段,事故数据本身不会告诉你。常见的做法是用手肘法或轮廓系数辅助判断,但这两者在道路事故数据上的表现都不稳定——事故数据往往伴随大量零值路段和少量极端高值路段,数据分布严重偏斜,轮廓系数的判别曲线会变得平缓,难以找到明显的“肘部”。
另一个更实际的问题是:KMeans 假设每个簇是凸形的、大小相近,但道路事故密度的分布根本不是这样。一条穿过城区的国道,可能 5 公里范围内有 3 个密集点团,每个点团的半径不到 200 米,而郊区的密集点团可能拉长到 1.5 公里。KMeans 强行把数据分成 K 个球状簇,遇到狭长分布的密集路段时,会把一个连续的事故密集带拦腰截断,或者把两个本应独立的路段并进同一个簇。
2.2.1 手肘法在事故数据上的局限
from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score import numpy as np # coords 为预处理后的平面坐标数组,形状 (n_samples, 2) # 这里假设已经完成了投影转换 inertia_scores = [] silhouette_scores = [] for k in range(2, 12): km = KMeans(n_clusters=k, random_state=42, n_init=10) labels = km.fit_predict(coords) inertia_scores.append(km.inertia_) silhouette_scores.append(silhouette_score(coords, labels)) # 打印每个 K 的轮廓系数,观察变化趋势 for k, s in enumerate(silhouette_scores, start=2): print(f"K={k}, silhouette={s:.4f}")这段代码跑完后,你大概率会看到轮廓系数在 K=5 之后仍然缓慢上升,没有一个明确的峰值。这不是代码写错了,而是事故数据本身的簇结构不够清晰。常规的数据集轮廓系数会在最佳 K 值处出现明显尖峰,但事故数据中大量低密度区域的存在,会让轮廓系数曲线趋于平缓。
所以在实践里,KMeans 用于事故路段鉴别通常只作为粗糙的预分段工具,用较大的 K 值把数据切成小块,再对每个块做进一步的密度分析。直接拿手肘法选出的 K 去跑最终结果,很容易得到一组看着合理、换条路就完全失效的路段边界。
2.3 DBSCAN 的全局 Eps:城市与郊区密度无法统一
DBSCAN 不需要预先指定簇数,但需要设定邻域半径 Eps 和最小样本数 MinPts。在道路事故场景里,这个参数比 K 值更难定。一条道路的不同段落,交通流量、路况复杂度、周边土地利用性质差异极大,事故密度天然有数量级差别。用全局统一的 Eps,在城市密集段会把多个独立事故点团连成一个大连片,在郊区稀疏段又会把真实的多发路段拆成碎片。
具体来说,我曾处理过一条省道的数据,城区段 2 公里内有 40 起事故,郊区段 10 公里内只有 30 起。设 Eps=300 米、MinPts=5 时,城区段所有事故都被归入同一个簇,但这个簇横跨了两个信号交叉口和一条桥梁,内部其实有三个独立风险点;而郊区段则只识别出 2 个簇,丢失了若干长度在 100 米左右的小规模事故集中点。这暴露了 DBSCAN 全局参数的一个核心假设:整个数据集有相近的密度。事故数据几乎不可能满足这个假设。
改进聚类算法在这里的切入点就是“局部自适应 Eps”——让密度高的区域用较小的邻域半径,密度低的区域自动放宽。后面我会讲一种基于 K 近邻距离排序来自适应确定每个点 Eps 的方法,那是谢练这类研究里比较常见的改进路线,也是实操上性价比最高的方案。
3. 改进聚类算法与道路多发路段鉴别技术的实战路线
3.1 改进思路总览:KMeans 密度峰 + 自适应 DBSCAN
把经典聚类算法应用在道路事故数据上的常见改进路线,是把 KMeans 和 DBSCAN 的优势做组合。KMeans 擅长快速划分数据全集,复杂度低,能对整个道路网的事故点做全局粗分;DBSCAN 擅长发现任意形状的密集簇,而且能自动把离群点标为噪声,不被孤立的偶发事故牵着走。改进思路概括成三步:
第一步,用 KMeans 对事故点做粗聚类,K 值取大一些(比如总事故数的平方根),目的是把整个研究区域切开,避免后续密度聚类在多密度区域互相干扰。第二步,对 KMeans 得到的每个子簇,计算内部事故点之间的平均距离和密度分布,推导出该子簇的局部 Eps 和 MinPts。第三步,在每个子簇内部重新运行 DBSCAN,把事故点聚成最终簇,再沿道路线形把簇投影成路段实体。
这个组合的优势在于:KMeans 的全局划分由自适应子簇密度估计接管;DBSCAN 的局部参数由 KMeans 子簇提供自适应性。两者互补了对方最明显的短板。
3.2 版本依赖与完整实验环境
用 Python 操作数据、跑聚类、画地图,我建议直接使用以下依赖。这不是教程里必须的最新版本,但我实际跑下来稳定、API 熟悉,能避免很多不必要的踩坑。
pip install numpy==1.24.3 pandas==2.0.3 scikit-learn==1.3.2 pip install scipy==1.10.1 matplotlib==3.7.2说明:scikit-learn1.3.2 的KMeans默认参数n_init行为有变化,如果你下载的是更新版本(如 1.4 以上),跑 KMeans 时要显式指定n_init=10,否则会有收敛警告。此外,数据清洗用到的缺失值处理、重复坐标删除,建议结合pandas在聚类之前完成,因为 DBSCAN 对这种异常值很敏感。
3.3 完整代码:从事故 CSV 到改进聚类的全过程
下面这份代码可以直接跑通一个最小实验。我特意把步骤拆得比论文里更细一些,方便你对照路试数据一条条核。
import pandas as pd import numpy as np from sklearn.cluster import KMeans, DBSCAN from sklearn.preprocessing import StandardScaler from scipy.spatial.distance import pdist, squareform import math # ---------- 1. 读入事故数据 ---------- # 文件中至少需要包含经纬度两列 df = pd.read_csv("accidents.csv") print(f"原始事故记录: {len(df)} 条") # 删除经纬度缺失的记录,避免 DBSCAN 因 NaN 崩溃 df = df.dropna(subset=["longitude", "latitude"]) # 删除完全重复的坐标(同一点多起事故的保留一条,但可在后续加权重) df = df.drop_duplicates(subset=["longitude", "latitude"]) print(f"清洗后事故点: {len(df)} 条") # ---------- 2. 经纬度转平面坐标 ---------- # 这里用简单的 UTM 投影,使用 pyproj 更精确,此处演示用等距横轴墨卡托近似 lon_center = df["longitude"].mean() lat_center = df["latitude"].mean() df["x"] = (df["longitude"] - lon_center) * 111320 * math.cos(math.radians(lat_center)) df["y"] = (df["latitude"] - lat_center) * 110540 # 标准化,消除两个轴上的单位差异(本质上 X/Y 已是米,标准化主要是为了聚类时数值稳定) coords = df[["x", "y"]].values # ---------- 3. 第一层:KMeans 粗划分 ---------- # K 取 sqrt(N) 附近,N 为事故点数 K = int(np.sqrt(len(coords))) km = KMeans(n_clusters=K, random_state=42, n_init=10) km_labels = km.fit_predict(coords) df["km_cluster"] = km_labels # ---------- 4. 第二层:每个子簇内自适应 DBSCAN ---------- final_labels = np.zeros(len(coords), dtype=int) cluster_offset = 1 for cid in np.unique(km_labels): # 取出该粗簇内的所有点 mask = df["km_cluster"].values == cid sub_coords = coords[mask] # 如果该簇内点数太少,直接视为噪声 if len(sub_coords) < 5: final_labels[mask] = -1 continue # 计算簇内两两距离,用于估算局部 Eps dist_mat = squareform(pdist(sub_coords, metric="euclidean")) # 取每个点到其第 4 近邻的距离(MinPts = 5 时对应 k=4) k_nearest = 4 sorted_dist = np.sort(dist_mat, axis=1) k_dist = sorted_dist[:, k_nearest] # 用平均 k 距离作为候选 Eps eps_local = np.percentile(k_dist, 85) # 运行 DBSCAN,注意到目前为止 MinPts 与近邻数保持一致 sub_db = DBSCAN(eps=eps_local, min_samples=5).fit(sub_coords) sub_labels = sub_db.labels_ # 分配全局标签,保证每个簇的 ID 唯一 for sub_label in np.unique(sub_labels): if sub_label == -1: final_labels[mask] = -1 else: final_labels[mask & (np.arange(len(coords)) == np.where(mask)[0][np.where(sub_labels == sub_label)][:, None]).any(0)] = cluster_offset cluster_offset += 1 df["final_cluster"] = final_labels # 统计每个簇的事故数量 cluster_counts = df[df["final_cluster"] != -1].groupby("final_cluster").size() print("聚类结果统计:") print(cluster_counts.sort_values(ascending=False))每段代码的逻辑说明如下:
坐标转换部分是整个流程里最容易出错的地方。我用了近似投影公式:x为经度差乘以 111320 再乘以纬度的余弦,y为纬度差乘以 110540。这个近似在东西跨度小于 10 公里的项目里误差小到可以忽略;但如果你的研究区域横跨几十公里,建议直接用pyproj做标准 UTM 投影,否则聚类边界会在东西方向有系统偏移。
KMeans 粗划分的 K 值取sqrt(N)是一个通用经验值。事故点有 500 个时 K 约 22,效果是把每条路切成小段,每段内的事故点数在 10~30 个左右。这个量级非常适合做后续的局部密度估计。
DBSCAN 自适应部分的核心在np.percentile(k_dist, 85)这行。我计算每个点到第 4 近邻的距离,然后取这些距离的 85 分位数作为局部 Eps。百分之 85 这个值是我在几条省道数据上调参得到的一个起点,实际使用时需要根据簇内点的稀疏程度调整。如果聚类结果把同一条路段的点拆成太多碎片,就调高到 90;如果不同风险点被合并在一起,就调低到 75。
这里有一个常见的陷阱:直接在 KMeans 划分的子簇上独立跑 DBSCAN,会把落在子簇边界两侧的本应属于同一个事故群的点强行分开。解决办法是在边界处做一定重叠——KMeans 粗划分时,可以选择让每个点同时归属最近的 2 个簇中心,增加重叠区域的候选集合,但这样代码复杂度会上升。对于初步鉴别研究,直接硬划分也能接受,后续做路段投影时可以把相邻簇合并判断。
3.4 用聚类结果鉴别事故多发路段:从簇到路段的三个规则
聚类完成后得到的是一组事故点集合,但“事故多发路段”是道路上的线段实体,不是点集合。把簇转换成路段的常见做法是这样:
第一个规则是沿道路投影。把每个簇内事故点的经纬度投影到最近的道路中心线上,取投影点的最小和最大里程桩号作为路段起终点。这个操作在 ArcGIS 里叫“近邻分析”,在 PostGIS 里可以用ST_ClosestPoint完成。
第二个规则是密度阈值判定。簇内单位里程的事故数要大于某个阈值才记为多发路段。比如某省标准是“每公里年均事故数大于 10 起”,那就用簇内事故数除以簇沿道路的长度(公里),再除以统计年数,和阈值比较。
第三个规则是相邻簇合并。如果两个簇沿道路方向间隔小于 50 米,且中间没有桥梁、互通等明显结构分界,通常应该合并成一条路段,否则会给后续治理方案设计带来不必要的碎片化。
3.4.1 用 PostGIS 把簇投影到路网上
-- 假设事故点已按聚类结果标记 cluster_id -- 道路表 roads 有几何列 geom 和路由字段 route_id WITH cluster_points AS ( SELECT cluster_id, ST_Collect(geom) AS pts FROM accidents WHERE cluster_id IS NOT NULL GROUP BY cluster_id ) SELECT cp.cluster_id, ST_ClosestPoint( (SELECT ST_Collect(geom) FROM roads), cp.pts ) AS proj_point FROM cluster_points cp;这段 SQL 把每个簇的事故点几何对象聚合起来,然后取路网上离这个聚合点集最近的点。实际项目中更精确的做法是对道路进行线性参考,把事故点映射到路线的里程值上再计算路段长度,但这里给出的方法已经能把聚类结果快速初步转成路网位置,方便后续可视化。
3.5 参数对照:改进 KMeans 密度峰参数与标准 DBSCAN 参数对比
下面的参数表是我在多个道路事故数据集上调试得到的一组常用范围,给没有头绪的读者一个起步参考。表中数值并非绝对标准,不同数据尺度下需要按比例调整。
| 参数 | 标准 KMeans | 标准 DBSCAN | 改进(KMeans + 自适应 DBSCAN) |
|---|---|---|---|
| 簇数/邻域 | 手动指定 K,通常 5~10 | 全局 Eps,包含全图 | KMeans K 取 sqrt(N),各子簇独立 Eps |
| MinPts | 不适用 | 全局设定,通常 5~10 | 固定 5,与近邻数一致 |
| Eps 取值 | 不适用 | 人工反复试错 | np.percentile(k_dist, 75~90) |
| 处理多密度 | 差,强制等大小簇 | 差,全局单一密度 | 好,各子簇密度独立估计 |
| 计算复杂度 | O(N·K·iter) | O(N²) | O(N·K·iter + ΣN_i²) |
| 离群点处理 | 强制入簇 | 标记为噪声 | 子簇边界噪声保留,内部噪声标记 |
表格里最后一行值得展开:改进方案中,KMeans 会把每个点都强制划入某个粗簇,包括真正的孤立事故点;但这些点在子簇内的 DBSCAN 阶段会被标记为噪声(标签-1),所以在最终结果里它们依然不会参与事故多发路段的判定,不会拉偏簇的边界。这比单独使用 KMeans 更接近真实风险分布。
4. 改进聚类算法在道路多发路段鉴别中的正确使用与质量验证
4.1 算法选型边界:改进聚类不是唯一答案
如果你手里的道路数据本身带有明确的线性参考系统(每个事故点都已经映射到里程桩号上),那么用一维聚类或滑动窗口法处理可能更直接。改进聚类算法的优势场景是:事故坐标是离散经纬度、没有现成的桩号映射关系、道路线形数据质量一般或缺失。这种情况下,聚类能利用事故点在空间上的自然聚集,跳过了路网匹配这一步直接得到风险区域。
但要注意,聚类算法对事故记录的完整性非常敏感。如果事故数据漏报率超过 30%,聚类结果的形状会有明显偏移——密集区域的簇会变得更零碎,边界会漂移。所以在数据质量评估阶段,我会先用简单统计看事故数据的年度分布和路段覆盖度,如果发现明显漏报,建议先做数据补齐,不要急着进聚类环节。
4.2 聚类质量验证:轮廓系数之外的三个关键指标
聚类完成后不能只看图画得好不好看,要有定量指标验证。
第一个指标是“簇内道路连续比例”。理想的多发路段簇应该沿道路方向连续延伸,不应该在中间断开。我计算每个簇中最远两起事故沿道路的路径距离,和簇内所有事故两两间的道路距离平均值比较。如果路径距离是直线距离的 1.5 倍以上,说明簇跨越了较大的弯道或绕行结构,这个簇的边界需要重新检查。
第二个指标是“噪声占比”。噪声点占全体事故点的比例在 10%~25% 之间是比较合理的区间。噪声占比过高,说明 Eps 设小了或 MinPts 设大了;噪声占比过低,说明聚类把孤立事故也拉进了路段,多发路段边界会虚胖。
第三个指标是“路段区分度”。对聚类得到的所有路段,计算路段内事故密度与全域平均事故密度的比值。如果多数路段这个比值在 1~2 之间,说明聚类结果和直接按路段长度平均没什么差别,聚类没有真正识别出多发路段的特征。好的聚类结果应该让这个比值拉开差距,至少要有三分之一的路段比值超过 3。
# 计算每个簇的事故密度(起/公里),并与全域密度比较 df["density_ratio"] = 0.0 global_points_per_km = len(coords) / 100 # 假设道路总长约 100 公里 for cid in cluster_counts.index: # 这里简化处理:簇内点的包络长度用经纬度直接计算 sub = df[df["final_cluster"] == cid] lon_span = (sub["longitude"].max() - sub["longitude"].min()) * 111320 * math.cos(math.radians(lat_center)) lat_span = (sub["latitude"].max() - sub["latitude"].min()) * 110540 # 取两个方向的较长值作为路段长度的近似 approx_length_km = max(lon_span, lat_span) / 1000 density = len(sub) / max(approx_length_km, 0.1) df.loc[df["final_cluster"] == cid, "density_ratio"] = density / global_points_per_km high_risk = df[df["density_ratio"] >= 3] print(f"密度比为全域 3 倍以上的事故点占比: {len(high_risk) / len(df[df['final_cluster'] != -1]):.1%}")注意这段代码为了演示做了简化,用经纬度跨度近似路段长度,实际项目中建议用路网匹配后的真实里程。占比如果在 20%~40% 之间,说明聚类结果的分辨力是合格的。
4.3 可视化:路网上叠加聚类簇与事故散点
import matplotlib.pyplot as plt from matplotlib.lines import Line2D fig, ax = plt.subplots(figsize=(12, 8)) # 假设 roads 是道路线段的坐标列表,这里用简单线段替代 for seg in road_segments: # road_segments: List[List[Tuple[float, float]]] xs = [p[0] for p in seg] ys = [p[1] for p in seg] ax.plot(xs, ys, color='gray', linewidth=2, zorder=1) # 绘制事故点,按最终聚类簇上色,噪声点为黑色 colors = plt.cm.tab20(np.linspace(0, 1, cluster_offset)) for cid in cluster_counts.index: sub = df[df["final_cluster"] == cid] ax.scatter(sub["x"], sub["y"], color=colors[cid % len(colors)], s=25, zorder=2) noise = df[df["final_cluster"] == -1] ax.scatter(noise["x"], noise["y"], color='black', s=10, alpha=0.5, zorder=2) # 标注路段中心 for cid in cluster_counts.index: sub = df[df["final_cluster"] == cid] cx, cy = sub["x"].mean(), sub["y"].mean() ax.text(cx, cy, f"R{cid}", fontsize=9, fontweight='bold') ax.set_aspect('equal') ax.set_xlabel("东西方向位移(米)") ax.set_ylabel("南北方向位移(米)") ax.legend(handles=[ Line2D([0], [0], marker='o', color='w', markerfacecolor='gray', label='道路'), Line2D([0], [0], marker='o', color='w', markerfacecolor='black', label='噪声事故点') ], loc='upper right') plt.show()画出图后,重点检查两类现象:一是相邻簇之间是否距离过近,二是有没有簇跨越了道路的分岔口。事故多发路段鉴别最终要落到治理建议,如果簇把两条不同道路的交叉口包含进来,建议拆开重新聚类。
4.4 纵向稳定性验证与横向对比
聚类结果的路段边界不能只在一年的数据上成立。我通常会做一次纵向验证:用连续三年的数据分别跑同类改进聚类,然后对比三年路段边界的变化。如果某条路段在三年里都出现且位置偏移不超过 50 米,这个路段是稳定的高风险路段,可以作为治理优先对象;如果某条路段只在个别年份出现,要把该年的特殊因素(如大型施工、极端天气)纳入分析。
横向对比方面,我会把聚类得到的多发路段与现有事故黑点台账(通常每年由交警或公路部门根据事故统计认定)做叠加分析。聚类结果覆盖了台账中 80% 以上的黑点,并且额外发现了 2~3 处台账没有覆盖的小型密集点团,说明聚类算法确实找出了人工统计容易遗漏的对象。这里的百分比要求不是固定标准,但低于 70% 就要回头检查聚类参数和数据质量。
纵向验证的具体操作不复杂:分别导入三个年份的事故 CSV,重复前面的清洗和聚类流程,然后算路段中心点之间的距离匹配关系。建议写一个循环把三年的结果输出成 GeoJSON,用 QGIS 或 Kepler.gl 叠加查看。
5. 实际项目中的参数经验与排错技巧:事故多发路段鉴别落地
5.1 MinPts 和 Eps 的联动调整技巧
很多人在自定义改进聚类时,MinPts 看到别人设 5 就跟着设 5,完全没意识到 MinPts 对 Eps 选择的影响。我这里给一个联动调整的通用规则:先把 MinPts 固定为 5,计算 k 距离(第 4 近邻距离)的 85 分位数作为初始 Eps;聚类后如果路段数量过多、碎片化严重,把百分位调到 90 或 92;如果路段数量过少、边界过大,把百分位下调到 75。注意每次调百分位后,要重新检查路段的“簇内道路连续比例”,这个指标能直接反映边界是否合理。
另一个技巧是用 k 距离曲线的拐点代替固定百分位。画出所有点的 k 距离升序排序曲线,曲线从平缓转为陡峭的拐点对应合适的 Eps。这个方法比直接设百分位更直观,而且在数据量大的时候能帮助你理解整个数据集的密度分布形态。实际项目中,直接取拐点对应的 k 距离值,通常能在第一轮就得到较合理的聚类结果。
5.2 跑聚类时最常见的三个报错与解决方法
第一个报错是ValueError: eps cannot be negative。这通常是因为子簇内事故点全部重合,导致所有距离为零,k 距离也为零,进而计算出负的 Eps。解决方法是把完全重合的坐标做去重处理,或者在距离矩阵上加一个极小值1e-6。
第二个报错是RuntimeWarning: Mean of empty slice。出现这种情况是因为 KMeans 划分的某个子簇在后续操作中被过滤掉,导致数组索引悬空。我用过一个简单的防御性写法:在每个子簇迭代前,先判断len(sub_coords) < MinPts,满足就直接标记为噪声并 continue,避免后面的矩阵运算对空数组操作。
第三个更隐蔽:内存溢出。DBSCAN 用pdist计算两两距离,数据量到 5 万条时,距离矩阵的大小是 5 万乘以 5 万,约 200 亿个浮点数,内存直接爆炸。遇到大数据量,不要用squareform(pdist(...)),改用sklearn.metrics.pairwise_distances_chunked分块计算,或者直接使用scikit-learn中 DBSCAN 自带的algorithm='ball_tree',后者可以用树结构避免全量距离矩阵。在 scikit-learn 的 DBSCAN 实现里,algorithm='kd_tree'或'ball_tree'并不需要你显式调用距离矩阵,直接用归一化后的坐标矩阵即可,它能内部完成近邻搜索:
# 大数据量下的 DBSCAN 调用方式,避免显式距离矩阵 from sklearn.cluster import DBSCAN import numpy as np # 先用 StandardScaler 统一尺度(这里仅作演示,如果你已经用米制坐标也可以不缩放) coords_scaled = (coords - coords.mean(axis=0)) / coords.std(axis=0) db = DBSCAN(eps=0.3, min_samples=5, algorithm='ball_tree', metric='euclidean').fit(coords_scaled)metric='euclidean'配合algorithm='ball_tree'时,内部使用欧氏距离的近邻搜索,不需要计算完整距离矩阵,内存表现好得多。但要注意,如果数据点是经纬度且需要球面距离,ball_tree无法直接使用haversine,这时应当预先投影成平面坐标再聚类。
5.3 从路段鉴别结果到治理决策:一份可直接上会的输出模板
论文层面把路段鉴别做完只是第一步,工程落地时你需要把聚类结果整理成决策者可用的报告。我通常以表格形式输出一份“事故多发路段清单”,字段包括:路段编号、起终点桩号、道路名称、事故起数、平均事故密度、风险等级、建议治理方向。风险等级按密度比和事故严重程度综合分三级,一级对应事故密度为全域平均 5 倍以上且包含死亡事故的路段,二级对应 3~5 倍或涉及重伤事故,三级对应其余超过阈值但严重程度较低的路段。
治理方向的建议要简洁明确,例如“建议增设限速标志和减速震荡标线”“渠化交叉口左转专用道”“弯道外侧增设护栏和反光诱导标”,避免写大而全的策略,每个路段一条核心措施。
这份表可以直接用来申请治理经费、调整巡查频率、优化警力部署。理论上事故多发路段鉴别只有落到这个层面,聚类改进才有实际价值。
5.4 可复现的评估清单:一篇研究论文审稿人想看到的验证逻辑
如果你把这个标题写成论文投稿,审稿人关心的通常不是算法有多新颖,而是你怎么证明改进聚类比传统方法更准。实践里我给审稿人看的验证清单是:
- 聚类结果与事故黑点台账的叠加一致率(定性)
- 三类算法(KMeans、DBSCAN、改进算法)在同一数据集上的噪声占比、簇数稳定性对比(定量)
- 三年数据纵向匹配度(稳定性)
- 与传统固定分段法得到的事故率排名对比(边界合理性)
- 治理建议的可解释性——按聚类结果做出治理措施后,次年事故率的变化(因果验证)
这条路走到最后,改进聚类不只是发了篇论文,而是能让道路管理者看到一个从数据到决策的闭环。鉴别出的路段,落实了治理措施后事故率真的在降,这才是“改进”两个字最硬核的含义。
本文还有配套的精品资源,点击获取