Python 离群点检测算法 -- LOF

Python
22
0
0
2024-09-26

局部离群因子(LOF)是一种有效的无监督学习方法,它使用最近邻搜索来识别异常点,是一种基于密度的技术。在本章中,我将解释其动机和算法,并带领大家在 PyOD 中进行实践。基于密度的算法都对异常值很敏感,很容易出现过拟合,解决方法是训练多个模型,然后汇总得分。通过聚合多个模型,过拟合的几率会降低,预测精度也会提高。PyOD 模块提供了几种汇总结果的方法:平均法(Average)、最大值的最大值法(MOM)、最大值的平均值法(AOM)和平均值的最大值法(MOA)。在文中,我将只演示平均法。

离群点可以是全局性的,也可以是局部性的

离群点很容易感知,但用数学定义却不容易。相距甚远的数据点就是离群点。一次我在海滩岩石上看日落,一群海鸥站在我旁边的岩石上,有一只灰色的海鸥独自站在另一块岩石上。从我坐的地方看,那只灰色的海鸥是个异类。虽然海滩很长,海滩上还有很多其他的单身海鸥。从海滩的近处向远处看,那只灰色海鸥并不像离群的。

上述故事表明,离群点可能是全局离群点,也可能是局部离群点。数据点远离其附近的数据体时,会被视为离群值。图 (A) 展示了局部异常值和全局异常值。全局的算法可能只能捕捉到全局离群值。如果需要识别局部异常值,就需要一种能够关注局部邻域的算法。局部邻域中数据点的密度是关键,而 LOF 将不同密度的局部邻域纳入了识别局部离群值的考虑范围。

全局和局部离群值

LOF 如何工作?

LOF计算的是数据点相对于其相邻数据点的密度偏差,用于识别离群点。下图中a1点的密度远低于蓝色簇的点,显示蓝色聚类比绿色聚类更密集。当数据密度不同,LOF尤其有效。点a2是绿色聚类的局部离群点。LOF可以检测局部离群点。

局部离群因子(LOF)旨在调整不同局部密度的变化。它分为五个步骤,其中第四步涉及局部密度,第五步比较点的邻域密度与附近数据集群的密度。

  • Step 1: K-neighbors
  • Step 2: K-distance
  • Step 3: Reachability-distance (RD)
  • Step 4: Local reachability density (LRD)
  • Step 5: Local Outlier Factor of K-neighbor: LOF(k)

Reachability-distance

  • 首先,K-近邻是指 K-近邻之前的圆周区域。在图 (B) 中,O点的第一、第二和第三近邻分别是p1p2p3。虚线圆是 K=3 时 K 最近邻的面积。这一定义与 KNN 相同。
  • 其次,K-distance(o) 是点O到 K 最近邻居的距离。该距离可以用欧氏距离或曼哈顿距离测量。
  • 第三,点 O 的可达性距离是 K-distance*(o*)或任意点PO 之间距离的最大值。P2O的距离为d(P2,O),小于点P3O 的距离,即 K-distance(o)。因此P2的 RD 为 K-distance(o),即P3O 的距离。

步骤 1 和 2 只是帮助定义步骤 3 中的可达性距离。可及距离公式是为了减少所有靠近O 点的点P的*d(p,o)*的统计波动。

在步骤 4 中,LRD 是O点与其邻居的平均可达距离的倒数。LRD 值低意味着最近的数据体远离O 点。

最后,在步骤 5 中,LOF 是点O的 K 个邻居的平均 LRD 与其 LRD 之比,如下式所示。第一项是 K 个邻居的平均 LRD。LOF 是p的 LRD 与点p的 K 个近邻的 LRD 之比的平均值。第二项是点O 的 LRD。

如果O点不是离群点,则邻近点的平均 LRD 与 O 点的 LRD 大致相等,此时 LOF 几乎等于 1。另一方面,如果O点是一个离群点,则第一期的邻居平均 LRD 将高于第二期,此时 LOF 将大于 1。

在 LOF 中使用距离比可以确保考虑到不同的局部密度。无论聚类密度如何,聚类中数据点的 LOF 值通常都接近于 1。例如,在图(A)中,蓝色或绿色聚类中数据点的 LOF 值都接近 1,尽管这两个聚类的密度不同。

一般来说,如果 LOF> 1,则被视为离群点。该数据点与相邻数据点的距离比预期的要远。另一方面,如果一个数据点位于数据密集区域,它就不是离群点。它的 LOF 值将接近 1。

建模流程

可以通过选择一个阈值来区分离群点得分高的异常观测值和正常观测值。如果先验知识表明异常值的百分比不应超过1%,那么您可以选择一个相应的阈值。

