Gira y Enfrenta lo Extraño

'Turn and Face the Strange.

Cómo aprovechar los métodos de detección de anomalías para mejorar su aprendizaje supervisado

Foto de Stefan Fluck en Unsplash

La analítica predictiva tradicional ofrece dos paradigmas a través de los cuales se pueden abordar la mayoría de los problemas: la estimación de puntos y la clasificación. La ciencia de datos moderna se preocupa principalmente por esta última, enmarcando muchas preguntas en términos de categorización (piense en cómo una aseguradora podría buscar identificar qué clientes generarán altos costos, en lugar de predecir los costos para cada cliente; o cómo un comercializador podría estar más interesado en qué anuncios generarán un ROI positivo en lugar de predecir el ROI específico de cada anuncio). Sin embargo, hay un problema: muchos de estos métodos funcionan mejor en datos con clases de resultados equilibradas, algo que rara vez se encuentra en aplicaciones del mundo real. En este artículo, mostraré cómo utilizar métodos de detección de anomalías para mitigar los problemas creados por clases de resultados desequilibradas en el aprendizaje supervisado.

Imaginemos que estoy planeando un viaje fuera de mi ciudad natal, Pittsburgh, Pennsylvania. No tengo preferencia por el destino, pero realmente quiero evitar contratiempos en el viaje, como vuelos cancelados, desviados o con retrasos graves. Un modelo de clasificación podría ayudarme a identificar qué vuelos tienen más probabilidades de experimentar problemas, y Kaggle tiene algunos datos que podrían ayudarme a construir uno.

Comienzo leyendo mis datos y desarrollando mi propia definición de un vuelo problemático: cualquier vuelo cancelado, desviado o con un retraso en la llegada de más de 30 minutos.

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# leer los datosairlines2022 = pd.read_csv('miRuta/Combined_Flights_2022.csv')print(airlines2022.shape)# (4078318, 61)# filtrar por mi ciudad de salida objetivoairlines2022PIT = airlines2022[airlines2022.Origin == 'PIT']print(airlines2022PIT.shape)# (24078, 61)# combinar cancelaciones, desvíos y retrasos de 30 minutos o más en un resultado de Vuelo Problemáticoairlines2022PIT = airlines2022PIT.assign(arrDel30 = airlines2022PIT['ArrDelayMinutes'] >= 30)airlines2022PIT = (airlines2022PIT                   .assign(badFlight = 1 * (airlines2022PIT.Cancelled                                             + airlines2022PIT.Diverted                                            + airlines2022PIT.arrDel30))                  )print(airlines2022PIT.badFlight.mean())# 0.15873411412908048

Aproximadamente el 15% de los vuelos entran en la categoría de “vuelos problemáticos”. Eso no es lo suficientemente bajo como para considerarlo tradicionalmente un problema de detección de anomalías, pero es lo suficientemente bajo como para que los métodos supervisados no funcionen tan bien como esperaría. Sin embargo, comenzaré construyendo un modelo de árbol de aumento de gradiente simple para predecir si un vuelo experimentará el tipo de problema que quiero evitar.

Para comenzar, necesito identificar qué características me gustaría usar en mi modelo. Por el bien de este ejemplo, seleccionaré solo algunas características que parecen prometedoras para modelar; en realidad, la selección de características es una parte muy importante de cualquier proyecto de ciencia de datos. La mayoría de las características disponibles aquí son categóricas y deben codificarse como parte de esta etapa de preparación de datos; la distancia entre las ciudades debe escalarse.

# categorizar columnas por tipo de característicatoFactor = ['Airline', 'Dest', 'Month', 'DayOfWeek'            , 'Marketing_Airline_Network', 'Operating_Airline']toScale = ['Distance']# eliminar campos que no parecen útiles para la predicciónairlines2022PIT = airlines2022PIT[toFactor + toScale + ['badFlight']]print(airlines2022PIT.shape)# (24078, 8)# dividir los datos de entrenamiento originales en conjunto de entrenamiento y validacióntrain, test = train_test_split(airlines2022PIT                               , test_size = 0.2                               , random_state = 412)print(train.shape)# (19262, 8)print(test.shape)# (4816, 8)# escalar manualmente la característica de distanciamn = 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)# crear un codificadorenc = make_column_transformer(    (OneHotEncoder(min_frequency = 0.025, handle_unknown = 'ignore'), toFactor)    , remainder = 'passthrough'    , sparse_threshold = 0)# aplicarlo al conjunto de datos de entrenamientotrain_enc = enc.fit_transform(train)# convertirlo de nuevo a un dataframe de Pandas para facilitar su usotrain_enc_pd = pd.DataFrame(train_enc, columns = enc.get_feature_names_out())# codificar el conjunto de prueba de la misma maneratest_enc = enc.transform(test)test_enc_pd = pd.DataFrame(test_enc, columns = enc.get_feature_names_out())

