本文首發於 知乎專欄-數據科學與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
轉載請聲明出處