「振り返って奇妙さに向き合え」

Face the strangeness with reflection.

異常検出手法を活用して教師あり学習を改善する方法

Photo by Stefan Fluck on Unsplash

従来の予測分析には、ほとんどの問題を見るための2つのパラダイムがあります:点推定と分類です。現代のデータサイエンスは主に後者に関心を持ち、多くの問題を分類(保険会社がどのクライアントが高いコストを生み出すかを特定すること、各広告の特定のROIを予測するのではなく、どの広告が正のROIをもたらすかをマーケターがより関心を持つなど)という観点でフレーム化しています。そのため、データサイエンティストは、ロジスティック回帰から木やフォレストベースの手法、ニューラルネットまで、さまざまな分類手法を開発し、使い慣れています。しかし、現実世界のアプリケーションでは、これらの手法の多くは、アウトカムクラスがほぼバランスしているデータで最もうまく機能します。しかし、現実世界のアプリケーションでは、これらの手法の多くは、アウトカムクラスがほぼバランスしているデータで最もうまく機能します。この記事では、教師あり学習における不均衡なアウトカムクラスによって生じる問題を軽減するために、異常検出手法を使用する方法を紹介します。

例えば、私は自宅のあるペンシルベニア州ピッツバーグからの旅行計画を立てています。どこに行くかは気にしませんが、キャンセル、転送、または重大な遅延などのトラベルのトラブルを避けたいと思っています。分類モデルを使用して、問題が発生する可能性のあるフライトを特定することができます。そのためにKaggleには私が構築するのに役立つデータがあります。

まず、データを読み込んで、自分自身の「悪いフライト」の定義を開発します。キャンセル、転送、または到着遅延が30分以上のものです。

import pandas as pd
import numpy as np
from sklearn.compose import make_column_transformer
from sklearn.ensemble import GradientBoostingClassifier, IsolationForest
from sklearn.metrics import accuracy_score, confusion_matrix
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import OneHotEncoder
# データの読み込み
airlines2022 = pd.read_csv('myPath/Combined_Flights_2022.csv')
print(airlines2022.shape) # (4078318, 61)
# 出発地がピッツバーグのデータを抽出
airlines2022PIT = airlines2022[airlines2022.Origin == 'PIT']
print(airlines2022PIT.shape) # (24078, 61)
# キャンセル、転送、30分以上の遅延を悪いフライトの結果にまとめる
airlines2022PIT = airlines2022PIT.assign(arrDel30 = airlines2022PIT['ArrDelayMinutes'] >= 30)
airlines2022PIT = (airlines2022PIT
                   .assign(badFlight = 1 * (airlines2022PIT.Cancelled
                                             + airlines2022PIT.Diverted
                                             + airlines2022PIT.arrDel30))
                  )
print(airlines2022PIT.badFlight.mean()) # 0.15873411412908048

約15%のフライトが「悪いフライト」のカテゴリーに該当します。これは通常、異常検出の問題とは考えられませんが、教師ありの手法では期待通りのパフォーマンスを発揮しない可能性があります。それでも、避けたい問題のタイプを予測するための単純な勾配ブースティングツリーモデルを構築して始めます。

まず、モデルで使用する特徴を特定する必要があります。この例では、モデリングに有望な特徴のいくつかだけを選択します。実際には、特徴選択はデータサイエンスプロジェクトの非常に重要な部分です。ここで利用可能なほとんどの特徴はカテゴリカルであり、このデータ準備の段階でエンコードする必要があります。また、都市間の距離はスケーリングする必要があります。

# 特徴の種類別に列を分類する
toFactor = ['Airline', 'Dest', 'Month', 'DayOfWeek'
            , 'Marketing_Airline_Network', 'Operating_Airline']
