4、基于距离的异常检测:欧氏距离与马氏距离、KNN异常检测、LOF(局部异常因子)算法
各位同学,今天我们来聊一个在量化风控里非常实用的技术——基于距离的异常检测。
说实话,我在做风控模型初期,最头疼的就是怎么把那些「看起来正常、实际上有问题」的样本揪出来。规则过滤太死板,统计分布又不够灵活。后来我接触到距离检测的思路,才算是找到了突破口。
这一章,我会把欧氏距离、马氏距离、KNN异常检测和LOF算法串起来讲。你想想看,这些方法本质上都是在回答同一个问题:「这个样本,离正常群体有多远?」
4.1 距离度量:欧氏距离 vs 马氏距离
做异常检测,第一步就是定义「距离」。距离选错了,后面全白搭。
4.1.1 欧氏距离——最直观,但容易踩坑
欧氏距离就是我们在初中几何里学的直线距离。公式很简单:
d(x, y) = sqrt( (x1 - y1)² + (x2 - y2)² + ... + (xn - yn)² )
在风控场景里,我经常用它来算两个用户的行为向量有多像。比如用户的登录频次、交易金额、设备指纹这些特征。
但这里有个大坑:欧氏距离对特征的量纲非常敏感。
举个例子,假设我们有两个特征:
- 交易金额(范围 0 ~ 100000)
- 登录次数(范围 0 ~ 10)
如果不做标准化,交易金额的差异会完全主导距离计算。登录次数那点变化,根本起不到作用。我曾经在项目里吃过这个亏——模型跑出来全是金额异常,但真正的欺诈行为其实藏在登录模式里。
我的建议:用欧氏距离之前,一定要做标准化(Z-score 或 Min-Max)。这是基本功,别偷懒。
4.1.2 马氏距离——考虑相关性的进阶版
欧氏距离有个硬伤:它假设各个特征之间是独立的。但在风控数据里,特征之间往往高度相关。比如「交易金额」和「交易次数」通常是正相关的——金额高的用户,交易次数往往也多。
马氏距离就是来解决这个问题的。它的公式长这样:
D_M(x) = sqrt( (x - μ)ᵀ · Σ⁻¹ · (x - μ) )
其中 Σ 是协方差矩阵。说白了,马氏距离是在做两件事:
- 去相关:把特征之间的线性关系剥离掉
- 尺度归一:让每个维度的方差变得可比
我个人习惯在风控特征工程阶段,先用马氏距离做一轮粗筛。尤其是当特征维度在 10~20 之间时,效果比欧氏距离好很多。
注意:马氏距离要求协方差矩阵可逆。如果特征之间存在完全共线性(比如一个特征是另一个的线性组合),Σ 就不可逆了。这时候需要先做 PCA 降维或者剔除冗余特征。
4.2 KNN异常检测——邻居说了算
KNN 异常检测的思路非常朴素:如果一个样本的 k 个最近邻居都离它很远,那它大概率是异常的。
具体做法:
- 计算每个样本到它第 k 个最近邻居的距离
- 把这个距离作为异常得分
- 得分越高,越可能是异常点
代码实现也很简单:
from sklearn.neighbors import NearestNeighbors
import numpy as np
# 假设 X 是已经标准化后的特征矩阵
nbrs = NearestNeighbors(n_neighbors=5)
nbrs.fit(X)
# 计算每个样本到第5个最近邻居的距离
distances, indices = nbrs.kneighbors(X)
knn_scores = distances[:, -1] # 取第5近的距离
# 设定阈值,比如取前1%作为异常
threshold = np.percentile(knn_scores, 99)
anomalies = np.where(knn_scores > threshold)[0]
我在项目中遇到过:KNN 对 k 值的选择非常敏感。k 太小,容易把局部噪声当成异常;k 太大,又会漏掉真正的异常点。我一般会尝试 k=5, 10, 20,然后看异常样本的稳定性——如果某个样本在多个 k 值下都被标记为异常,那它基本就是实锤了。
KNN 的优点是简单、可解释性强。但缺点也很明显:计算复杂度高。如果样本量超过 10 万,每次都要算全量距离,性能扛不住。这时候可以用 KD-Tree 或 Ball-Tree 做加速。
4.3 LOF(局部异常因子)——密度说了算
KNN 有个问题:它只看距离,不看密度。举个例子:
- A 点离最近的邻居距离是 3
- B 点离最近的邻居距离也是 3
但 A 点周围样本稀疏,B 点周围样本密集。实际上 A 点可能是正常的(因为周围本来就没人),而 B 点才是异常的(因为周围人挤人,它却离群了)。
LOF 算法就是来解决这个问题的。它比较的是「局部密度」:
LOF = (样本周围邻居的平均密度) / (样本自身的局部密度)
如果 LOF 远大于 1,说明样本的密度明显低于邻居,那就是异常。
核心步骤:
- 计算每个样本的 k-距离(到第 k 近邻居的距离)
- 计算局部可达密度(基于可达距离)
- 比较局部密度,得到 LOF 值
代码示例:
from sklearn.neighbors import LocalOutlierFactor
# 训练 LOF 模型
lof = LocalOutlierFactor(n_neighbors=20, contamination=0.01)
y_pred = lof.fit_predict(X) # -1 表示异常,1 表示正常
# 获取 LOF 得分(负值越小越异常)
lof_scores = -lof.negative_outlier_factor_
避坑指南:我曾经在信用卡交易数据上用 LOF,发现它会把「高消费但频率正常」的用户误判为异常。原因是高消费群体的密度本身就低,LOF 容易把稀疏的正常区域也标记出来。后来我加了一层业务规则做二次过滤,才把误报率降下来。
4.4 三种方法的对比与选择
我把这三种方法的核心差异整理成了一张表,方便你对照:
| 方法 | 核心思想 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 欧氏距离 | 直线距离 | 简单、直观 | 忽略相关性、对量纲敏感 | 特征独立、已标准化的数据 |
| 马氏距离 | 考虑相关性的距离 | 去相关、尺度归一 | 协方差矩阵需可逆 | 特征相关性强的中低维数据 |
| KNN | 基于邻居距离 | 可解释性强 | 计算量大、对 k 敏感 | 小样本、需要解释异常原因 |
| LOF | 基于局部密度 | 能发现局部异常 | 对参数敏感、易误判稀疏区域 | 密度分布不均匀的数据 |
4.5 知识体系总览
为了让你更直观地理解这一章的知识结构,我画了一张图:
嗯,这张图把这一章的核心逻辑串起来了。从距离度量出发,到具体的检测算法,最后落到异常得分上。你可以在脑子里过一遍这个流程。
4.6 实战建议
最后,我结合自己的经验,给你几条实操建议:
- 先做数据清洗和标准化——这是所有距离方法的前提,别跳过
- 小样本用 KNN,大样本用 LOF——KNN 的计算量随样本量线性增长,LOF 相对更高效
- 不要只看一个指标——我习惯把欧氏距离、马氏距离、LOF 的得分都算出来,然后做加权融合
- 业务规则兜底——算法给出的异常,一定要经过业务人员确认。我曾经见过一个模型把「双十一大促的高消费用户」全标成了异常,闹了笑话
一句话总结:距离是工具,密度是视角,业务是底线。三者缺一不可。
好了,这一章的内容就到这里。下一章我们会聊基于密度的异常检测方法,到时候你会发现,LOF 其实只是密度方法的入门版。