[譯] 時間順序的價格異常檢測

kasheemlew發表於2019-03-26

Photo credit: Pixabay

異常檢測是指檢測資料集中不遵循其他資料的統計規律的資料點

異常檢測,也叫離群點檢測,是資料探勘中確定異常型別和異常出現的相關細節的過程。如今,自動化異常檢測至關重要,因為現在的資料量太龐大了,人工標記已經不可能實現了。自動異常檢測有著廣泛的應用,例如反欺詐、系統監控、錯誤檢測、感測器網路中的事件檢測等等。

但我將對酒店房費進行異常檢測,原因說起來有點自私。

不知道你是否有過這樣的經歷,比如,你定期到某地出差,每次下榻同一個酒店。通常情況下,房費的波動都不大。但是有些時候,即便還是同一個酒店的同一個房型都貴得嚇人。由於出差補貼的限制,這時你就只能選擇換一家酒店了。被坑了好幾次之後,我開始考慮建立一個模型來自動檢測這種價格異常。

當然,有些反常情況你一輩子只會遇到一次,我們可以提前知道,後面幾年應該不會在同一時間再次碰上。比如 2019 年 2 月 2 日至 2 月 4 日亞特蘭大驚人的房費。

Figure 1

在這篇文章中,我會嘗試不同的異常檢測技術,使用無監督學習對時間序列的酒店房費進行異常檢測。下面我們開始吧!

資料

獲取資料的過程很艱難,我只拿到了一些不夠完美的資料。

我們要使用的資料是 Expedia 個性化酒店搜尋資料的子集,點這裡獲取資料集。

我們將從 training.csv 中分割出一個子集:

  • 選擇資料點最多的酒店 property_id = 104517
  • 選擇 visitor_location_country_id = 219(從另一段分析中可知國家號 219 代表美國)來統一 price_usd 列。 這樣做是因為各個國家在顯示稅費和房費上有不同的習慣,這個房費可能是每晚的費用也可能是總計的費用,但我們知道美國的酒店顯示的就是每晚不含稅費的價格。
  • 選擇 search_room_count = 1.
  • 選擇我們需要的其他特徵:date_timeprice_usdsrch_booking_windowsrch_saturday_night_bool
expedia = pd.read_csv('expedia_train.csv')
df = expedia.loc[expedia['prop_id'] == 104517]
df = df.loc[df['srch_room_count'] == 1]
df = df.loc[df['visitor_location_country_id'] == 219]
df = df[['date_time', 'price_usd', 'srch_booking_window', 'srch_saturday_night_bool']]
複製程式碼

完成分割之後就能得到我們要使用的資料了:

df.info()
複製程式碼

Figure 2

df['price_usd'].describe()
複製程式碼

[譯] 時間順序的價格異常檢測

現在我們發現了一個嚴重的異常,price_usd 的最大值竟然是 5584。

如果一個單獨的資料項與其他資料相比有些反常的話,我們就稱它為單點異常(例如鉅額交易)。我們可以檢查日誌,看看到底是怎麼回事。經過一番調查,我覺得可能是資料錯誤,或者是某個使用者無意間搜了一下總統套房,但是並沒有預定或者瀏覽。為了發現更多比較輕微的異常,我決定刪掉這條資料。

expedia.loc[(expedia['price_usd'] == 5584) & (expedia['visitor_location_country_id'] == 219)]
複製程式碼

Figure 3

df = df.loc[df['price_usd'] < 5584]
複製程式碼

看到這裡,你一定已經發現我們漏掉了些條件,我們不知道使用者搜尋的房型,標準間的價格可是和海景大床房的價格大相徑庭的。為了證明,請記住這一點。好了,該繼續了。

時間序列視覺化

df.plot(x='date_time', y='price_usd', figsize=(12,6))
plt.xlabel('Date time')
plt.ylabel('Price in USD')
plt.title('Time Series of room price by date time of search');
複製程式碼

Figure 4

a = df.loc[df['srch_saturday_night_bool'] == 0, 'price_usd']
b = df.loc[df['srch_saturday_night_bool'] == 1, 'price_usd']
plt.figure(figsize=(10, 6))
plt.hist(a, bins = 50, alpha=0.5, label='Search Non-Sat Night')
plt.hist(b, bins = 50, alpha=0.5, label='Search Sat Night')
plt.legend(loc='upper right')
plt.xlabel('Price')
plt.ylabel('Count')
plt.show();
複製程式碼

Figure 5

總的來說,搜尋非週六的晚上得到的價格更加穩定和低廉,搜尋週六晚上得到的價格明顯上升。看來這家酒店週末的時候比較受歡迎。

基於聚類的異常檢測

k-平均演算法

k-平均是一個應用廣泛的聚類演算法。它建立 ‘k’ 個相似資料點簇。在這些聚類之外的資料項可能被標記為異常。在我們開始用 k-平均聚類之前,我們使用肘部法則來確定最優簇數。

Figure 6

從上圖的肘部曲線來看,我們發現影象在 10 個簇之後逐漸水平,也就是說增加更多的簇並不能解釋相關變數更多的方差;這個例子中的相關變數是 price_usd