toScale = ['Distance']
# 予測には役立たなさそうなフィールドを削除
airlines2022PIT = airlines2022PIT[toFactor + toScale + ['badFlight']]
print(airlines2022PIT.shape) # (24078, 8)
# 元のトレーニングデータをトレーニングセットとバリデーションセットに分割
train, test = train_test_split(airlines2022PIT
                               , test_size = 0.2
                               , random_state = 412)
print(train.shape) # (19262, 8)
print(test.shape) # (4816, 8)
# 都市間の距離のスケーリング
mn = train.Distance.min()
rng = train.Distance.max() - train.Distance.min()
train = train.assign(Distance_sc = (train.Distance - mn) / rng)
test = test.assign(Distance_sc = (test.Distance - mn) / rng)
train.drop('Distance', axis = 1, inplace = True)
test.drop('Distance', axis = 1, inplace = True)
# エンコーダーの作成
enc = make_column_transformer(
    (OneHotEncoder(min_frequency = 0.025, handle_unknown = 'ignore'), toFactor),
    remainder = 'passthrough',
    sparse_threshold = 0)
# トレーニングデータに適用
train_enc = enc.fit_transform(train)
# Pandasのデータフレームに変換
train_enc_pd = pd.DataFrame(train_enc, columns = enc.get_feature_names_out())
# テストデータも同様にエンコード
test_enc = enc.transform(test)
test_enc_pd = pd.DataFrame(test_enc, columns = enc.get_feature_names_out())

ツリーベースモデルの開発と調整は、独自の投稿になる可能性があるため、ここでは詳しく触れません。初期モデルの特徴重要度の評価を使用して逆特徴選択を行い、そこからモデルを調整しました。その結果、このモデルは遅延、キャンセル、または運行変更されたフライトを比較的正確に特定する性能を発揮します。

# 特徴選択 - 重要度の低い項目を削除
lowimp = ['onehotencoder__Airline_Delta Air Lines Inc.'          , 'onehotencoder__Dest_IAD'          , 'onehotencoder__Operating_Airline_AA'          , 'onehotencoder__Airline_American Airlines Inc.'          , 'onehotencoder__Airline_Comair Inc.'          , 'onehotencoder__Airline_Southwest Airlines Co.'          , 'onehotencoder__Airline_Spirit Air Lines'          , 'onehotencoder__Airline_United Air Lines Inc.'          , 'onehotencoder__Airline_infrequent_sklearn'          , 'onehotencoder__Dest_ATL'          , 'onehotencoder__Dest_BOS'          , 'onehotencoder__Dest_BWI'          , 'onehotencoder__Dest_CLT'          , 'onehotencoder__Dest_DCA'          , 'onehotencoder__Dest_DEN'          , 'onehotencoder__Dest_DFW'          , 'onehotencoder__Dest_DTW'          , 'onehotencoder__Dest_JFK'          , 'onehotencoder__Dest_MDW'          , 'onehotencoder__Dest_MSP'          , 'onehotencoder__Dest_ORD'          , 'onehotencoder__Dest_PHL'          , 'onehotencoder__Dest_infrequent_sklearn'          , 'onehotencoder__Marketing_Airline_Network_AA'          , 'onehotencoder__Marketing_Airline_Network_DL'          , 'onehotencoder__Marketing_Airline_Network_G4'          , 'onehotencoder__Marketing_Airline_Network_NK'          , 'onehotencoder__Marketing_Airline_Network_WN'          , 'onehotencoder__Marketing_Airline_Network_infrequent_sklearn'          , 'onehotencoder__Operating_Airline_9E'          , 'onehotencoder__Operating_Airline_DL'          , 'onehotencoder__Operating_Airline_NK'          , 'onehotencoder__Operating_Airline_OH'          , 'onehotencoder__Operating_Airline_OO'          , 'onehotencoder__Operating_Airline_UA'          , 'onehotencoder__Operating_Airline_WN'          , 'onehotencoder__Operating_Airline_infrequent_sklearn']
lowimp = [x for x in lowimp if x in train_enc_pd.columns]
train_enc_pd = train_enc_pd.drop(lowimp, axis = 1)
test_enc_pd = test_enc_pd.drop(lowimp, axis = 1)
# 目的変数を除いた予測変数を分ける
train_x = train_enc_pd.drop('remainder__badFlight', axis = 1)
train_y = train_enc_pd['remainder__badFlight']
test_x = test_enc_pd.drop('remainder__badFlight', axis = 1)
test_y = test_enc_pd['remainder__badFlight']
print(train_x.shape)
print(test_x.shape)
# (19262, 25)
# (4816, 25)
# モデルを構築する
gbt = GradientBoostingClassifier(learning_rate = 0.1
                                 , n_estimators = 100
                                 , subsample = 0.7
                                 , max_depth = 5
                                 , random_state = 412)
