本文首发于 知乎专栏-数据科学与python小记html
原文连接 LOF离群因子检测算法及python3实现 - Suranyi的文章 - 知乎python
随着数据挖掘技术的快速发展,人们在关注数据总体趋势的同时,开始愈来愈关注那些明显偏离数据总体趋势的离群数据点,由于这些数据点每每蕴含着更加剧要的信息,而处理这些离群数据要依赖于相应的数据挖掘技术。 离群点挖掘的目的是有效的识别出数据集中的异常数据,而且挖掘出数据集中有意义的潜在信息。出现离群点的缘由各有不一样,其中主要有如下几种状况:ios
离群点检测具备很是强的实际意义,在相应的应用领域有着普遍前景。其中工程应用领域主要有如下几个方面:算法
目前,随着离群点检测技术的日渐成熟,在将来的发展中将会应用在更多的行业当中,而且能更好地为人类的决策提供指导做用。编程
离群点检测的一个目标是从看似杂乱无章的大量数据中挖掘有价值的信息,使这些数据更好地为咱们的平常生活所服务。可是现实生活中的数据每每具备成百上千的维度,而且数据量极大,这无疑给目前现有的离群点检测方法带来大难题。传统的离群点检测方法虽然在各自特定的应用领域里表现出很好效果,但在高维大数据集中却再也不适用。所以如何把离群点检测方法有效地应用于大数据、高维度数据,是目前离群点检测方法的首要目标之一。数组
分类学习算法在训练时有一个共同的基本假设:不一样类别的训练样例数目至关。若是不一样类别的训练样例数目稍有差异,一般影响不大,但若差异很大,则会对学习过程形成困扰。 ——周志华《机器学习》安全
2018 年 Mathorcup 数学建模竞赛 B 题——“品牌手机目标用户的精准营销”中就出现这样的问题。原题中,检测用户数 1万+,购买手机用户500+。若使用分类算法,那么分类器极可能会返回这样的一个算法:“全部用户都不会购买这款手机”,分类的正确率高达96%,但显然没有实际意义,由于它并不能预测出任何的正例。bash
这类问题称为“类别不平衡”,指不一样类别的训练样例数目差异很大的状况(上例中,购买的与没有购买用户数量差异大)。处理这类问题每每采用“欠采样”、“过采样”进行数据处理,但经过这样的方法,可能会损失原始数据中的信息。所以,从离群点的角度出发,将购买行为视为“异常”,进行离群点挖掘。网络
基于密度的离群点检测方法的关键步骤在于给每一个数据点都分配一个离散度,其主要思想是:针对给定的数据集,对其中的任意一个数据点,若是在其局部邻域内的点都很密集,那么认为此数据点为正常数据点,而离群点则是距离正常数据点最近邻的点都比较远的数据点。一般有阈值进行界定距离的远近。在基于密度的离群点检测方法中,最具备表明性的方法是局部离群因子检测方法 (Local Outlier Factor, LOF)。app
在众多的离群点检测方法中,LOF 方法是一种典型的基于密度的高精度离群点检测方法。在 LOF 方法中,经过给每一个数据点都分配一个依赖于邻域密度的离群因子 LOF,进而判断该数据点是否为离群点。若 LOF 1, 则该数据点为离群点;若 LOF 接近于 1,则该数据点为正常数据点。
设对于没有相同点的样本集合 ,假设共有
个检测样本,数据维数为
,对于
针对数据集 中的任意两个数据点
,定义以下几种经常使用距离度量方式
注 汉明距离使用在数据传输差错控制编码里面,用于度量信息不相同的位数。
取 ,易见
与
中有
位数字不相同,所以
与
的汉明距离为
。
对于数据处理,一种技巧是先对连续数据进行分组,化为分类变量(分组变量),对分类变量能够引入汉明距离进行度量。——沃兹基 · 硕德
设样本集 的协方差矩阵为
,记其逆矩阵为
若
可逆,对
作
分解(奇异值分解),获得:
若 不可逆,则使用广义逆矩阵
代替
,对其求彭罗斯广义逆,有:
则两个数据点 的马氏距离为:
注 马氏距离表示数据的协方差距离,利用 Cholesky 变换处理不一样维度之间的相关性和度量尺度变换的问题,是一种有效计算样本集之间的类似度的方法。
球面距离实际上是在欧式距离基础上进行转换获得的,并非一种独特的距离度量方式,在地理信息转换中常用,本文对此进行详细介绍。
符号 | 说明 | 符号 | 说明 | 符号 | 说明 |
---|---|---|---|---|---|
![]() |
球体球心 | ![]() |
![]() |
![]() |
![]() |
![]() |
球体球径 | ![]() |
![]() |
![]() |
![]() |
![]() |
待测距离点 | ![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
交点 | ![]() |
![]() |
设 A,B 两点的球面坐标为 。若该球体为地球,则 x,y 分别表明纬度和经度。(注:下文的
为 x 的余角,便于推导所使用的记号)
如图所示,链接,在
和
中计算:
因为 与
是异面直线,
是它们的公垂线,所成角度经度差为
,利用异面直线上两点距离公式:
在 中,由余弦定理:
因为此处的 表明纬度的补角,对其进行转换:
所以,点 A,B 的球面距离为:
此外还有 Chebyshev(切比雪夫)距离、Minkowski(闵科夫斯基)距离、绝对值距离、Lance & Williams 距离,具体问题具体分析,选择合适的度量方式。
统一使用 表示点
和点
之间的距离。根据定义,易知交换律成立:
定义 为点
的第
距离,
,知足以下条件
简言之:点 P 是距离 O 最近的第 k 个点
定义 设 N_{k}(O) 为点 O 的第 k 距离邻域,知足:
注 此处的邻域概念与国内高数教材略有不一样(具体的点,而非区间)。该集合中包含全部到点 O 距离小于点 O 第 k 邻域距离的点。易知有,如上图,点 O 的第 5 距离邻域为:
定义 点 P 到点 O 的第 k 可达距离定义为:
注 即点 到点
的第
可达距离至少是点
的第
距离。距离
点最近的
个点,它们到
的可达距离被认为是至关的,且都等于
定义 局部可达密度定义为:
注 表示点 的第
邻域内全部点到
的平都可达距离,位于第
邻域边界上的点即便个数大于
,也仍将该范围内点的个数计为
。若是
和周围邻域点是同一簇,那么可达距离越可能为较小的
,致使可达距离之和越小,局部可达密度越大。若是
和周围邻域点较远,那么可达距离可能会取较大值
,致使可达距离之和越大,局部可达密度越小。
部分资料这里使用
而不是
。笔者查阅大量资料及数据测试后认为,此处应为
,不然
会由于过多点在内部同一圆环上(如式13中的
位于同一圆环上)而致使
是一个很小的数,提示此处的密度低,可能为离群值。
此外,本文 2.1 开头指出“没有样本点重合”在这里也能获得解释:若是考虑重合样本点,可能会形成此处的可达密度为或下文的
为
形式,计算上带来困扰。
注 表示点 的邻域
内其余点的局部可达密度与点
的局部可达密度之比的平均数。若是这个比值越接近
,说明
的邻域点密度差很少,
可能和邻域同属一簇;若是这个比值小于
,说明
的密度高于其邻域点密度,
为密集点;若是这个比值大于
,说明
的密度小于其邻域点密度,
多是异常点。
此部分先介绍 sklearn 提供的函数,再介绍逐步编程实现方法。
在 python3 中,sklearn 模块提供了 LOF 离群检测算法
import pandas as pd
import matplotlib.pyplot as plt
复制代码
clf = LocalOutlierFactor(n_neighbors=20, algorithm='auto', contamination=0.1, n_jobs=-1)
复制代码
n_neighbors = 20
:即上文说起的 algorithm = 'auto'
:使用的求解算法,使用默认值便可contamination = 0.1
:范围为 (0, 0.5),表示样本中的异常点比例,默认为 0.1n_jobs = -1
:并行任务数,设置为-1表示使用全部CPU进行工做p = 2
:距离度量函数,默认使用欧式距离。(其余距离模型使用较少,这里不做介绍。具体参考官方文档)clf.fit(data)
复制代码
无监督学习,只须要传入训练数据data,传入的数据维度至少是 2 维
clf.kneighbors(data)
复制代码
做用:获取第 k 距离邻域内的每个点到中心点的距离,并按从小到大排序
返回 数组,[距离,样本索引]
-clf._decision_function(data)
复制代码
clf._decision_function
的输出方式更为灵活:若使用 clf._predict(data) 函数,则按照原先设置的 contamination 输出判断结果(按比例给出判断结果,异常点返回-1,非异常点返回1)localoutlierfactor(data, predict, k)
plot_lof(result,method)
lof(data, predict=None, k=5, method=1, plot = False)
def localoutlierfactor(data, predict, k):
from sklearn.neighbors import LocalOutlierFactor
clf = LocalOutlierFactor(n_neighbors=k + 1, algorithm='auto', contamination=0.1, n_jobs=-1)
clf.fit(data)
# 记录 k 邻域距离
predict['k distances'] = clf.kneighbors(predict)[0].max(axis=1)
# 记录 LOF 离群因子,作相反数处理
predict['local outlier factor'] = -clf._decision_function(predict.iloc[:, :-1])
return predict
def plot_lof(result, method):
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
plt.figure(figsize=(8, 4)).add_subplot(111)
plt.scatter(result[result['local outlier factor'] > method].index,
result[result['local outlier factor'] > method]['local outlier factor'], c='red', s=50,
marker='.', alpha=None,
label='离群点')
plt.scatter(result[result['local outlier factor'] <= method].index,
result[result['local outlier factor'] <= method]['local outlier factor'], c='black', s=50,
marker='.', alpha=None, label='正常点')
plt.hlines(method, -2, 2 + max(result.index), linestyles='--')
plt.xlim(-2, 2 + max(result.index))
plt.title('LOF局部离群点检测', fontsize=13)
plt.ylabel('局部离群因子', fontsize=15)
plt.legend()
plt.show()
def lof(data, predict=None, k=5, method=1, plot=False):
import pandas as pd
# 判断是否传入测试数据,若没有传入则测试数据赋值为训练数据
try:
if predict == None:
predict = data.copy()
except Exception:
pass
predict = pd.DataFrame(predict)
# 计算 LOF 离群因子
predict = localoutlierfactor(data, predict, k)
if plot == True:
plot_lof(predict, method)
# 根据阈值划分离群点与正常点
outliers = predict[predict['local outlier factor'] > method].sort_values(by='local outlier factor')
inliers = predict[predict['local outlier factor'] <= method].sort_values(by='local outlier factor')
return outliers, inliers
复制代码
测试数据:2017年全国大学生数学建模竞赛B题数据
测试数据有俩份文件,进行三次测试:没有输入测试样本、输入测试样本、测试样本与训练样本互换
测试 1 没有输入测试样本状况:任务密度
数据背景:众包任务价格制定中,地区任务的密度反映任务的密集程度,从而影响任务的订价,此处不考虑球面距离误差(即认为是同一个平面上的点),如今须要使用一个合理的指标刻画任务的密集程度。
import numpy as np
import pandas as pd
# 根据文件位置自行修改
posi = pd.read_excel(r'已结束项目任务数据.xls')
lon = np.array(posi["任务gps经度"][:]) # 经度
lat = np.array(posi["任务gps 纬度"][:]) # 纬度
A = list(zip(lat, lon)) # 按照纬度-经度匹配
# 获取任务密度,取第5邻域,阈值为2(LOF大于2认为是离群值)
outliers1, inliers1 = lof(A, k=5, method = 2)
复制代码
给定数据中共有835条数据,设置 LOF 阈值为 2,输出17个离群点信息:
绘制数据点检测状况分布以下图所示,其中蓝色表示任务分布状况,红色范围表示 LOF 值大小:
# 绘图程序
import matplotlib.pyplot as plt
for k in [3,5,10]:
plt.figure('k=%d'%k)
outliers1, inliers1 = lof(A, k=k, method = 2)
plt.scatter(np.array(A)[:,0],np.array(A)[:,1],s = 10,c='b',alpha = 0.5)
plt.scatter(outliers1[0],outliers1[1],s = 10+outliers1['local outlier factor']*100,c='r',alpha = 0.2)
plt.title('k=%d' % k)
复制代码
测试 2 有输入测试样本状况:任务对会员的密度
数据背景:众包任务价格制定中,地区任务的密度反映任务的密集程度、会员密度反映会员的密集程度。而任务对会员的密度则能够用于刻画任务点周围会员的密集程度,从而体现任务被完成的相对几率。此时训练样本为会员密度,测试样本为任务密度。
import numpy as np
import pandas as pd
posi = pd.read_excel(r'已结束项目任务数据.xls')
lon = np.array(posi["任务gps经度"][:]) # 经度
lat = np.array(posi["任务gps 纬度"][:]) # 纬度
A = list(zip(lat, lon)) # 按照纬度-经度匹配
posi = pd.read_excel(r'会员信息数据.xlsx')
lon = np.array(posi["会员位置(GPS)经度"][:]) # 经度
lat = np.array(posi["会员位置(GPS)纬度"][:]) # 纬度
B = list(zip(lat, lon)) # 按照纬度-经度匹配
# 获取任务对会员密度,取第5邻域,阈值为2(LOF大于2认为是离群值)
outliers2, inliers2 = lof(B, A, k=5, method=2)
复制代码
给定训练样本中共有1877条数据,测试样本中共有835条数据,设置 LOF 阈值为 2,输出34个离群点信息:
绘制数据点检测状况分布以下图所示,其中蓝色表示任务分布状况,绿色表示会员分布状况,红色范围表示 LOF 值大小。
# 绘图程序
import matplotlib.pyplot as plt
for k,v in ([1,5],[5,2]):
plt.figure('k=%d'%k)
outliers2, inliers2 = lof(B, A, k=k, method=v)
plt.scatter(np.array(A)[:,0],np.array(A)[:,1],s = 10,c='b',alpha = 0.5)
plt.scatter(np.array(B)[:,0],np.array(B)[:,1],s = 10,c='green',alpha = 0.3)
plt.scatter(outliers2[0],outliers2[1],s = 10+outliers2['local outlier factor']*100,c='r',alpha = 0.2)
plt.title('k = %d, method = %g' % (k,v))
复制代码
测试 3 测试样本与训练样本互换:会员对任务的密度
数据背景:众包任务价格制定中,地区任务的密度反映任务的密集程度、会员密度反映会员的密集程度。而任务对会员的密度则能够用于刻画会员周围任务的密集程度,从而体现会员能接到任务的相对几率。此时训练样本为任务密度,测试样本为会员密度。
import numpy as np
import pandas as pd
posi = pd.read_excel(r'已结束项目任务数据.xls')
lon = np.array(posi["任务gps经度"][:]) # 经度
lat = np.array(posi["任务gps 纬度"][:]) # 纬度
A = list(zip(lat, lon)) # 按照纬度-经度匹配
posi = pd.read_excel(r'会员信息数据.xlsx')
lon = np.array(posi["会员位置(GPS)经度"][:]) # 经度
lat = np.array(posi["会员位置(GPS)纬度"][:]) # 纬度
B = list(zip(lat, lon)) # 按照纬度-经度匹配
# 获取会员对任务密度,取第5邻域,阈值为5(LOF大于5认为是离群值)
outliers3, inliers3 = lof(A, B, k=5, method=5)
复制代码
给定训练样本中共有835条数据,测试样本中共有1877条数据,设置 LOF 阈值为 5,输出20个离群点信息:
绘制数据点检测状况分布以下图所示,其中蓝色表示会员分布状况,绿色表示任务分布状况,红色范围表示 LOF 值大小。
# 绘图程序
plt.figure('k=5')
outliers3, inliers3 = lof(A, B, k=5, method=5)
plt.scatter(np.array(B)[:, 0], np.array(B)[:, 1], s=10, c='b', alpha=0.5)
plt.scatter(np.array(A)[:, 0], np.array(A)[:, 1], s=10, c='green', alpha=0.3)
plt.scatter(outliers3[0], outliers3[1], s=10 + outliers3['local outlier factor'] * 20, c='r', alpha=0.2)
plt.title('k = 5, method = 5')
复制代码
将 method 设置为 0 就能输出每个点的 LOF 值,做为密度指标。
distances(A, B,model = 'euclidean')
def distances(A, B,model = 'euclidean'):
'''LOF中定义的距离,默认为欧式距离,也提供球面距离'''
import numpy as np
A = np.array(A); B = np.array(B)
if model == 'euclidean':
from scipy.spatial.distance import pdist, squareform
distance = squareform(pdist(np.vstack([A, B])))[:A.shape[0],A.shape[0]:]
if model == 'geo':
from geopy.distance import great_circle
distance = np.zeros(A.shape[0]*B.shape[0]).reshape(A.shape[0],B.shape[0])
for i in range(len(A)):
for j in range(len(B)):
distance[i,j] = great_circle(A[i], B[j]).kilometers
if distance.shape == (1,1):
return distance[0][0]
return distance
复制代码
k_distance(k, instance_A, instance_B, result, model)
def k_distance(k, instance_A, instance_B, result, model):
'''计算k距离邻域半径及邻域点'''
distance_all = distances(instance_B, instance_A, model)
# 对 instance_A 中的每个点进行遍历
for i,a in enumerate(instance_A):
distances = {}
distance = distance_all[:,i]
# 记录 instance_B 到 instance_A 每个点的距离,不重复记录
for j in range(distance.shape[0]):
if distance[j] in distances.keys():
if instance_B[j].tolist() in distances[distance[j]]:
pass
else:
distances[distance[j]].append(instance_B[j].tolist())
else:
distances[distance[j]] = [instance_B[j].tolist()]
# 距离排序
distances = sorted(distances.items())
if distances[0][0] == 0:
distances.remove(distances[0])
neighbours = [];k_sero = 0;k_dist = None
# 截取前 k 个点
for dist in distances:
k_sero += len(dist[1])
neighbours.extend(dist[1])
k_dist = dist[0]
if k_sero >= k:
break
# 输出信息
result.loc[str(a.tolist()),'k_dist'] = k_dist
result.loc[str(a.tolist()),'neighbours'] = str(neighbours)
return result
复制代码
local_reachability_density(k,instance_A,instance_B,result, model)
def local_reachability_density(k,instance_A,instance_B,result, model):
'''局部可达密度'''
result = k_distance(k, instance_A, instance_B, result, model)
# 对 instance_A 中的每个点进行遍历
for a in instance_A:
# 获取k_distance中获得的邻域点坐标,解析为点坐标(字符串 -> 数组 -> 点)
try:
(k_distance_value, neighbours) = result.loc[str(a.tolist())]['k_dist'].mean(),eval(result.loc[str(a.tolist())]['neighbours'])
except Exception:
(k_distance_value, neighbours) = result.loc[str(a.tolist())]['k_dist'].mean(), eval(result.loc[str(a.tolist())]['neighbours'].values[0])
# 计算局部可达距离
reachability_distances_array = [0]*len(neighbours)
for j, neighbour in enumerate(neighbours):
reachability_distances_array[j] = max([k_distance_value, distances([a], [neighbour],model)])
sum_reach_dist = sum(reachability_distances_array)
# 计算局部可达密度,并储存结果
result.loc[str(a.tolist()),'local_reachability_density'] = k / sum_reach_dist
return result
复制代码
k_distance(k, instance_A, instance_B, result, model)
def local_outlier_factor(k,instance_A,instance_B,model):
'''局部离群因子'''
result = local_reachability_density(k,instance_A,instance_B,pd.DataFrame(index=[str(i.tolist()) for i in instance_A]), model)
# 判断:若测试数据=样本数据
if np.all(instance_A == instance_B):
result_B = result
else:
result_B = local_reachability_density(k, instance_B, instance_B, k_distance(k, instance_B, instance_B, pd.DataFrame(index=[str(i.tolist()) for i in instance_B]), model), model)
for a in instance_A:
try:
(k_distance_value, neighbours, instance_lrd) = result.loc[str(a.tolist())]['k_dist'].mean(),np.array(eval(result.loc[str(a.tolist())]['neighbours'])),result.loc[str(a.tolist())]['local_reachability_density'].mean()
except Exception:
(k_distance_value, neighbours, instance_lrd) = result.loc[str(a.tolist())]['k_dist'].mean(), np.array(eval(result.loc[str(a.tolist())]['neighbours'].values[0])), result.loc[str(a.tolist())]['local_reachability_density'].mean()
finally:
lrd_ratios_array = [0]* len(neighbours)
for j,neighbour in enumerate(neighbours):
neighbour_lrd = result_B.loc[str(neighbour.tolist())]['local_reachability_density'].mean()
lrd_ratios_array[j] = neighbour_lrd / instance_lrd
result.loc[str(a.tolist()), 'local_outlier_factor'] = sum(lrd_ratios_array) / k
return result
复制代码
lof(k,instance_A,instance_B,k_means=False,$n_clusters$=False,k_means_pass=3,method=1,model = 'euclidean'
# 函数中的 k_means将在第4部分介绍
def lof(k, instance_A, instance_B, k_means=False, $n_clusters$=False, k_means_pass=3, method=1, model='euclidean'):
'''A做为定点,B做为动点'''
import numpy as np
instance_A = np.array(instance_A);
instance_B = np.array(instance_B)
if np.all(instance_A == instance_B):
if k_means == True:
if $n_clusters$ == True:
$n_clusters$ = elbow_method(instance_A, maxtest=10)
instance_A = kmeans(instance_A, $n_clusters$, k_means_pass)
instance_B = instance_A.copy()
result = local_outlier_factor(k, instance_A, instance_B, model)
outliers = result[result['local_outlier_factor'] > method].sort_values(by='local_outlier_factor', ascending=False)
inliers = result[result['local_outlier_factor'] <= method].sort_values(by='local_outlier_factor', ascending=True)
plot_lof(result, method) # 该函数见3.1.3
return outliers, inliers
复制代码
本文 3.1 中 sklearn 模块提供的 LOF 方法进行训练时会进行数据类型判断,若数据类型为list、tuple、numpy.array 则要求传入数据的维度至少是 2 维。实际上要筛选 1 维数据中的离群点,直接在坐标系中绘制出图像进行阈值选取判断也很方便。但此情形下若要使用 LOF 算法,能够为数据添加虚拟维度,并赋相同的值:
# data 是原先的1维数据,经过下面的方法转换为2维数据
data = list(zip(data, np.zeros_like(data)))
复制代码
此外,也能够经过将数据转化为 pandas.DataFrame 形式避免上述问题:
data = pd.DataFrame(data)
复制代码
LOF 计算结果对于多大的值定义为离群值没有明确的规定。在一个平稳数据集中,可能 1.1 已是一个异常值,而在另外一个具备强烈数据波动的数据集中,即便 LOF 值为 2 可能还是一个正常值。因为方法的局限性,数据集中的异常值界定可能存在差别,笔者认为可使用统计分布方法做为参考,再结合数据状况最终肯定阈值。
基于统计分布的阈值划分
将 LOF 异常值分数归一化到 [0, 1] 区间,运用统计方法进行划分下面提供使用箱型图进行界定的方法,根据异常输出状况参考选取。
box(data, legend=True)
def box(data, legend=True):
import matplotlib.pyplot as plt
import pandas as pd
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.style.use("ggplot")
plt.figure()
# 若是不是DataFrame格式,先进行转化
if type(data) != pd.core.frame.DataFrame:
data = pd.DataFrame(data)
p = data.boxplot(return_type='dict')
warming = pd.DataFrame()
y = p['fliers'][0].get_ydata()
y.sort()
for i in range(len(y)):
if legend == True:
plt.text(1, y[i] - 1, y[i], fontsize=10, color='black', ha='right')
if y[i] < data.mean()[0]:
form = '低'
else:
form = '高'
warming = warming.append(pd.Series([y[i], '偏' + form]).T, ignore_index=True)
print(warming)
plt.show()
复制代码
box 函数能够插入封装函数 lof 中,传入 data = predict['local outlier factor'] 实现;也能够先随机指定一个初始阈值,(输出的离群点、正常点分别命名为outliers, inliers)再输入:
box(outliers['local outlier factor'].tolist()+inliers['local outlier factor'].tolist(), legend=True)
复制代码
此时,交互控制台中输出状况以下左图所示,箱型图以下右图所示。输出状况提示咱们从数据分布的角度上,能够将 1.4 做为离群识别阈值,但实际上取 7 更为合适(从 2 到 7 间有明显的断层,而上文中设定为 5 是通过屡次试验后选取的数值)。
数据维度过大一方面会增大量纲的影响,另外一方面增大计算难度。此时直接使用距离度量的表达形式不合理,并有人为放大较为分散数据影响的风险。一种处理方式是采用马氏距离做为距离度量的方式(去量纲化)。另外一种处理方式,参考随机森林的决策思想,能够考虑在多个维度投影上使用 LOF 并将结果结合起来,以提升高维数据的检测质量。
集成学习
集成学习:经过构建并结合多个学习器来完成学习任务,一般能够得到比单一学习器更显著优越的泛化性能。 ——周志华《机器学习》
数据检测中进行使用的数据应该是有意义的数据,这就须要进行简单的特征筛选,不然不管多么“离群”的样本,可能也没有多大的实际意义。根据集成学习的思想,须要将数据按维度拆分,对于同类型的数据,这里假设你已经作好了规约处理(如位置坐标能够放在一块儿做为一个特征“距离”进行考虑),而且数据的维度大于 1,不然使用 4.1 中的数据变换及通常形式 LOF 便可处理。
投票表决模式认为每个维度的数据都是同等重要,单独为每一个维度数据设置 LOF 阈值并进行比对,样本的 LOF 值超过阈值则异常票数积 1 分,最终超过票数阈值的样本认为是离群样本。
localoutlierfactor(data, predict, k)
plot_lof(result,method)
ensemble_lof(data, predict=None, k=5, groups=[], method=1, vote_method = 'auto')
def localoutlierfactor(data, predict, k, group_str):
from sklearn.neighbors import LocalOutlierFactor
clf = LocalOutlierFactor(n_neighbors=k + 1, algorithm='auto', contamination=0.1, n_jobs=-1)
clf.fit(data)
# 记录 LOF 离群因子,作相反数处理
predict['local outlier factor %s' % group_str] = -clf._decision_function(predict.iloc[:, eval(group_str)])
return predict
def plot_lof(result, method, group_str):
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
plt.figure('local outlier factor %s' % group_str)
try:
plt.scatter(result[result > method].index,
result[result > method], c='red', s=50,
marker='.', alpha=None,
label='离群点')
except Exception:
pass
try:
plt.scatter(result[result <= method].index,
result[result <= method], c='black', s=50,
marker='.', alpha=None, label='正常点')
except Exception:
pass
plt.hlines(method, -2, 2 + max(result.index), linestyles='--')
plt.xlim(-2, 2 + max(result.index))
plt.title('LOF局部离群点检测', fontsize=13)
plt.ylabel('局部离群因子', fontsize=15)
plt.legend()
plt.show()
def ensemble_lof(data, predict=None, k=5, groups=[], method=1, vote_method = 'auto'):
import pandas as pd
import numpy as np
# 判断是否传入测试数据,若没有传入则测试数据赋值为训练数据
try:
if predict == None:
predict = data.copy()
except Exception:
pass
data = pd.DataFrame(data); predict = pd.DataFrame(predict)
# 数据标签分组,默认独立自成一组
for i in range(data.shape[1]):
if i not in pd.DataFrame(groups).values:
groups += [[i]]
# 扩充阈值列表
if type(method) != list:
method = [method]
method += [1] * (len(groups) - 1)
else:
method += [1] * (len(groups) - len(method))
vote = np.zeros(len(predict))
# 计算LOF离群因子并根据阈值进行票数统计
for i in range(len(groups)):
predict = localoutlierfactor(pd.DataFrame(data).iloc[:, groups[i]], predict, k, str(groups[i]))
plot_lof(predict.iloc[:, -1], method[i], str(groups[i]))
vote += predict.iloc[:, -1] > method[i]
# 根据票数阈值划分离群点与正常点
predict['vote'] = vote
if vote_method == 'auto':
vote_method = len(groups)/2
outliers = predict[vote > vote_method].sort_values(by='vote')
inliers = predict[vote <= vote_method].sort_values(by='vote')
return outliers, inliers
复制代码
测试 4 仍然使用测试3的状况进行分析,此时将经度、纬度设置为独立的特征,分别对两个维度数据进行识别(尽管单独的纬度、经度数据没有太大的实际意义)
import numpy as np
import pandas as pd
posi = pd.read_excel(r'已结束项目任务数据.xls')
lon = np.array(posi["任务gps经度"][:]) # 经度
lat = np.array(posi["任务gps 纬度"][:]) # 纬度
A = list(zip(lat, lon)) # 按照纬度-经度匹配
posi = pd.read_excel(r'会员信息数据.xlsx')
lon = np.array(posi["会员位置(GPS)经度"][:]) # 经度
lat = np.array(posi["会员位置(GPS)纬度"][:]) # 纬度
B = list(zip(lat, lon)) # 按照纬度-经度匹配
# 获取会员对任务密度,取第5邻域,阈值分别为 1.5,2,得票数超过 1 的认为是异常点
outliers4, inliers4 = ensemble_lof(A, B, k=5, method=[1.5,2], vote_method = 1)
# 绘图程序
plt.figure('投票集成 LOF 模式')
plt.scatter(np.array(B)[:, 0], np.array(B)[:, 1], s=10, c='b', alpha=0.5)
plt.scatter(np.array(A)[:, 0], np.array(A)[:, 1], s=10, c='green', alpha=0.3)
plt.scatter(outliers4[0], outliers4[1], s=10 + 1000, c='r', alpha=0.2)
plt.title('k = 5, method = [1.5, 2]')
复制代码
异常分数加权模式则是对各维度数据的 LOF 值进行加权,获取最终的 LOF 得分做为总体数据的 LOF 得分。权重能够认为是特征的重要程度,也能够认为是数据分布的相对离散程度,若视为后面一种情形,能够根据熵权法进行设定,关于熵权法的介绍详见笔者另外一篇博文。
式中 表示第
个数据的第
维度 LOF 异常分数值。
localoutlierfactor(data, predict, k)
plot_lof(result,method)
ensemble_lof(data, predict=None, k=5, groups=[], method=2, weight=1)
def localoutlierfactor(data, predict, k, group_str):
from sklearn.neighbors import LocalOutlierFactor
clf = LocalOutlierFactor(n_neighbors=k + 1, algorithm='auto', contamination=0.1, n_jobs=-1)
clf.fit(data)
# 记录 LOF 离群因子,作相反数处理
predict['local outlier factor %s' % group_str] = -clf._decision_function(predict.iloc[:, eval(group_str)])
return predict
def plot_lof(result, method):
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
plt.scatter(result[result > method].index,
result[result > method], c='red', s=50,
marker='.', alpha=None,
label='离群点')
plt.scatter(result[result <= method].index,
result[result <= method], c='black', s=50,
marker='.', alpha=None, label='正常点')
plt.hlines(method, -2, 2 + max(result.index), linestyles='--')
plt.xlim(-2, 2 + max(result.index))
plt.title('LOF局部离群点检测', fontsize=13)
plt.ylabel('局部离群因子', fontsize=15)
plt.legend()
plt.show()
def ensemble_lof(data, predict=None, k=5, groups=[], method='auto', weight=1):
import pandas as pd
# 判断是否传入测试数据,若没有传入则测试数据赋值为训练数据
try:
if predict == None:
predict = data
except Exception:
pass
data = pd.DataFrame(data);
predict = pd.DataFrame(predict)
# 数据标签分组,默认独立自成一组
for i in range(data.shape[1]):
if i not in pd.DataFrame(groups).values:
groups += [[i]]
# 扩充权值列表
if type(weight) != list:
weight = [weight]
weight += [1] * (len(groups) - 1)
else:
weight += [1] * (len(groups) - len(weight))
predict['local outlier factor'] = 0
# 计算LOF离群因子并根据特征权重计算加权LOF得分
for i in range(len(groups)):
predict = localoutlierfactor(pd.DataFrame(data).iloc[:, groups[i]], predict, k, str(groups[i]))
predict['local outlier factor'] += predict.iloc[:, -1] * weight[i]
if method == 'auto':
method = sum(weight)
plot_lof(predict['local outlier factor'], method)
# 根据离群阈值划分离群点与正常点
outliers = predict[predict['local outlier factor'] > method].sort_values(by='local outlier factor')
inliers = predict[predict['local outlier factor'] <= method].sort_values(by='local outlier factor')
return outliers, inliers
复制代码
测试 5 仍然使用测试3的状况进行分析,此时将经度、纬度设置为独立的特征,分别对两个维度数据进行识别(尽管单独的纬度、经度数据彷佛没有太大的实际意义)
import numpy as np
import pandas as pd
posi = pd.read_excel(r'已结束项目任务数据.xls')
lon = np.array(posi["任务gps经度"][:]) # 经度
lat = np.array(posi["任务gps 纬度"][:]) # 纬度
A = list(zip(lat, lon)) # 按照纬度-经度匹配
posi = pd.read_excel(r'会员信息数据.xlsx')
lon = np.array(posi["会员位置(GPS)经度"][:]) # 经度
lat = np.array(posi["会员位置(GPS)纬度"][:]) # 纬度
B = list(zip(lat, lon)) # 按照纬度-经度匹配
# 获取会员对任务密度,取第5邻域,阈值为 100,权重分别为5,1
outliers5, inliers5 = ensemble_lof(A, B, k=5, method=100,weight = [5,1])
# 绘图程序
plt.figure('LOF 异常分数加权模式')
plt.scatter(np.array(B)[:, 0], np.array(B)[:, 1], s=10, c='b', alpha=0.5)
plt.scatter(np.array(A)[:, 0], np.array(A)[:, 1], s=10, c='green', alpha=0.3)
plt.scatter(outliers5[0], outliers5[1], s=10 + outliers5['local outlier factor'], c='r', alpha=0.2)
plt.title('k = 5, method = 100')
复制代码
混合模式适用于数据中有些特征同等重要,有些特征有重要性区别的状况,即对 4.3.一、4.3.2 情形综合进行考虑。同等重要的数据将使用投票表决模式,重要程度不一样的数据使用加权模式并根据阈值转换为投票表决模式。程序上只需将两部分混合使用便可,本文在此不作展现。
LOF 算法在检测离群点的过程当中,遍历整个数据集以计算每一个点的 LOF 值,使得算法运算速度慢。同时,因为数据正常点的数量通常远远多于离群点的数量,而 LOF 方法经过比较全部数据点的 LOF 值判断离群程度,产生了大量不必的计算。所以,经过对原始数据进行修剪能够有效提升 LOF 方法的计算效率。此外,实践过程当中也发现经过数据集修剪后,能够大幅度减小数据误判为离群点的概率。这种基于聚类修剪得离群点检测方法称为 CLOF (Cluster-Based Local Outlier Factor) 算法。
基于 K-Means 的 CLOF 算法
在应用 LOF 算法前,先用 K-Means 聚类算法,将原始数据聚成 簇。对其中的每一簇,计算簇的中心
,求出该簇中全部点到该中心的平均距离并记为该簇的半径
。对该类中全部点,若该点到簇中心的距离大于等于
则将其放入“离群点候选集”
,最后对
中的数据使用 LOF 算法计算离群因子。
设第 个簇中的点的个数为
,点集为
中心和半径的计算公式以下:
如何肯定最佳的 —— 肘部法则
K-Means算法经过指定聚类簇数及随机生成聚类中心,对最靠近他们的对象进行迭代归类,逐次更新各聚类中心的值,直到最好的聚类效果(代价函数值最小)。
对于的选取将直接影响算法的聚类效果。肘部法则将不一样的
值的成本函数值刻画出来,随着
增大,每一个簇包含的样本数会减小,样本离其中心更接近,代价函数会减少。但随着
继续增大,代价函数的改善程度不断降低(在图像中,代价函数曲线趋于平稳)。
值增大过程当中,代价函数改善程度最大的位置对应的
就是肘部,使用此
通常能够取得不错的效果。但肘部法则的使用仅仅是从代价水平进行考虑,有时候还需结合实际考虑。
因为离群值样本数量通常较少,若是聚类出来的簇中样本量太少(如 1-4 个,但其余簇有成百上千个样本),则这种聚类簇不该进行修剪。
定义代价函数:
elbow_method(data,maxtest = 11)
def elbow_method(data,maxtest = 11):
'''使用肘部法则肯定$n_clusters$值'''
from sklearn.cluster import KMeans
from scipy.spatial.distance import cdist
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
ax = plt.figure(figsize=(8,4)).add_subplot(111)
N_test = range(1, maxtest)
# 代价函数值列表
meandistortions = []
for $n_clusters$ in N_test:
model = KMeans($n_clusters$=$n_clusters$).fit(data)
# 计算代价函数值
meandistortions.append(sum(np.min(cdist(data, model.cluster_centers_, 'euclidean'), axis=1)) / len(data))
plt.plot(N_test, meandistortions, 'bx-',alpha = 0.4)
plt.xlabel('k')
plt.ylabel('代价函数',fontsize= 12)
plt.title('用肘部法则来肯定最佳的$n_clusters$值',fontsize= 12)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.set_xticks(np.arange(0, maxtest, 1))
plt.show()
复制代码
kmeans(data, $n_clusters$, m)
:
def kmeans(data, $n_clusters$, m):
'''使用K-Means算法修剪数据集'''
from sklearn.cluster import KMeans
import numpy as np
data_select = []
model = KMeans($n_clusters$=$n_clusters$).fit(data)
centeroids = model.cluster_centers_
label_pred = model.labels_
import collections
for k, v in collections.Counter(label_pred).items():
if v < m:
data_select += np.array(data)[label_pred == k].tolist()
else:
distance = np.sqrt(((np.array(data)[label_pred == k] - centeroids[k]) ** 2).sum(axis=1))
R = distance.mean()
data_select += np.array(data)[label_pred == k][distance >= R].reshape(-1, np.array(data).shape[-1]).tolist()
return np.array(data_select)
复制代码
测试6 对B数据集进行修剪分析,B数据集共有 1877 条数据
# 肘部法则肯定最佳修剪集
elbow_method(B,maxtest = 11)
复制代码
# 根据上面设定的 $n_clusters$ 为3,最小样本量设置为3
B_cut = kmeans(B, $n_clusters$ = 3, m = 3)
复制代码
执行上述程序,原先包含 1877 条数据的 B 数据集修剪为含有 719 条数据的较小的数据集。使用 LOF 算法进行离群检测,检测结果以下:
# 获取会员分布密度,取第10邻域,阈值为3(LOF大于3认为是离群值)
outliers6, inliers6 = lof(B_cut, k=10, method=3)
# 绘图程序
plt.figure('CLOF 离群因子检测')
plt.scatter(np.array(B)[:, 0], np.array(B)[:, 1], s=10, c='b', alpha=0.5)
plt.scatter(outliers6[0], outliers6[1], s=10 + outliers6['local outlier factor']*10, c='r', alpha=0.2)
plt.title('k = 10, method = 3')
复制代码
修剪后的数据集 LOF 意义再也不那么明显,但离群点的 LOF 仍然会是较大的值,而且 k 选取越大的值,判别效果越明显。
合理增大 值能显著提升识别精度。但
值的增长会带来没必要要的计算、影响算法执行效率,也正所以本文所用的
值都取较小。合理选取
与阈值将是
成功与否的关键。
本文内容主要参考算法原文及笔者学习经验进行总结。在异常识别领域,LOF 算法和 Isolation Forest 算法已经被指出是性能最优、识别效果最好的算法。对于经常使用的人群密度(或其余)的刻画,LOF异常分数值不失为一种“高端”方法(参考文献[3]),相比传统方法,更具备成熟的理论支撑。
后续有时间的话,笔者会根据此文方法,结合实际数据详细进一步说明如何在数据处理中应用 LOF 算法。
做者:张柳彬 仇礼鸿
若有疑问,请联系QQ:965579168
转载请声明出处