Press "Enter" to skip to content

面向陌生

如何利用异常检测方法来改进您的监督学习

Photo by Stefan Fluck on Unsplash

传统的预测分析提供了两种范式来解决大多数问题:点估计和分类。现代数据科学主要关注后者,将许多问题以分类的方式来解决(想象一下保险公司如何寻找可能产生高成本的客户,而不是为每个客户预测成本;或者市场营销人员更感兴趣的是哪些广告将带来正回报,而不是预测每个广告的具体回报率)。鉴于此,数据科学家已经开发并熟悉了许多分类方法,从逻辑回归到树和森林方法再到神经网络。然而,有一个问题,即这些方法中的许多在处理大致平衡的结果类别数据时效果最好,而现实世界的应用很少能提供这种数据。在本文中,我将展示如何使用异常检测方法来缓解监督学习中由不平衡结果类别所带来的问题。

让我们假设我计划离开我的家乡匹兹堡,宾夕法尼亚州去旅行。我对去哪里并不挑剔,但我真的想避免像航班取消、改道或者严重延误的旅行问题。一个分类模型可以帮助我识别哪些航班可能会遇到问题,而Kaggle上有一些数据可以帮助我构建这个模型。

我首先读取我的数据并定义了自己对糟糕航班的定义 – 任何被取消、改道或者到达延误超过30分钟的航班。

import pandas as pdimport numpy as npfrom sklearn.compose import make_column_transformerfrom sklearn.ensemble import GradientBoostingClassifier, IsolationForestfrom sklearn.metrics import accuracy_score, confusion_matrixfrom sklearn.model_selection import train_test_splitfrom 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.5倍。尽管孤立森林是一种无监督方法,并且通常识别的是一般意义上的非典型航班,而不是我希望避免的某种特定方式的非典型航班。这似乎是对监督模型而言有价值的信息。二元的异常值标志已经是一个很好的格式,可以作为我的监督模型中的预测因素,所以我将输入并查看是否可以提高模型性能。

# 使用异常标签作为输入特征构建第二个模型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]]

将异常值状态作为监督模型中的预测因素确实能够提高其前十分位提升几个百分点。似乎以一种未定义的方式具有“奇怪”属性与我所期望的结果足够相关,从而提供了预测能力。

当然,这种特性的实用性是有限的。它显然并不适用于所有不平衡分类问题,并且如果解释性对最终产品非常重要的话,这种特性也不是特别有帮助。尽管如此,这种替代性的表述方式可以对各种分类问题提供有益的见解,值得一试。

Leave a Reply

Your email address will not be published. Required fields are marked *