我們設定 n_clusters=10,使用 k-平均輸出的資料繪製 3D 的簇。

Figure 7

現在我們得搞清楚要保留幾個成分(特徵)。

Figure 8

我們可以看到,第一個成分解釋瞭解釋了幾乎 50% 的方差,第二個成分解釋了超過 30% 的方差。然而,我們應該注意,沒有哪一個成分是可以忽略不計的。前兩個成分包含了超過 80% 的資訊,所以我們設定 n_components=2

基於聚類的異常檢測中強調的假設是我們對資料聚類,正常的資料歸屬於簇,而異常不屬於任何簇或者屬於很小的簇。下面我們找出異常並進行視覺化。

  • 計算每個點和離它最近的聚類中心的距離。最大的那些距離就是異常。
  • 我們用 outliers_fraction 給演算法提供資料集中離群點比例的資訊。不同的資料集情況可能不同,但是作為一個起點,我估計 outliers_fraction=0.01,這正是標準正態分佈中,偏離均值的距離以 Z 分數的絕對值計超過 3 的觀測值所佔比例。
  • 使用 outliers_fraction 計算 number_of_outliers
  • threshold 設定為離群點間的最短距離。
  • anomaly1 的異常結果包括上述方法的簇(0:正常,1:異常)。
  • 使用叢集檢視視覺化異常。
  • 使用時序檢視視覺化異常。

Figure 9

Figure 10

結果表明,k-平均聚類檢測到的異常房費要麼非常高,要麼非常低。

使用孤立森林進行異常檢測

孤立森林純粹基於異常值的數量少且取值有異這一情況來進行檢測。異常隔離不用度量任何距離或者密度就可以實現。這與基於聚類或者基於距離的演算法完全不同。

  • 我們使用一個IsolationForest模型,設定 contamination = outliers_fraction,這意味著資料集中異常的比例是 0.01。
  • fitpredict(data) 在資料集上執行異常檢測,對於正常值返回 1,對於異常值返回 -1。
  • 最終,我們得到了異常的時序檢視。

Figure 11

基於支援向量機的異常檢測(SVM)

SVM 和監督學習緊密相連,但是 OneClassSVM 可以將異常檢測當作一個無監督的問題,學得一個決策函式:將新資料歸類為與訓練資料集相似或者與訓練資料集不同。

OneClassSVM

根據這篇論文: Support Vector Method for Novelty Detection。SVM 是基於間隔最大的方法,也就是不對一種概率分佈建模。基於 SVM 的異常檢測的核心就是找到一個函式,這個函式對於點密度高的區域輸出正值,對於點密度低的區域返回負值。

  • 在擬合 OneClassSVM 模型時,我們設定 nu=outliers_fraction,這是訓練誤差的上界和支援向量的下界,這個值必須在 0 到 1 之間。這基本上是我們預計資料裡面的離群值佔比多少。
  • 指定演算法中的核函式型別:rbf。此時 SVM 使用非線性函式將超空間對映到更高維度中。
  • gamma 是 RBF 核心型別的一個引數,控制著單個訓練樣本的影響 — 它影響著模型的"平滑度"。經過試驗,我沒發現什麼重要的差別。
  • predict(data) 執行資料分類。因為我們的模型是一個單類模型,所以只會返回 +1 或者 -1,-1 代表異常,1 代表正常。

Figure 12

使用高斯分佈進行異常檢測

高斯分佈又稱為正態分佈。我們將使用高斯分佈開發一個異常檢測演算法,換言之,我們假設資料服從正態分佈。這個假設並不適用於所有資料集,一旦成立,就能高效地確定離群點。

Scikit-Learn 的 [**covariance.EllipticEnvelope**](https://scikit-learn.org/stable/modules/generated/sklearn.covariance.EllipticEnvelope.html) 函式假設我們的全體資料是一概率分佈的外在表現形式,其背後服從一項多變數高斯分佈,以此嘗試計算資料資料總體分佈的關鍵引數。過程類似這樣:

  • 根據之前定義的類別建立兩個不同的資料集 —— search_Sat_night、Search_Non_Sat_night。
  • 對每個類別使用 EllipticEnvelope(高斯分佈)。
  • 我們設定 contamination 引數,它是資料集中出現的離群點的比例。
  • 我們用 decision_function 來計算給定觀測值的決策函式,它和平移馬氏距離等價。為了確保和其他離群點檢測演算法的相容性,成為離群點的閾值被設定為 0。
  • predict(X_train) 使用擬合好的模型預測 X_train 的標籤(1 表示正常,-1 表示異常)。

Figure 13

有趣的是,這種方式檢測只檢測到了異常高的價格,卻沒有檢測到異常低的價格。

目前為止,我們已經用四種方法完成了價格異常檢測。因為我們是用無監督學習進行異常檢測的,建好模型之後,我們沒什麼可以用來對比測試,也就無法知道它的表現究竟如何。因此,在用這些方法處理關鍵問題之前必須對它們的結果進行測試。

Jupyter notebook 已經上傳至 Github。 好好享受這一週吧!

參考文獻:

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章