对于模型的合理性,两组之间特征的描述统计数据(比如均值和标准差)非常关键。如果预期异常组的某一特征平均值应该高于正常组,而结果恰恰相反,就需要对该特征进行调查、修改或放弃,并重新建模。

第一步:建立 LOF 模型

我将使用 PyOD 的generate_data()函数生成百分之十的离群值。数据生成过程(DGP)将创建六个变量,并且模拟数据集中包含目标变量 Y,但无监督模型只使用 X 变量。目的是为了增加案例的趣味性,异常值的百分比设置为 5%,"contamination=0.05"。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from pyod.utils.data import generate_data
contamination = 0.05 # percentage of outliers
n_train = 500       # number of training points
n_test = 500        # number of testing points
n_features = 6      # number of features
X_train, X_test, y_train, y_test = generate_data(
    n_train=n_train, 
    n_test=n_test, 
    n_features= n_features, 
    contamination=contamination, 
    random_state=123)

X_train_pd = pd.DataFrame(X_train)
X_train_pd.head()

前两个变量绘制成散点图。散点图中的黄色点是百分之十的异常值。紫色点为正常观测值。

# Plot
plt.scatter(X_train_pd[0], X_train_pd[1], c=y_train, alpha=0.8)
plt.title('Scatter plot')
plt.xlabel('x0')
plt.ylabel('x1')
plt.show()

PyOD提供了统一的应用程序接口,使得使用PyOD来建立模型非常容易。接下来,我们声明并拟合模型,然后使用函数decision_functions()来生成训练数据和测试数据的离群值。

在这里,参数contamination=0.05表示污染率为5%。污染率是异常值的百分比。通常情况下,我们可能不知道离群值的百分比,因此可以根据先验知识来指定一个值。PyOD将污染率默认为10%。尽管该参数不影响离群值分数的计算,但PyOD使用它来推导离群值的阈值,并应用函数predict()来分配标签(1 或 0)。我已经创建了一个简短的函数count_stat(),用于显示预测值"1"和"0"的计数。threshold_语法显示了指定污染率的阈值。任何高于阈值的离群值都被视为离群值。

from pyod.models.lof import LOF
lof = LOF(contamination=0.05) 
lof.fit(X_train)

# Training data
y_train_scores = lof.decision_function(X_train)
y_train_pred = lof.predict(X_train)

# Test data
y_test_scores = lof.decision_function(X_test)
y_test_pred = lof.predict(X_test) # outlier labels (0 or 1)

def count_stat(vector):
    # Because it is '0' and '1', we can run a count statistic. 
    unique, counts = np.unique(vector, return_counts=True)
    return dict(zip(unique, counts))

print("The training data:", count_stat(y_train_pred))
print("The training data:", count_stat(y_test_pred))
# Threshold for the defined comtanimation rate
print("The threshold for the defined comtanimation rate:" , lof.threshold_)
The training data: {0: 477, 1: 23}
The training data: {0: 472, 1: 28}
The threshold for the defined comtanimation rate: 
1.321258785666126
lof.get_params()
{'algorithm': 'auto',
 'contamination': 0.05,
 'leaf_size': 30,
 'metric': 'minkowski',
 'metric_params': None,
 'n_jobs': 1,
 'n_neighbors': 20,
 'novelty': True,
 'p': 2}

第 2 步--为 LOF 模型确定一个合理的阈值

PyOD内置了一个名为threshold_的函数,可根据污染率计算训练数据的阈值。默认情况下,污染率为0.10,因此训练数据的阈值为1.2311。这意味着任何异常值大于1.2311的观测值都会被视为离群值。另一种确定阈值的方法是使用PCA离群点得分的直方图。我们可以根据业务需求来选择阈值。图©展示了得分直方图。我们可以采取更保守的方法,选择一个较高的阈值,这样离群值组中的离群值会更少,但期望更准确。

import matplotlib.pyplot as plt
plt.hist(y_train_scores, bins='auto')  # arguments are passed to np.histogram
plt.title("Histogram with 'auto' bins")
plt.xlabel('LOF outlier score')
plt.show()

第三步--展示 LOF 模型正常组和异常组的汇总统计量

在第 1 章中提到了两组特征之间的描述性统计数据(如均值和标准差)对于证明模型的合理性非常重要。我创造了一个名为 descriptive_stat_threshold() 的简短函数,用于展示基于阈值的正常组和离群组特征的大小和描述性统计。我将阈值简单地设置为 5%。您可以尝试不同的阈值来确定离群值组的合理大小。

threshold = lof.threshold_ # Or other value from the above histogram