# トレーニングデータに適合させる
gbt.fit(train_x, train_y)
# 各テスト観測値の確率スコアを計算する
gbtPreds1Test = gbt.predict_proba(test_x)[:,1]
# カスタムの閾値を使用してこれらをバイナリスコアに変換する
gbtThresh = np.percentile(gbtPreds1Test, 100 * (1 - obsRate))
gbtPredsCTest = 1 * (gbtPreds1Test > gbtThresh)
# モデルの精度をチェックする
acc = accuracy_score(gbtPredsCTest, test_y)
print(acc)
# 0.7742940199335548
# リフトをチェックする
topDecile = test_y[gbtPreds1Test > np.percentile(gbtPreds1Test, 90)]
lift = sum(topDecile) / len(topDecile) / test_y.mean()
print(lift)
# 1.8591454794381614
# 混同行列を表示する
cm = (confusion_matrix(gbtPredsCTest, test_y) / len(test_y)).round(2)
print(cm)
# [[0.73 0.11]
# [0.12 0.04]]

しかし、より良くなる可能性はありますか?他の方法を使って飛行パターンについてもっと学ぶことができるかもしれません。孤立した森は、木ベースの異常検出手法です。入力データセットからランダムに特徴を選択し、その特徴の範囲内でランダムな分割点を選択することにより機能します。このようにして木を構築し、入力データセットの各観測値が独自の葉に分割されるまで続けます。アイデアは、異常値またはデータの外れ値は他の観測値と異なるため、この選択と分割のプロセスでそれらを分離することが容易であるということです。したがって、わずか数回の選択と分割で分離された観測値は異常と見なされ、隣人からすばやく分離できない観測値は異常と見なされません。

孤立した森は非教示学習の手法であるため、データサイエンティスト自身が選択した特定の種類の異常を識別するためには使用できません(例:キャンセル、転送、非常に遅いフライトなど)。しかし、何らかの未指定の方法で他の観測値と異なる観測値を識別するのに役立つことがあります(例:何かが異なるフライト)。

# 孤立した森を構築するisf = IsolationForest(n_estimators = 800                      , max_samples = 0.15                      , max_features = 0.1                      , random_state = 412)# トレーニングデータにフィットさせるisf.fit(train_x)# 各テスト観測値の異常スコアを計算する(値が低いほど異常とされる)isfPreds1Test = isf.score_samples(test_x)# カスタムの閾値を使用してこれらをバイナリスコアに変換するisfThresh = np.percentile(isfPreds1Test, 100 * (obsRate / 2))isfPredsCTest = 1 * (isfPreds1Test < isfThresh)

異常スコアと教示モデルのスコアを組み合わせることで、追加の洞察が得られます。

