DBSCAN 原理深度解析:从聚类算法到风控团伙识别的实战指南

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 是目前最直接有效的工具。


参考资料

相关推荐
Smilecoc1 小时前
决策树(四):决策树实战之鸢尾花分类
算法·决策树·分类
-Thinker1 小时前
【无标题】
java·开发语言·算法·图搜索
凡人叶枫1 小时前
Effective C++ 条款24:若所有参数皆须要类型转换,请为此采用 non-member 函数
linux·前端·c++·算法·嵌入式开发
洛水水1 小时前
【力扣100题】87.只出现一次的数字
数据结构·算法·leetcode
HZ·湘怡1 小时前
排序算法之希尔排序(2)--菜鸟先飞
数据结构·算法·排序算法·希尔排序
乐观勇敢坚强的老彭1 小时前
2026全国青少年信息素养大赛(Python小学组)复赛复习讲义
python·算法·数学建模
林间码客2 小时前
02数据挖掘:数据属性、类型与相似性度量
人工智能·算法·机器学习
阿标在干嘛2 小时前
从“拍脑袋”到“数据驱动”:政策平台的A/B测试实践
大数据·人工智能·算法·ab测试
实在智能RPA2 小时前
气象预警Agent等级判定算法:2026年AI驱动的概率集合预报与自动化闭环实践
人工智能·算法·ai·自动化