ST-DBSCAN算法多维扩展实战:从时间维度到任意属性的Python实现
当大多数人看到ST-DBSCAN中的"T"时,第一反应就是"时间"(Temporal)。但如果你认为这个算法只能处理时空数据,那可能错过了它90%的应用潜力。实际上,ST-DBSCAN的核心思想是通过在传统DBSCAN基础上增加一个可自定义的维度约束,来实现更灵活的聚类分析。这个维度可以是温度、价格、用户评分,甚至是文本相似度——任何你能定义"距离"的维度。
1. ST-DBSCAN算法核心原理再思考
ST-DBSCAN算法在2007年由Derya Birant和Alp Kut提出时,确实是为了解决时空聚类问题。但仔细研究其论文会发现,作者特别强调:"额外的维度约束不一定非得是时间距离,可以是与二维空间无相关性的任何维度"。这一设计使得算法具备了惊人的扩展性。
传统DBSCAN的两个核心参数是:
eps:邻域半径min_samples:形成核心点所需的最小样本数
ST-DBSCAN在此基础上增加了:
- 空间距离阈值(
spatial_threshold) - 额外维度距离阈值(
temporal_threshold)
关键突破点在于retrieve_neighbors函数的实现逻辑。在原论文的时空场景中,这个函数同时满足两个条件:
- 空间距离 ≤
spatial_threshold - 时间差 ≤
temporal_threshold
但如果我们把时间维度替换为其他维度,算法依然成立。例如在电商场景中,我们可以定义:
- 空间距离:商品展示位置的接近程度
- 额外维度:商品价格的差异
python复制def retrieve_neighbors(index_center, df, spatial_threshold, price_threshold):
neighbors = []
center = df.loc[index_center]
# 价格维度过滤
min_price = center['price'] - price_threshold
max_price = center['price'] + price_threshold
df_filtered = df[(df['price'] >= min_price) & (df['price'] <= max_price)]
# 空间距离过滤
for index, point in df_filtered.iterrows():
if index != index_center:
spatial_dist = great_circle((center['lat'], center['lon']),
(point['lat'], point['lon'])).meters
if spatial_dist <= spatial_threshold:
neighbors.append(index)
return neighbors
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 多维度距离度量设计
要让ST-DBSCAN适用于不同场景,关键在于如何定义"额外维度"的距离。以下是几种常见场景的距离度量方法:
| 应用场景 | 维度类型 | 距离度量方法 | 阈值单位 |
|---|---|---|---|
| 零售货架分析 | 价格 | 绝对差值 | 货币单位 |
| 社交网络分析 | 用户活跃度 | 活跃度分数差值 | 分数差值 |
| 文本聚类 | 文本相似度 | 余弦相似度 | 0-1之间的相似度 |
| 工业生产 | 温度 | 温度差值 | 摄氏度 |
| 推荐系统 | 用户评分 | 评分差值 | 评分等级差值 |
对于类别型数据,我们可以使用相似度度量而非严格的距离。例如在用户兴趣聚类中:
python复制from sklearn.metrics import jaccard_score
def interest_similarity(user1, user2):
# 假设interests是用户兴趣标签的one-hot编码
return jaccard_score(user1['interests'], user2['interests'])
提示:当处理混合类型数据(数值+类别)时,可以考虑将不同维度的距离标准化后加权求和,形成综合距离度量。
3. 实战案例:电商商品聚类分析
让我们通过一个完整的电商商品聚类案例,展示如何将ST-DBSCAN应用于非时空场景。假设我们有以下数据特征:
- 商品在网页上的展示位置(x,y坐标)
- 商品价格
- 商品类别
数据准备:
python复制import pandas as pd
import numpy as np
from sklearn.preprocessing import MinMaxScaler
# 模拟电商商品数据
np.random.seed(42)
data = {
'item_id': range(100),
'pos_x': np.random.uniform(0, 1000, 100),
'pos_y': np.random.uniform(0, 500, 100),
'price': np.random.normal(100, 30, 100).clip(10, 200),
'category': np.random.choice(['电子', '服装', '家居', '食品'], 100)
}
df = pd.DataFrame(data)
# 对类别进行one-hot编码
df = pd.get_dummies(df, columns=['category'])
# 标准化位置和价格
scaler = MinMaxScaler()
df[['pos_x', 'pos_y', 'price']] = scaler.fit_transform(df[['pos_x', 'pos_y', 'price']])
改进的ST-DBSCAN实现:
python复制from sklearn.metrics.pairwise import euclidean_distances
def custom_retrieve_neighbors(index, df, spatial_thresh, price_thresh, cat_thresh):
center = df.loc[index]
neighbors = []
# 价格维度过滤
price_mask = (df['price'] >= center['price'] - price_thresh) & \
(df['price'] <= center['price'] + price_thresh)
# 类别相似度过滤 (使用Jaccard相似度)
cat_cols = [c for c in df.columns if c.startswith('category_')]
cat_sim = df[cat_cols].apply(lambda x: jaccard_score(center[cat_cols], x), axis=1)
cat_mask = cat_sim >= cat_thresh
# 综合过滤
candidates = df[price_mask & cat_mask].index
# 空间距离过滤
for idx in candidates:
if idx != index:
pos_dist = euclidean_distances(
[[center['pos_x'], center['pos_y']]],
[[df.loc[idx, 'pos_x'], df.loc[idx, 'pos_y']]]
)[0][0]
if pos_dist <= spatial_thresh:
neighbors.append(idx)
return neighbors
def custom_stdbscan(df, spatial_thresh, price_thresh, cat_thresh, min_neighbors):
cluster_label = 0
NOISE = -1
UNMARKED = 777777
stack = []
df['cluster'] = UNMARKED
for index, _ in df.iterrows():
if df.loc[index, 'cluster'] == UNMARKED:
neighbors = custom_retrieve_neighbors(
index, df, spatial_thresh, price_thresh, cat_thresh
)
if len(neighbors) < min_neighbors:
df.at[index, 'cluster'] = NOISE
else:
cluster_label += 1
df.at[index, 'cluster'] = cluster_label
for neigh_idx in neighbors:
df.at[neigh_idx, 'cluster'] = cluster_label
stack.append(neigh_idx)
while stack:
current_idx = stack.pop()
new_neighbors = custom_retrieve_neighbors(
current_idx, df, spatial_thresh, price_thresh, cat_thresh
)
if len(new_neighbors) >= min_neighbors:
for neigh_idx in new_neighbors:
if df.loc[neigh_idx, 'cluster'] in [UNMARKED, NOISE]:
df.at[neigh_idx, 'cluster'] = cluster_label
stack.append(neigh_idx)
return df
聚类分析与结果可视化:
python复制# 参数设置
params = {
'spatial_thresh': 0.2, # 标准化后的位置距离阈值
'price_thresh': 0.15, # 价格差异阈值
'cat_thresh': 0.5, # 类别相似度阈值
'min_neighbors': 3 # 形成核心点的最小邻居数
}
# 执行聚类
df_clustered = custom_stdbscan(df, **params)
# 结果可视化
import matplotlib.pyplot as plt
plt.figure(figsize=(12, 8))
for cluster in df_clustered['cluster'].unique():
if cluster == -1:
color = 'gray'
label = 'Noise'
else:
color = np.random.rand(3,)
label = f'Cluster {cluster}'
cluster_data = df_clustered[df_clustered['cluster'] == cluster]
plt.scatter(
cluster_data['pos_x'],
cluster_data['pos_y'],
c=[color] * len(cluster_data),
s=cluster_data['price'] * 100 + 20, # 用点大小表示价格
alpha=0.7,
label=label
)
plt.title('电商商品多维聚类结果')
plt.xlabel('页面X位置(标准化)')
plt.ylabel('页面Y位置(标准化)')
plt.legend()
plt.colorbar(label='价格')
plt.show()
这个案例展示了如何同时考虑商品在网页上的位置、价格和类别三个维度进行聚类。在实际业务中,这种聚类可以帮助我们:
- 发现价格敏感区域:某些页面区域集中展示低价商品
- 识别商品展示规律:特定类别商品在页面上的分布模式
- 优化页面布局:避免相似商品过度集中造成竞争
4. 工业传感器异常检测应用
工业物联网(IIoT)场景中,传感器数据通常包含多个维度的测量值。我们可以扩展ST-DBSCAN来检测异常设备。假设我们有如下传感器数据:
- 设备位置(工厂内坐标)
- 温度读数
- 振动幅度
- 工作时长
数据处理与聚类:
python复制# 模拟工业传感器数据
sensor_data = {
'device_id': [f'D{i}' for i in range(50)],
'pos_x': np.random.uniform(0, 100, 50),
'pos_y': np.random.uniform(0, 50, 50),
'temperature': np.concatenate([
np.random.normal(70, 5, 45),
np.random.normal(85, 3, 5) # 异常高温设备
]),
'vibration': np.concatenate([
np.random.normal(2, 0.5, 40),
np.random.normal(4, 0.3, 10) # 异常高振动设备
]),
'uptime': np.random.uniform(0, 30, 50) # 设备运行天数
}
sensor_df = pd.DataFrame(sensor_data)
# 标准化数据
scaler = MinMaxScaler()
sensor_df[['pos_x', 'pos_y', 'temperature', 'vibration', 'uptime']] = \
scaler.fit_transform(sensor_df[['pos_x', 'pos_y', 'temperature', 'vibration', 'uptime']])
# 定义多维度距离函数
def sensor_distance(device1, device2):
# 空间距离
spatial_dist = euclidean_distances(
[[device1['pos_x'], device1['pos_y']]],
[[device2['pos_x'], device2['pos_y']]]
)[0][0]
# 温度差异
temp_dist = abs(device1['temperature'] - device2['temperature'])
# 振动差异
vib_dist = abs(device1['vibration'] - device2['vibration'])
# 综合距离 (加权求和)
return 0.3 * spatial_dist + 0.4 * temp_dist + 0.3 * vib_dist
def sensor_retrieve_neighbors(index, df, threshold):
center = df.loc[index]
neighbors = []
for idx, row in df.iterrows():
if idx != index:
dist = sensor_distance(center, row)
if dist <= threshold:
neighbors.append(idx)
return neighbors
# 执行聚类 (参数需要根据实际数据调整)
sensor_df['cluster'] = -1 # 初始化为噪声
current_cluster = 0
for idx, _ in sensor_df.iterrows():
if sensor_df.loc[idx, 'cluster'] == -1: # 未分类点
neighbors = sensor_retrieve_neighbors(idx, sensor_df, threshold=0.25)
if len(neighbors) >= 3: # min_neighbors=3
sensor_df.loc[idx, 'cluster'] = current_cluster
for neigh_idx in neighbors:
sensor_df.loc[neigh_idx, 'cluster'] = current_cluster
# 扩展聚类
to_expand = neighbors.copy()
while to_expand:
current_idx = to_expand.pop()
new_neighbors = sensor_retrieve_neighbors(current_idx, sensor_df, threshold=0.25)
if len(new_neighbors) >= 3:
for neigh_idx in new_neighbors:
if sensor_df.loc[neigh_idx, 'cluster'] == -1:
sensor_df.loc[neigh_idx, 'cluster'] = current_cluster
to_expand.append(neigh_idx)
current_cluster += 1
# 标记异常点 (小聚类和噪声点)
cluster_sizes = sensor_df['cluster'].value_counts()
small_clusters = cluster_sizes[cluster_sizes < 3].index
sensor_df['is_anomaly'] = sensor_df['cluster'].isin(small_clusters) | (sensor_df['cluster'] == -1)
在这个应用中,我们通过多维度聚类成功识别出了:
- 高温异常设备
- 高振动异常设备
- 同时具有高温和高振动的潜在故障设备
5. 算法优化与参数调优
实现自定义维度的ST-DBSCAN后,如何优化算法性能和提高聚类质量成为关键问题。以下是几个实用技巧:
1. 参数选择策略:
对于多维度ST-DBSCAN,参数选择更为复杂。推荐采用以下方法:
- 空间/位置阈值:
- 计算所有样本间距离的k-距离图(k-distance graph)
- 选择"拐点"处对应的距离作为eps值
python复制from sklearn.neighbors import NearestNeighbors
import matplotlib.pyplot as plt
def find_optimal_eps(data, k=5):
neigh = NearestNeighbors(n_neighbors=k)
nbrs = neigh.fit(data[['pos_x', 'pos_y']])
distances, _ = nbrs.kneighbors(data[['pos_x', 'pos_y']])
distances = np.sort(distances[:, k-1], axis=0)
plt.plot(distances)
plt.xlabel('Points sorted by distance')
plt.ylabel(f'{k}-NN distance')
plt.show()
# 通常选择拐点处的距离值
return distances[-len(distances)//10]] # 示例:取后10%处的距离
optimal_spatial_eps = find_optimal_eps(df)
- 其他维度阈值:
- 数值型维度:使用数据标准差的百分比(如20-30%)
- 类别型维度:根据业务需求设定相似度阈值(通常0.5-0.7)
2. 维度加权优化:
当不同维度重要性不同时,可以采用加权距离:
python复制def weighted_distance(point1, point2, weights):
"""
point1, point2: 数据点
weights: 各维度权重字典,如{'spatial':0.4, 'price':0.3, 'category':0.3}
"""
spatial_dist = euclidean_distances(
[[point1['pos_x'], point1['pos_y']]],
[[point2['pos_x'], point2['pos_y']]]
)[0][0]
price_dist = abs(point1['price'] - point2['price'])
cat_cols = [c for c in point1.index if c.startswith('category_')]
cat_sim = jaccard_score(point1[cat_cols], point2[cat_cols])
cat_dist = 1 - cat_sim
return (weights['spatial'] * spatial_dist +
weights['price'] * price_dist +
weights['category'] * cat_dist)
3. 并行化加速:
对于大规模数据,可以优化邻居搜索过程:
python复制from joblib import Parallel, delayed
def parallel_retrieve_neighbors(indices, df, threshold, n_jobs=4):
def process_chunk(chunk):
neighbors = {}
for idx in chunk:
center = df.loc[idx]
# ... (距离计算逻辑)
neighbors[idx] = valid_neighbors
return neighbors
chunks = np.array_split(indices, n_jobs)
results = Parallel(n_jobs=n_jobs)(
delayed(process_chunk)(chunk) for chunk in chunks
)
# 合并结果
all_neighbors = {}
for res in results:
all_neighbors.update(res)
return all_neighbors
4. 动态参数调整:
在某些场景下,固定阈值可能不适用。可以实现基于局部数据特性的动态阈值:
python复制def dynamic_threshold_retrieve_neighbors(index, df, k=5):
center = df.loc[index]
# 计算k近邻的距离统计量
distances = []
for idx, row in df.iterrows():
if idx != index:
distances.append(calculate_distance(center, row))
distances = np.sort(distances)
dynamic_thresh = np.mean(distances[:k]) * 1.5 # 基于局部密度调整
neighbors = []
for idx, row in df.iterrows():
if idx != index:
if calculate_distance(center, row) <= dynamic_thresh:
neighbors.append(idx)
return neighbors
在实际项目中,我发现结合可视化分析工具评估聚类结果至关重要。下面是一个评估聚类质量的实用函数:
python复制def evaluate_clusters(df, cluster_col='cluster'):
# 1. 聚类大小分布
cluster_sizes = df[cluster_col].value_counts()
print("Cluster size distribution:")
print(cluster_sizes.describe())
# 2. 噪声点比例
noise_ratio = (df[cluster_col] == -1).mean()
print(f"\nNoise points ratio: {noise_ratio:.2%}")
# 3. 可视化各维度分布
numeric_cols = df.select_dtypes(include=np.number).columns
numeric_cols = [c for c in numeric_cols if c != cluster_col]
plt.figure(figsize=(15, 3*len(numeric_cols)))
for i, col in enumerate(numeric_cols, 1):
plt.subplot(len(numeric_cols), 1, i)
for cluster in df[cluster_col].unique():
if cluster == -1:
color = 'gray'
alpha = 0.3
else:
color = f'C{cluster}'
alpha = 0.7
cluster_data = df[df[cluster_col] == cluster]
plt.scatter(
cluster_data.index,
cluster_data[col],
c=color,
alpha=alpha,
label=f'Cluster {cluster}' if cluster != -1 else 'Noise'
)
plt.ylabel(col)
if i == 1:
plt.legend()
plt.tight_layout()
plt.show()