El desarrollo y ajuste de un modelo basado en árboles podría ser fácilmente su propio post, así que no entraré en detalles aquí. He utilizado las clasificaciones de importancia de las características de un modelo inicial para hacer una selección inversa de características y ajustar el modelo a partir de ahí. El modelo resultante tiene un rendimiento decente en la identificación de vuelos retrasados, cancelados o desviados.

# selección de características - eliminar términos de baja importancia|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)# separar posibles predictores del resultado 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)# construir modelo gbt = GradientBoostingClassifier(learning_rate = 0.1                                 , n_estimators = 100                                 , subsample = 0.7                                 , max_depth = 5                                 , random_state = 412)# ajustarlo a los datos de entrenamientogbt.fit(train_x, train_y)# calcular las puntuaciones de probabilidad para cada observación de prueba gbtPreds1Test = gbt.predict_proba(test_x)[:,1]# utilizar un umbral personalizado para convertirlos en puntuaciones binarias gbtThresh = np.percentile(gbtPreds1Test, 100 * (1 - obsRate))gbtPredsCTest = 1 * (gbtPreds1Test > gbtThresh)# comprobar la precisión del modelo acc = accuracy_score(gbtPredsCTest, test_y)print(acc)# 0.7742940199335548# comprobar aumento topDecile = test_y[gbtPreds1Test > np.percentile(gbtPreds1Test, 90)]lift = sum(topDecile) / len(topDecile) / test_y.mean()print(lift)# 1.8591454794381614# ver matriz de confusión cm = (confusion_matrix(gbtPredsCTest, test_y) / len(test_y)).round(2)print(cm)# [[0.73 0.11]# [0.12 0.04]]

Pero ¿podría ser mejor? Tal vez haya más cosas que aprender sobre los patrones de vuelo utilizando otros métodos. Un bosque de aislamiento es un método de detección de anomalías basado en árboles. Funciona seleccionando de manera iterativa una característica aleatoria del conjunto de datos de entrada y un punto de división aleatorio a lo largo del rango de esa característica. Continúa construyendo un árbol de esta manera hasta que cada observación en el conjunto de datos de entrada se haya dividido en su propia hoja. La idea es que las anomalías, o valores atípicos de datos, son diferentes de otras observaciones, por lo que es más fácil aislarlas con este proceso de selección y división. Por lo tanto, las observaciones que se aíslan con solo unas pocas rondas de selección y división son consideradas anómalas, y aquellas que no pueden separarse rápidamente de sus vecinos no lo son.

El bosque de aislamiento es un método no supervisado, por lo que no se puede utilizar para identificar un tipo particular de anomalía elegido por el científico de datos (por ejemplo, vuelos cancelados, desviados o muy retrasados). Aún así, puede ser útil para identificar observaciones que son diferentes de otras de alguna manera no especificada (por ejemplo, vuelos en los que algo es diferente).

# construir un bosque de aislamientoisf = IsolationForest(n_estimators = 800                      , max_samples = 0.15                      , max_features = 0.1                      , random_state = 412)# ajustarlo a los mismos datos de entrenamientoisf.fit(train_x)# calcular la puntuación de anomalía de cada observación de prueba (valores más bajos son más anómalos)isfPreds1Test = isf.score_samples(test_x)# usar un umbral personalizado para convertirlos en puntuaciones binariasisfThresh = np.percentile(isfPreds1Test, 100 * (obsRate / 2))isfPredsCTest = 1 * (isfPreds1Test < isfThresh)

Combinar las puntuaciones de anomalía con las puntuaciones del modelo supervisado proporciona información adicional.

