-clf._decision_function(data)
作用 :获取每一个样本点的 LOF 值,该函数范围 LOF 值的相反数,需要取反号
clf._decision_function
的输出方式更为灵活 :若使用 clf._predict(data) 函数,则按照原先设置的 contamination 输出判断结果(按比例给出判断结果,异常点返回-1,非异常点返回1)
3.1.3 封装函数
localoutlierfactor(data, predict, k)
输入 :训练样本,测试样本, 值
输出 :每一个测试样本 LOF 值及对应的第 距离
plot_lof(result,method)
输入 :LOF 值、阈值
输出 :以索引为横坐标的 LOF 值分布图
lof(data, predict=None, k=5, method=1, plot = False)
输入 :训练样本,测试样本, 值,离群阈值,是否绘制 LOF 图
输出 :离群点、正常点分类情况
没有输入测试样本时,默认测试样本=训练样本
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)
predict['k distances'] = clf.kneighbors(predict)[0] .max(axis =1 )
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
if predict == None:
predict = data.copy()
except Exception:
predict = pd.DataFrame(predict)
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
复制代码
3.1.4 实例测试
测试数据 :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))
outliers1, inliers1 = lof(A, k=5 , method = 2 )
复制代码
给定数据中共有835条数据,设置 LOF 阈值为 2,输出17个离群点信息:
绘制数据点检测情况分布如下图所示,其中蓝色表示任务分布情况,红色范围表示 LOF 值大小:
k=5时检测情况:红色圈越大,LOF 值越大。从图中可以看出检测效果显著
调整 k 值进行多次检测效果,k值越大精度越高
# 绘图程序
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))
outliers2, inliers2 = lof(B, A, k=5 , method=2 )
复制代码
给定训练样本中共有1877条数据,测试样本中共有835条数据,设置 LOF 阈值为 2,输出34个离群点信息:
绘制数据点检测情况分布如下图所示,其中蓝色表示任务分布情况,绿色表示会员分布情况,红色范围表示 LOF 值大小。
k=5时检测情况:红色圈越大,LOF 值越大。从图中可以看出检测效果显著,但误判也较为严重
k=1时检测情况,检测情况不佳。因为k=1时只对点间的距离进行比较,没有太大实际意义
# 绘图程序
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))
outliers3, inliers3 = lof(A, B, k=5 , method=5 )
复制代码
给定训练样本中共有835条数据,测试样本中共有1877条数据,设置 LOF 阈值为 5,输出20个离群点信息:
绘制数据点检测情况分布如下图所示,其中蓝色表示会员分布情况,绿色表示任务分布情况,红色范围表示 LOF 值大小。
k=5时检测情况:红色圈越大,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 值,作为密度指标。
3.2 逐步编程实现方法
3.2.1 距离度量尺度
distances(A, B,model = 'euclidean')
调用 geopy 模块中的函数进行球面距离计算
输入 :A,B两点坐标,距离模式('euclidean', 'geo' 分别为欧式距离和球面距离)
输出 :若A,B为两个坐标点,则返回坐标点距离;若A,B为矩阵,计算规则如下:
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
复制代码
3.2.2 k 邻域半径及邻域点
k_distance(k, instance_A, instance_B, result, model)
调用 3.2.1 的 distances 函数
输入 : k 值,A,B两点坐标,pandas.DataFrame 表用于存储中间信息量,距离模式
输出 :pandas.DataFrame 表(距离、邻域点坐标)
def k_distance(k, instance_A, instance_B, result, model):
'''计算k距离邻域半径及邻域点'''
distance_all = distances(instance_B, instance_A, model)
for i,a in enumerate(instance_A):
distances = {}
distance = distance_all[:,i]
for j in range(distance.shape[0] ):
if distance[j] in distances.keys():
if instance_B[j] .tolist() in distances[distance[j]] :
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 = []
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
复制代码
3.2.3 局部可达密度
local_reachability_density(k,instance_A,instance_B,result, model)
调用 3.2.2 的 k_distance 函数
输入 : k 值,A,B两点坐标,pandas.DataFrame 表用于存储中间信息量,距离模式
输出 :pandas.DataFrame 表(距离、邻域点坐标、局部可达密度)
def local_reachability_density (k,instance_A,instance_B,result, model ):
'''局部可达密度'''
result = k_distance(k, instance_A, instance_B, result, model)
for a in instance_A:
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
复制代码
3.2.4 局部离群因子
k_distance(k, instance_A, instance_B, result, model)
调用 3.2.3 的 local_reachability_density 函数
输入 : k 值,A,B两点坐标,pandas.DataFrame 表用于存储中间信息量,距离模式
输出 :pandas.DataFrame 表(距离、邻域点坐标、局部可达密度、离群因子)
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:
(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
复制代码
3.2.5 封装函数
lof(k,instance_A,instance_B,k_means=False,$n_clusters$=False,k_means_pass=3,method=1,model = 'euclidean'
调用 3.2.4 的 k_distance 函数
输入 : k 值,A,B两点坐标,是否使用聚类算法,聚类簇数,跳过聚类样本数低于3的簇,阈值,距离模式
输出 :离群点信息、正常点信息、LOF 分布图
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)
return outliers, inliers
复制代码
四、LOF 算法相关思考及改进
4.1 一维数据可以使用 LOF 吗?
本文 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)
复制代码
4.2 多大的 LOF 才是离群值?
LOF 计算结果对于多大的值定义为离群值没有明确的规定。在一个平稳数据集中,可能 1.1 已经是一个异常值,而在另一个具有强烈数据波动的数据集中,即使 LOF 值为 2 可能仍是一个正常值。由于方法的局限性,数据集中的异常值界定可能存在差异,笔者认为可以使用统计分布方法作为参考,再结合数据情况最终确定阈值。
基于统计分布的阈值划分
将 LOF 异常值分数归一化到 [0, 1] 区间,运用统计方法进行划分下面提供使用箱型图进行界定的方法,根据异常输出情况参考选取。
box(data, legend=True)
输入 :LOF 异常分数值,在箱型图中绘制异常数据(设置为False不绘制)
输出 :异常识别情况
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()
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 是经过多次试验后选取的数值)。
设置 legend=False 可以关闭右图的标签
4.3 数据维度过大,还能使用 LOF 算法吗?
数据维度过大一方面会增大量纲的影响,另一方面增大计算难度。此时直接使用距离度量的表达形式不合理,并有人为放大较为分散数据影响的风险。一种处理方式是采用马氏距离作为距离度量的方式(去量纲化)。另一种处理方式,参考随机森林的决策思想,可以考虑在多个维度投影上使用 LOF 并将结果结合起来,以提高高维数据的检测质量。
集成学习:通过构建并结合多个学习器来完成学习任务,通常可以获得比单一学习器更显著优越的泛化性能。
——周志华《机器学习》
数据检测中进行使用的数据应该是有意义的数据,这就需要进行简单的特征筛选,否则无论多么“离群”的样本,可能也没有多大的实际意义。根据集成学习的思想,需要将数据按维度拆分,对于同类型的数据,这里假设你已经做好了规约处理(如位置坐标可以放在一起作为一个特征“距离”进行考虑),并且数据的维度大于 1,否则使用 4.1 中的数据变换及一般形式 LOF 即可处理。
4.3.1 投票表决模式
投票表决模式认为每一个维度的数据都是同等重要,单独为每个维度数据设置 LOF 阈值并进行比对,样本的 LOF 值超过阈值则异常票数积 1 分,最终超过票数阈值的样本认为是离群样本。
localoutlierfactor(data, predict, k)
输入 :训练样本,测试样本, k 值
输出 :每一个测试样本 LOF 值及对应的第 k 距离
plot_lof(result,method)
输入 :LOF 值、阈值
输出 :以索引为横坐标的 LOF 值分布图
ensemble_lof(data, predict=None, k=5, groups=[], method=1, vote_method = 'auto')
输入 :训练样本,测试样本, k 值,组合特征索引,离群阈值,票数阈值
组合特征索引(列位置 - 1):如第一列数据与第二列数据作为同类型数据,则传入groups = [[0, 1]]
离群阈值 :每一个特征的离群阈值,缺少位数则以 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)
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)
plt.scatter(result[result > method] .index,
result[result > method] , c ='red' , s=50 ,
marker ='.' , alpha=None,
label ='离群点' )
except Exception:
plt.scatter(result[result <= method] .index,
result[result <= method] , c ='black' , s=50 ,
marker ='.' , alpha=None, label='正常点' )
except Exception:
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
if predict == None:
predict = data.copy()
except Exception:
data = pd.DataFrame(data)
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))
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))
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 ]')
复制代码
仍能较为准确地识别异常点
4.3.2 LOF 异常分数加权模式
异常分数加权模式则是对各维度数据的 LOF 值进行加权,获取最终的 LOF 得分作为整体数据的 LOF 得分。权重可以认为是特征的重要程度,也可以认为是数据分布的相对离散程度,若视为后面一种情形,可以根据熵权法进行设定,关于熵权法的介绍详见笔者另一篇博文 。
式中 表示第 个数据的第 维度 LOF 异常分数值。
localoutlierfactor(data, predict, k)
输入 :训练样本,测试样本, k 值
输出 :每一个测试样本 LOF 值及对应的第 k 距离
plot_lof(result,method)
输入 :LOF 值、阈值
输出 :以索引为横坐标的 LOF 值分布图
ensemble_lof(data, predict=None, k=5, groups=[], method=2, weight=1)
输入 :训练样本,测试样本, k 值,组合特征索引,离群阈值,特征权重
组合特征索引(列位置 - 1):如第一列数据与第二列数据作为同类型数据,则传入groups = [[0, 1]]
离群阈值 :加权 LOF 离群阈值,默认为 2
特征权重 :为每个维度的 LOF 设置权重,缺少位数则以 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)
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
if predict == None:
predict = data
except Exception:
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
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))
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.3 混合模式
混合模式适用于数据中有些特征同等重要,有些特征有重要性区别的情况,即对 4.3.1、4.3.2 情形综合进行考虑。同等重要的数据将使用投票表决模式,重要程度不同的数据使用加权模式并根据阈值转换为投票表决模式。程序上只需将两部分混合使用即可,本文在此不做展示。
4.4 数据量太大,算法执行效率过低,有什么改进方法吗?
LOF 算法在检测离群点的过程中,遍历整个数据集以计算每个点的 LOF 值,使得算法运算速度慢。同时,由于数据正常点的数量一般远远多于离群点的数量,而 LOF 方法通过比较所有数据点的 LOF 值判断离群程度,产生了大量没必要的计算。因此,通过对原始数据进行修剪可以有效提高 LOF 方法的计算效率。此外,实践过程中也发现通过数据集修剪后,可以大幅度减少数据误判为离群点的几率。这种基于聚类修剪得离群点检测方法称为 CLOF (Cluster-Based Local Outlier Factor) 算法。
基于 K-Means 的 CLOF 算法
在应用 LOF 算法前,先用 K-Means 聚类算法,将原始数据聚成 簇。对其中的每一簇,计算簇的中心 ,求出该簇中所有点到该中心的平均距离并记为该簇的半径 。对该类中所有点,若该点到簇中心的距离大于等于 则将其放入“离群点候选集” ,最后对 中的数据使用 LOF 算法计算离群因子。
设第 个簇中的点的个数为 ,点集为
中心和半径的计算公式如下:
如何确定最佳的 —— 肘部法则
K-Means算法通过指定聚类簇数 及随机生成聚类中心,对最靠近他们的对象进行迭代归类,逐次更新各聚类中心的值,直到最好的聚类效果(代价函数值最小)。
对于 的选取将直接影响算法的聚类效果。肘部法则将不同的 值的成本函数值刻画出来,随着 增大,每个簇包含的样本数会减少,样本离其中心更接近,代价函数会减小。但随着 继续增大,代价函数的改善程度不断下降(在图像中,代价函数曲线趋于平稳)。 值增大过程中,代价函数改善程度最大的位置对应的 就是肘部,使用此 一般可以取得不错的效果。但肘部法则的使用仅仅是从代价水平进行考虑,有时候还需结合实际考虑。
由于离群值样本数量一般较少,如果聚类出来的簇中样本量太少(如 1-4 个,但其他簇有成百上千个样本),则这种聚类簇不应进行修剪。
定义代价函数:
CLOF局部离群因子检测算法流程
elbow_method(data,maxtest = 11)
输入 :需要修剪的数据集,最大测试范围(默认为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()
复制代码
不同聚类簇数对应的代价曲线,图中提示 2~4 可能为理想值
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 )
B数据集代价曲线:图中提示 3 可能为理想聚类簇数
B_cut = kmeans(B, $n_clusters $ = 3 , m = 3 )
复制代码
执行上述程序,原先包含 1877 条数据的 B 数据集修剪为含有 719 条数据的较小的数据集。使用 LOF 算法进行离群检测,检测结果如下:
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 ')
使用 CLOF 检测情况,精度进一步得到提升,减少了数据误判率
修剪后的数据集 LOF 意义不再那么明显,但离群点的 LOF 仍然会是较大的值,并且 k 选取越大的值,判别效果越明显。
4.5 如何提高识别精度
合理增大 值能显著提高识别精度。但 值的增加会带来不必要的计算、影响算法执行效率,也正因此本文所用的 值都取较小。合理选取 与阈值将是 成功与否的关键。
阈值选取 1.2,方便展示识别精度随着k增大的提升过程,计算耗时也越来越长(仅取1次实验,小样本、小k值时效果不明显,因此并没有表出严格的递增关系,但最后一图可以明显看到耗时延长)。从气泡大小也可以看出,适当调高阈值也能提高识别精度,不一定依赖于k
本文内容主要参考算法原文及笔者学习经验进行总结。在异常识别领域,LOF 算法和 Isolation Forest 算法已经被指出是性能最优、识别效果最好的算法。对于常用的人群密度(或其他)的刻画,LOF异常分数值不失为一种“高端”方法(参考文献[3]),相比传统方法,更具有成熟的理论支撑。
后续有时间的话,笔者会根据此文方法,结合实际数据详细进一步说明如何在数据处理中应用 LOF 算法。
六、参考文献
[1] 维基百科. Moore–Penrose inverse[EB/OL]. [2018-6-2]. https://en.wikipedia.org/wiki/Moore%E2%80%93Penrose_inverse
[2] Breunig M M, Kriegel H P, Ng R T, et al. LOF: identifying density-based local outliers[C]// ACM SIGMOD International Conference on Management of Data. ACM, 2000:93-104.
[3] 董天文,潘伟堤,戚铭珈,张晓敏. “拍照赚钱”的任务定价. 教育部中国大学生在线网站[J/OL]. [2017-11-13]. http://upload.univs.cn/2017/1113/1510570109509.pdf
作者:张柳彬
如有疑问,请联系QQ:965579168
转载请声明出处