You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用DBSCAN聚类车辆经纬度时触发sklearn MemoryError问题排查

解决DBSCAN处理大经纬度CSV时的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

问题根源

别担心,这个问题很常见,主要和以下几个因素有关:

  1. 样本量超出内存承载:4.3MB的CSV看似不大,但经纬度数据每条约几十字节,换算下来可能有10万+条记录。DBSCAN的BallTree实现会为每个样本存储邻域索引,当样本量过大时,内存占用会急剧飙升。
  2. 参数设置不合理:你用np.radians(X)将经纬度转为弧度,但如果eps参数设置过大(比如对应几百米甚至几公里),每个样本的邻域会包含大量其他点,直接导致内存溢出。
  3. 未做数据预处理:车辆数据通常存在重复上报、无效经纬度(如全0值)的情况,这些冗余数据会进一步增加计算量和内存占用。
  4. 多线程内存开销:默认的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.09 10:42:29