# 予測、異常スコア、生存データを結合するcomb = pd.concat([pd.Series(gbtPredsCTest), pd.Series(isfPredsCTest), pd.Series(test_y)]                 , keys = ['Prediction', 'Outlier', 'badFlight']                 , axis = 1)comb = comb.assign(Correct = 1 * (comb.badFlight == comb.Prediction))print(comb.mean())#Prediction    0.159676#Outlier       0.079942#badFlight     0.153239#Correct       0.774294#dtype: float64# 多数派クラスの方がより正確print(comb.groupby('badFlight').agg(accuracy = ('Correct', 'mean'))) #          accuracy#badFlight          #0.0        0.862923#1.0        0.284553# アウトライアの中にはより多くの悪いフライトがあるprint(comb.groupby('Outlier').agg(badFlightRate = ('badFlight', 'mean')))#        badFlightRate#Outlier               #0             0.148951#1             0.202597

ここにはいくつか注意すべき点があります。1つは、教示モデルが「良い」フライトを予測するのに「悪い」フライトよりも優れていることです。これは稀なイベントの予測では一般的な動きであり、単純な正確さだけでなく、適合率や再現率などの指標を見ることが重要である理由です。さらに興味深いのは、孤立した森が異常と分類したフライトの中で「悪いフライト」の割合が約1.5倍も高いことです。これは孤立した森が非教示学習の手法であり、特定の方法で異常なフライトではなく一般的に異常なフライトを識別しているためです。これは教示モデルにとって貴重な情報であるように思われます。バイナリのアウトライアフラグは、教示モデルの予測子として使用するのに適した形式で既に存在しているため、これを入力として使用し、モデルのパフォーマンスを向上させるかどうかを確認します。

# アウトライアラベルを入力特徴として使用して2番目のモデルを構築するisfPreds1Train = isf.score_samples(train_x)isfPredsCTrain = 1 * (isfPreds1Train < isfThresh)mn = isfPreds1Train.min(); rng = isfPreds1Train.max() - isfPreds1Train.min()isfPreds1SCTrain = (isfPreds1Train - mn) / rngisfPreds1SCTest = (isfPreds1Test - mn) / rngtrain_2_x = (pd.concat([train_x, pd.Series(isfPredsCTrain)]                       , axis = 1)             .rename(columns = {0:'isfPreds1'}))test_2_x = (pd.concat([test_x, pd.Series(isfPredsCTest)]                      , axis = 1)            .rename(columns = {0:'isfPreds1'}))# モデルを構築するgbt2 = GradientBoostingClassifier(learning_rate = 0.1                                  , n_estimators = 100                                  , subsample = 0.7                                  , max_depth = 5                                  , random_state = 412)# トレーニングデータにフィットさせるgbt2.fit(train_2_x, train_y)# 各テスト観測値の確率スコアを計算するgbt2Preds1Test = gbt2.predict_proba(test_2_x)[:,1]# カスタムの閾値を使用してこれらをバイナリスコアに変換するgbtThresh = np.percentile(gbt2Preds1Test, 100 * (1 - obsRate))gbt2PredsCTest = 1 * (gbt2Preds1Test > gbtThresh)# モデルの正確性をチェックするacc = accuracy_score(gbt2PredsCTest, test_y)print(acc)#0.7796926910299004# リフトをチェックするtopDecile = test_y[gbt2Preds1Test > np.percentile(gbt2Preds1Test, 90)]lift = sum(topDecile) / len(topDecile) / test_y.mean()print(lift)#1.9138477764819217# 混同行列を表示するcm = (confusion_matrix(gbt2PredsCTest, test_y) / len(test_y)).round(2)print(cm)#[[0.73 0.11]# [0.11 0.05]]

外れ値のステータスを予測子として教師ありモデルに含めることは、実際にはトップ10%のリフトを数ポイント上げる効果があります。何らかの未定義の方法で「奇妙」であることは、私の望む結果と十分に相関しているようで、予測力を提供するようです。

もちろん、この特異性は有用な場合にも限りがあります。すべての不均衡な分類問題に当てはまるわけではなく、最終的な製品の説明可能性が非常に重要な場合にはあまり役に立ちません。それでも、この代替フレームワークはさまざまな分類問題に有益な洞察を提供し、試す価値があります。

We will continue to update VoAGI; if you have any questions or suggestions, please contact us!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more