DBSCAN 原理深度解析:从聚类算法到风控团伙识别的实战指南
本文面向有机器学习基础的风控或数据开发者,从原理层面拆解 DBSCAN 的每个细节,并结合真实风控场景讲解如何落地。读完你会明白:为什么风控团伙识别要选 DBSCAN,而不是更常见的 KMeans。
一、为什么风控需要聚类
风控系统通常有两层武器:
有监督模型(XGBoost、LightGBM):识别历史见过的欺诈模式,需要标注样本。
无监督聚类:发现从未见过的异常群体,不需要标注。
两者互补。有监督模型解决"这个人像不像历史黑名单",聚类解决"这群人有没有组团作案"。
真实的作弊团伙往往有一个共同特征:行为高度同质化。同一个团伙的成员,下注时间、频率、金额、胜率会异常相似。这种"密度"恰好是 DBSCAN 最擅长发现的东西。
二、DBSCAN 的核心思想
DBSCAN(Density-Based Spatial Clustering of Applications with Noise)是一个基于密度的聚类算法,核心思想只有一句话:
密度够高的地方自动形成簇,密度不够的孤立点标记为噪声。
对比 KMeans 理解会更直观:
| 对比维度 | KMeans | DBSCAN |
|---|---|---|
| 需要指定簇数 | 必须提前指定 K | 不需要 |
| 异常点处理 | 强行分配到某个簇 | 单独标记为 -1 |
| 簇的形状 | 只能识别球形 | 任意形状 |
| 适合风控场景 | 差(漏检异常) | 好(天然识别团伙) |
风控场景根本不知道有多少个团伙,而且团伙成员的行为分布不一定是球形的,所以 DBSCAN 是更自然的选择。
三、两个关键参数
DBSCAN 只有两个参数,但这两个参数决定了一切。
eps(邻域半径):以某个点为圆心、半径为 eps 的圆形区域,就是这个点的"邻域"。eps 越大,邻域越大,越容易把点连在一起。
min_samples(最少邻居数):一个点的邻域内至少要有多少个点,它才算"密度足够高"。
这两个参数共同决定了三种点的分类:
核心点:邻域内的点数 ≥ min_samples。处于密集区域,是簇的骨干,负责发起和扩展簇。
边界点:邻域内点数 < min_samples,但自己在某个核心点的邻域内。紧贴着簇的边缘,能被拉进簇,但不能继续扩展。
噪声点:既不是核心点,也不在任何核心点的邻域内,标记为 -1。在风控里,噪声点就是孤立的异常用户------有问题但没有同伙,需要单独排查。
四、簇扩展过程:一步步走一遍
用 7 个用户的风控例子完整演示,参数设定 eps=0.5,min_samples=3:
css
用户A:高频下注,胜率异常高
用户B:高频下注,胜率异常高(和A极其相似)
用户C:高频下注,胜率异常高(和A/B极其相似)
用户D:高频下注,胜率异常高(和A/B/C极其相似)
用户E:行为异常,但特征孤立,没有同伙
用户F:行为略异常,只和G相似
用户G:行为略异常,只和F相似
距离关系(谁在谁的 eps 邻域内):
css
A 的邻域:B、C、D
B 的邻域:A、C、D
C 的邻域:A、B、D
D 的邻域:A、B、C
E 的邻域:无
F 的邻域:G(只有1个)
G 的邻域:F(只有1个)
步骤一:选 A,计算邻域内有 B、C、D 共 3 个点,3 ≥ min_samples,A 是核心点。A 加入簇 #0,待处理队列 = B, C, D。
步骤二:取出 B,邻域内有 A、C、D,B 也是核心点。B 加入簇 #0,无新邻居入队。队列 = C, D。
步骤三:取出 C,邻域内有 A、B、D,C 是核心点。C 加入簇 #0。队列 = D。
步骤四:取出 D,邻域内有 A、B、C,D 是核心点。D 加入簇 #0。队列为空,簇 #0 扩展完毕。
步骤五:选 E,邻域内无人,0 < min_samples,E 不是核心点,也不在任何核心点邻域内,标记为噪声 -1。
步骤六:选 F,邻域内只有 G,1 < min_samples,F 不是核心点。F 不在任何核心点邻域内,标记为噪声 -1。
步骤七:选 G,邻域内只有 F,同理标记为噪声 -1。
最终结果:
less
簇 #0 = {A, B, C, D} → 作弊团伙,批量处置
噪声 = {E, F, G} → 孤立异常,人工排查
如果把 min_samples 改为 2,F 和 G 就会形成簇 #1,被识别为一个小团伙。这就是业务参数调整的直接效果:min_samples 的值代表"几个人以上才算团伙"的业务判断。
五、在风控中的完整实现
5.1 纯数值特征场景
python
import numpy as np
import pandas as pd
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import DBSCAN
# 模拟风控特征
df = pd.DataFrame({
"uid": list(range(1, 11)),
"bet_count_24h": [120, 118, 115, 5, 8, 6, 110, 4, 116, 200],
"win_rate_24h": [0.85, 0.86, 0.88, 0.30, 0.28, 0.31, 0.87, 0.29, 0.84, 0.95],
"device_count": [8, 9, 7, 1, 1, 2, 8, 1, 7, 15],
"ip_count": [12, 10, 11, 1, 2, 1, 12, 2, 10, 20],
})
features = ["bet_count_24h", "win_rate_24h", "device_count", "ip_count"]
X = df[features].values
# 标准化(消除量纲差异)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# 特征加权(下注行为权重高)
weights = np.array([2.0, 2.5, 1.5, 1.0])
X_weighted = X_scaled * weights
# DBSCAN
dbscan = DBSCAN(eps=0.8, min_samples=3)
df["cluster"] = dbscan.fit_predict(X_weighted)
# 结果分析
print("各团伙成员:")
for c in sorted(df["cluster"].unique()):
members = df[df["cluster"] == c]["uid"].tolist()
if c == -1:
print(f" 噪声(孤立异常): uid={members}")
else:
avg_win_rate = df[df["cluster"] == c]["win_rate_24h"].mean()
print(f" 团伙 #{c}: uid={members}, 平均胜率={avg_win_rate:.2f}")
5.2 混合类型特征场景(含注册类型等离散变量)
纯数值用欧氏距离,有离散变量时改用 Gower 距离:
python
import gower
df_mixed = pd.DataFrame({
"reg_type": ["手机", "邮箱", "手机", "手机", "第三方"],
"bet_count_24h": [120, 5, 115, 118, 8 ],
"win_rate_24h": [0.85, 0.30, 0.88, 0.86, 0.29 ],
"device_count": [8, 1, 7, 9, 1 ],
})
# Gower 距离内部自动处理不同类型,不需要预先 One-Hot
weights = np.array([0.5, 2.0, 2.5, 1.5]) # 注册类型权重低,行为特征权重高
dist_matrix = gower.gower_matrix(df_mixed, weight=weights)
dbscan = DBSCAN(eps=0.35, min_samples=2, metric="precomputed")
labels = dbscan.fit_predict(dist_matrix)
print("聚类结果:", labels)
# [0, -1, 0, 0, -1] → uid1/3/4 是同一团伙,uid2/5 是噪声
六、参数调优:eps 和 min_samples 怎么定
这是 DBSCAN 最难的地方,但有方法论可循。
6.1 min_samples 先定
三条规则取最大值:
python
n_features = X_scaled.shape[1]
n_samples = len(X_scaled)
rule1 = n_features + 1 # 维度法:特征数+1
rule2 = int(np.log(n_samples)) # 数据量法:ln(样本数)
rule3 = 3 # 业务法:3人以上才算团伙
min_samples = max(rule1, rule2, rule3)
print(f"建议 min_samples = {min_samples}")
6.2 eps 用 K 距离图确定
python
from sklearn.neighbors import NearestNeighbors
import matplotlib.pyplot as plt
nbrs = NearestNeighbors(n_neighbors=min_samples).fit(X_weighted)
distances, _ = nbrs.kneighbors(X_weighted)
k_distances = np.sort(distances[:, -1])[::-1]
plt.figure(figsize=(8, 4))
plt.plot(k_distances)
plt.xlabel("点(按距离降序排列)")
plt.ylabel(f"到第 {min_samples} 近邻的距离")
plt.title("K 距离图 ------ 找拐点确定 eps")
plt.savefig("k_distance.png", dpi=150)
# 自动找拐点
second_diff = np.diff(np.diff(k_distances))
optimal_eps = k_distances[np.argmax(np.abs(second_diff)) + 1]
print(f"K距离图建议 eps = {optimal_eps:.3f}")
6.3 验证参数合理性
python
labels = DBSCAN(eps=optimal_eps, min_samples=min_samples).fit_predict(X_weighted)
n_clusters = len(set(labels)) - (1 if -1 in labels else 0)
noise_rate = (labels == -1).mean()
print(f"簇数量:{n_clusters}")
print(f"噪声率:{noise_rate:.1%}")
# 风控场景合理范围判断
if noise_rate > 0.5:
print("噪声太多 → eps 偏小,适当增大")
elif noise_rate < 0.02:
print("噪声太少 → eps 偏大,点都被合并了")
elif n_clusters == 1:
print("只有一个簇 → eps 太大,缩小 eps")
else:
print("参数合理")
七、特征权重怎么确定
权重决定哪个特征对聚类结果影响更大,有三种方式:
方式一:XGBoost 重要性(有历史黑名单时首选)
python
from xgboost import XGBClassifier
X_train = df[features].values
y_train = df["is_fraud"].values # 历史黑名单标注
xgb = XGBClassifier(n_estimators=200, random_state=42)
xgb.fit(X_train, y_train)
importance = xgb.feature_importances_
weights_auto = importance / importance.sum() * len(features)
for f, w in zip(features, weights_auto):
print(f"{f}: {w:.3f}")
方式二:方差分析(统计区分度)
python
from scipy import stats
for f in features:
risk_vals = df[df["is_fraud"] == 1][f]
normal_vals = df[df["is_fraud"] == 0][f]
_, p_val = stats.ttest_ind(risk_vals, normal_vals)
pooled_std = np.sqrt((risk_vals.std()**2 + normal_vals.std()**2) / 2)
cohens_d = abs(risk_vals.mean() - normal_vals.mean()) / (pooled_std + 1e-8)
print(f"{f}: Cohen's d={cohens_d:.2f}, p={p_val:.4f}")
# Cohen's d 越大,该特征区分风险/正常用户的能力越强,权重应越高
方式三:业务经验(冷启动时)
python
# 风控经验权重参考
weights = {
"win_rate": 3.0, # 胜率异常:最强信号
"bet_count": 2.5, # 下注频率:强信号
"device_count": 2.0, # 多设备:强信号
"ip_count": 1.5, # 多IP:中等信号
"reg_type": 0.5, # 注册方式:弱信号
}
八、和你的风控链路集成
如果你已经有 XGBoost 风控模型,DBSCAN 是很好的补充层:
python
from sklearn.cluster import DBSCAN
from sklearn.preprocessing import StandardScaler
import numpy as np
def gang_detection(df, xgb_model, features,
xgb_threshold=0.6, eps=0.8, min_samples=3):
"""
风控团伙识别:XGBoost 初筛 + DBSCAN 团伙聚类
"""
# 第一层:XGBoost 打分,筛出高风险用户
scores = xgb_model.predict_proba(df[features])[:, 1]
df["risk_score"] = scores
high_risk = df[df["risk_score"] >= xgb_threshold].copy()
if len(high_risk) < min_samples:
return df
# 第二层:对高风险用户跑 DBSCAN,识别团伙
X = high_risk[features].values
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# 用 XGBoost 特征重要性作为权重
weights = xgb_model.feature_importances_
weights = weights / weights.sum() * len(features)
X_weighted = X_scaled * weights
dbscan = DBSCAN(eps=eps, min_samples=min_samples)
high_risk["gang_id"] = dbscan.fit_predict(X_weighted)
# 汇总结果
gang_summary = []
for gang_id in high_risk["gang_id"].unique():
members = high_risk[high_risk["gang_id"] == gang_id]
gang_summary.append({
"gang_id": int(gang_id),
"member_count": len(members),
"avg_risk_score": members["risk_score"].mean(),
"is_noise": gang_id == -1,
})
return high_risk, pd.DataFrame(gang_summary)
high_risk_df, summary = gang_detection(df, xgb_model, features)
print(summary)
这套组合的逻辑是:XGBoost 识别单个高风险用户,DBSCAN 在高风险用户中发现团伙结构。两层过滤大幅降低误判率,团伙成员批量处置,孤立异常用户人工复核。
九、常见问题
Q:噪声率多少算合理?
风控场景一般在 5%-20%。噪声率过高(>40%)说明 eps 太小,大部分用户都被孤立了;噪声率过低(<2%)说明 eps 太大,正常用户和风险用户都被合并进同一个簇。
Q:数据量很大(百万级)怎么处理?
不要对全量用户跑 DBSCAN,先用 XGBoost 过滤到高风险用户(通常只有 1%-5%),再对这个子集跑 DBSCAN,速度问题迎刃而解。
Q:特征含离散变量(如注册类型)怎么处理?
两种方案:一是 One-Hot 编码后用欧氏距离(简单但维度膨胀);二是用 Gower 距离(天然支持混合类型,数据量不大时首选)。
Q:DBSCAN 识别出的团伙一定是作弊团伙吗?
不一定。DBSCAN 只识别行为相似的群体,相似不等于作弊。必须结合业务规则二次验证:比如要求团伙成员同时满足"高胜率 + 多设备 + 短时间注册",才判定为作弊。
十、总结
DBSCAN 在风控团伙识别中的价值可以用三句话概括:
不需要知道有多少个团伙,算法自动发现。孤立的异常用户自动标记为噪声,不会被强行归入正常簇。和 XGBoost 组合使用,覆盖"单点风险"和"群体风险"两个维度,形成互补。
它不是万能的。对于特征工程要求高,参数调优需要业务经验,大数据量下计算慢。但在团伙识别这个细分场景里,DBSCAN 是目前最直接有效的工具。
参考资料