使用DBSCAN聚类车辆经纬度时触发sklearn MemoryError问题排查
问题回顾
你在使用DBSCAN对车辆经纬度数据进行聚类、标记异常集群时,遇到了内存溢出问题:1000个CSV文件中,多数50KB的文件能正常生成可视化图,但处理那个4.3MB的大文件时,触发了sklearn.neighbors.ball_tree.BinaryTree.query_radius的MemoryError,报错栈如下:
Traceback (most recent call last):
File "E:/for_device_data.py", line 46, in
testPlt()
File "E:/for_device_data.py", line 18, in testPlt
.fit(np.radians(X)))
File "E:\Anaconda3\lib\site-packages\sklearn\cluster\dbscan_.py", line 351, in fit
**self.get_params())
File "E:\Anaconda3\lib\site-packages\sklearn\cluster\dbscan_.py", line 175, in dbscan
return_distance=False)
File "E:\Anaconda3\lib\site-packages\sklearn\neighbors\base.py", line 747, in radius_neighbors
for s in gen_even_slices(X.shape[0], n_jobs)
File "E:\Anaconda3\lib\site-packages\joblib\parallel.py", line 921, in call
if self.dispatch_one_batch(iterator):
File "E:\Anaconda3\lib\site-packages\joblib\parallel.py", line 759, in dispatch_one_batch
self._dispatch(tasks)
File "E:\Anaconda3\lib\site-packages\joblib\parallel.py", line 716, in _dispatch
job = self._backend.apply_async(batch, callback=cb)
File "E:\Anaconda3\lib\site-packages\joblib_parallel_backends.py", line 182, in apply_async
result = ImmediateResult(func)
File "E:\Anaconda3\lib\site-packages\joblib_parallel_backends.py", line 549, in init
self.results = batch()
File "E:\Anaconda3\lib\site-packages\joblib\parallel.py", line 225, in call
for func, args, kwargs in self.items]
File "E:\Anaconda3\lib\site-packages\joblib\parallel.py", line 225, in
for func, args, kwargs in self.items]
File "E:\Anaconda3\lib\site-packages\sklearn\neighbors\base.py", line 580, in _tree_query_radius_parallel_helper
return tree.query_radius(data, radius, return_distance)
File "sklearn\neighbors\binary_tree.pxi", line 1569, in sklearn.neighbors.ball_tree.BinaryTree.query_radius
File "sklearn\neighbors\binary_tree.pxi", line 1525, in sklearn.neighbors.ball_tree.BinaryTree.query_radius
MemoryError
问题根源
别担心,这个问题很常见,主要和以下几个因素有关:
- 样本量超出内存承载:4.3MB的CSV看似不大,但经纬度数据每条约几十字节,换算下来可能有10万+条记录。DBSCAN的BallTree实现会为每个样本存储邻域索引,当样本量过大时,内存占用会急剧飙升。
- 参数设置不合理:你用
np.radians(X)将经纬度转为弧度,但如果eps参数设置过大(比如对应几百米甚至几公里),每个样本的邻域会包含大量其他点,直接导致内存溢出。 - 未做数据预处理:车辆数据通常存在重复上报、无效经纬度(如全0值)的情况,这些冗余数据会进一步增加计算量和内存占用。
- 多线程内存开销:默认的
n_jobs=-1会启用所有CPU核心,多线程并行计算会额外占用更多内存,加剧溢出问题。
分步解决方案
1. 先清洗数据,减少样本量
这是最有效的第一步,直接砍掉冗余数据:
- 去重:去掉重复的位置记录(可按
device_id+upload_time或device_id+latitude+longitude去重)
import pandas as pd df = pd.read_csv("large_file.csv") # 按设备ID+经纬度去重,保留第一条记录 df_cleaned = df.drop_duplicates(subset=["device_id", "latitude", "longitude"], keep="first") # 过滤无效经纬度(确保在合理范围内) df_cleaned = df_cleaned[(df_cleaned["latitude"].abs() <= 90) & (df_cleaned["longitude"].abs() <= 180)]
- 降采样:如果数据时间密度极高(比如每秒上报一次),按固定时间间隔对每个设备的数据做降采样
# 转换时间格式 df_cleaned["upload_time"] = pd.to_datetime(df_cleaned["upload_time"]) # 按设备分组,每1分钟取一个点 df_downsampled = df_cleaned.groupby("device_id").resample("1min", on="upload_time").first().reset_index(drop=True) # 提取经纬度数组 X = df_downsampled[["latitude", "longitude"]].values
2. 调整DBSCAN参数与算法
- 合理设置
eps:因为用了弧度单位,eps要对应实际距离。比如100米对应的弧度是100/6371000 ≈ 1.57e-5(地球半径取6371000米),别设太大! - 更换算法:用
kd_tree替代默认的ball_tree,或者在内存紧张时用brute(虽然速度慢,但内存占用更低) - 关闭多线程:设置
n_jobs=1避免多线程的额外内存开销
from sklearn.cluster import DBSCAN import numpy as np # 计算对应100米的eps值 eps_meters = 100 eps_rad = eps_meters / 6371000 # 初始化DBSCAN,优化参数 db = DBSCAN( eps=eps_rad, min_samples=5, # 根据业务调整,比如至少5个点才形成集群 algorithm='kd_tree', n_jobs=1 ).fit(np.radians(X))
3. 分批次处理大文件
如果清洗后样本还是太多,可以按设备ID拆分数据,逐个处理后再合并结果:
min_samples = 5 all_results = [] # 按设备ID分组处理 for device_id, group in df_cleaned.groupby("device_id"): X_group = group[["latitude", "longitude"]].values if len(X_group) >= min_samples: # 对单个设备的数据做DBSCAN db_group = DBSCAN(eps=eps_rad, min_samples=min_samples).fit(np.radians(X_group)) group["cluster_label"] = db_group.labels_ else: # 样本太少直接标记为异常(-1) group["cluster_label"] = -1 all_results.append(group) # 合并所有结果 final_df = pd.concat(all_results)
4. 进一步优化内存使用
- 降低数据类型精度:把经纬度从
float64转为float32,直接减少一半内存占用
X = df_cleaned[["latitude", "longitude"]].astype("float32").values
- 处理文件时及时释放内存:处理1000个CSV时,不要一次性加载所有文件,处理完一个就删除变量并触发垃圾回收
import gc import glob csv_files = glob.glob("path/to/csvs/*.csv") for file_path in csv_files: df = pd.read_csv(file_path) # 这里写你的处理逻辑... # 处理完后释放内存 del df gc.collect()
验证建议
先拿大文件的前10000条数据测试参数,确认不会内存溢出后,再逐步扩展到整个文件。同时可以用df.info(memory_usage='deep')查看数据框的内存占用,调整预处理步骤。
内容的提问来源于stack exchange,提问作者user9270170