def descriptive_stat_threshold(df,pred_score, threshold):
    # Let's see how many '0's and '1's.
    df = pd.DataFrame(df)
    df['Anomaly_Score'] = pred_score
    df['Group'] = np.where(df['Anomaly_Score']< threshold, 'Normal', 'Outlier')

    # Now let's show the summary statistics:
    cnt = df.groupby('Group')['Anomaly_Score'].count().reset_index().rename(columns={'Anomaly_Score':'Count'})
    cnt['Count %'] = (cnt['Count'] / cnt['Count'].sum()) * 100 # The count and count %
    stat = df.groupby('Group').mean().round(2).reset_index() # The avg.
    stat = cnt.merge(stat, left_on='Group',right_on='Group') # Put the count and the avg. together
    return (stat)

descriptive_stat_threshold(X_train,y_train_scores, threshold)

表格显示了正常组和异常组的特征,包括计数和计数百分比。异常分数是指平均异常分数。重要的是用特征名称标注特征,以便有效展示。这张表告诉我们一些重要的结果。

异常值组占总体的约5%。离群组的大小取决于设定的阈值,较高的阈值会缩小异常值。异常值组的平均异常值远高于正常组(1.77>1.07),分数不需要过多解释。从统计特征来看,离群组的特征均值小于正常组。离群组中特征的均值高低取决于业务应用,但重要的是,所有均值都应与领域知识保持一致。

我们可以利用 y_test 数据生成混淆矩阵来评估模型性能,该模型表现出色,成功识别了全部 25 个异常值。

Actual_pred = pd.DataFrame({'Actual': y_test, 'Anomaly_Score': y_test_scores})
Actual_pred['Pred'] = np.where(Actual_pred['Anomaly_Score']< threshold,0,1)
pd.crosstab(Actual_pred['Actual'],Actual_pred['Pred'])

通过聚合多个模型实现模型稳定性

许多离群值检测算法,特别是基于邻近度和分布的算法,对离群值非常敏感,容易出现过度拟合的问题。为了建立一个结果稳定的模型,可以使用一系列超参数训练模型,然后对得分进行汇总。这样一来,过拟合的风险将大大降低,预测精度也会得到提升。PyOD 模块提供了四种汇总结果的方法,要使用这些函数需要安装 pip install combo。一个好的方法就能得出汇总结果。

因为任何基于密度的模型都可能对训练数据进行过拟合,所以只依赖一种模型并不是一个好主意。我将生成 20 个 LOF 模型来作为一系列近邻的代表,然后对这些模型的平均预测结果进行汇总,作为最终的模型预测结果。

from pyod.models.combination import aom, moa, average, maximization
from pyod.utils.utility import standardizer
from pyod.models.lof import LOF
# Standardize data
X_train_norm, X_test_norm = standardizer(X_train, X_test)
# Test a range of k-neighbors from 10 to 200. There will be 20 models.
n_clf = 20
k_list = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 
 120, 130, 140, 150, 160, 170, 180, 190, 200]
# Just prepare data frames so we can store the model results
train_scores = np.zeros([X_train.shape[0], n_clf])
test_scores = np.zeros([X_test.shape[0], n_clf])
train_scores.shape
# Modeling
for i in range(n_clf):
    k = k_list[i]
    lof = LOF(n_neighbors=k) 
    lof.fit(X_train_norm)

    # Store the results in each column:
    train_scores[:, i] = lof.decision_scores_
    test_scores[:, i] = lof.decision_function(X_test_norm) 
# Decision scores have to be normalized before combination
train_scores_norm, test_scores_norm = standardizer(train_scores,test_scores)

在“y_by_average”下有20个模型对训练数据预测的平均值,这些数据存储在数据帧“train_scores_norm”中,并在图(D)中创建了该数据的直方图。

# Combination by average
# The test_scores_norm is 500 x 20. The "average" function will take the average of the 20 columns. The result "y_by_average" is a single column: 
y_by_average = average(train_scores_norm)
import matplotlib.pyplot as plt
plt.hist(y_by_average, bins='auto') # arguments are passed to np.histogram
plt.title("Combination by average")
plt.show()

训练数据的平均预测直方图

可以将阈值设为 0.5,并生成表 (D) 的汇总统计表,其中有 25 个数据点被确定为异常值。读者可以在表(C.3)中进行类似的解释。

descriptive_stat_threshold(X_train,y_train_by_average, .5)

LOF 算法总结

LOF 是一种基于密度的技术,使用最近邻搜索来识别异常点。一般来说,当 LOF> 1 时,被视为离群点,即数据点与邻居的距离比预期的要远。另一方面,如果一个数据点位于数据密集区域,则它的 LOF 值将接近 1。