# combinar predicciones, puntuaciones de anomalía y datos de supervivenciacomb = pd.concat([pd.Series(gbtPredsCTest), pd.Series(isfPredsCTest), pd.Series(test_y)]                 , keys = ['Predicción', 'Atípico', 'Vuelo_malo']                 , axis = 1)comb = comb.assign(Correcto = 1 * (comb.Vuelo_malo == comb.Predicción))print(comb.mean())#Predicción    0.159676#Atípico       0.079942#Vuelo_malo     0.153239#Correcto       0.774294#dtype: float64# mejor precisión en la clase mayoritariaprint(comb.groupby('Vuelo_malo').agg(precisión = ('Correcto', 'mean'))) #          precisión#Vuelo_malo          #0.0        0.862923#1.0        0.284553# más vuelos malos entre los atípicosprint(comb.groupby('Atípico').agg(tasa_vuelo_malo = ('Vuelo_malo', 'mean')))#        tasa_vuelo_malo#Atípico               #0             0.148951#1             0.202597

Hay algunas cosas a tener en cuenta aquí. Una es que el modelo supervisado es mejor para predecir vuelos “buenos” que vuelos “malos” – esto es una dinámica común en la predicción de eventos raros y por eso es importante analizar métricas como precisión y recall además de la precisión simple. Lo más interesante es el hecho de que la tasa de “vuelos malos” es casi 1.5 veces mayor entre los vuelos que el bosque de aislamiento ha clasificado como atípicos. Esto a pesar de que el bosque de aislamiento es un método no supervisado y está identificando vuelos atípicos en general, en lugar de vuelos que son atípicos de la manera particular que me gustaría evitar. Esto parece ser información valiosa para el modelo supervisado. La bandera binaria de atípico ya está en un buen formato para usarla como predictor en mi modelo supervisado, así que la usaré y veré si mejora el rendimiento del modelo.

# construir un segundo modelo con las etiquetas de atípico como características de entradaisfPreds1Train = 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'}))# construir el modelogbt2 = GradientBoostingClassifier(learning_rate = 0.1                                  , n_estimators = 100                                  , subsample = 0.7                                  , max_depth = 5                                  , random_state = 412)# ajustarlo a los datos de entrenamientogbt2.fit(train_2_x, train_y)# calcular las puntuaciones de probabilidad para cada observación de pruebagbt2Preds1Test = gbt2.predict_proba(test_2_x)[:,1]# usar un umbral personalizado para convertirlas en puntuaciones binariasgbtThresh = np.percentile(gbt2Preds1Test, 100 * (1 - obsRate))gbt2PredsCTest = 1 * (gbt2Preds1Test > gbtThresh)# comprobar la precisión del modeloacc = accuracy_score(gbt2PredsCTest, test_y)print(acc)#0.7796926910299004# comprobar el incrementotopDecile = test_y[gbt2Preds1Test > np.percentile(gbt2Preds1Test, 90)]incremento = sum(topDecile) / len(topDecile) / test_y.mean()print(incremento)#1.9138477764819217# ver la matriz de confusióncm = (confusion_matrix(gbt2PredsCTest, test_y) / len(test_y)).round(2)print(cm)#[[0.73 0.11]# [0.11 0.05]]

Incluir el estado de los valores atípicos como un predictor en el modelo supervisado realmente aumenta su mejora en el décimo decil en varios puntos. Parece que ser “extraño” de una manera indefinida está suficientemente correlacionado con mi resultado deseado como para proporcionar poder predictivo.

Por supuesto, hay límites en cuanto a lo útil que puede ser esta peculiaridad. Ciertamente, no es cierto en todos los problemas de clasificación desequilibrados y no es particularmente útil si la explicabilidad es muy importante para el producto final. Sin embargo, este enfoque alternativo puede proporcionar ideas útiles sobre una variedad de problemas de clasificación y vale la pena intentarlo.

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

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more

Inteligencia Artificial

Los principales sitios web están bloqueando a los rastreadores de IA para acceder a su contenido.

En la era de la IA, los editores están bloqueando de manera más agresiva los rastreadores porque, por ahora, no hay b...

Inteligencia Artificial

Este chip centrado en la Inteligencia Artificial redefine la eficiencia duplicando el ahorro de energía al unificar el procesamiento y la memoria.

En un mundo donde la demanda de inteligencia local basada en datos está en aumento, el desafío de permitir que los di...