Más sobre ciencia de datos en: cienciadedatos.net
- Regresión lineal con Python
- Regresión lineal múltiple con Python
- Regresión logística con Python
- Regularización Ridge, Lasso y Elastic Net con Python
- Machine learning con Python y Scikitlearn
- Árboles de decisión con Python: regresión y clasificación
- Random Forest con Python y Scikit-learn
- Gradient Boosting con Python y Scikit-learn
- Gradient Boosting probabilístico con Python
- Máquinas de Vector Soporte (SVM)
- Redes neuronales con Python
- Análisis de componentes principales PCA
- Clustering
- Detección de anomalías con PCA
- Detección de anomalías con autoencoders
- Detección de anomalías con Gaussian Mixture Models
- Detección de anomalías con Isolation Forest
- Análisis de texto (text mining) con Python
- Reglas de asociación
Introducción¶
Isolation Forest es un método no supervisado para la detección de anomalías (outliers) en conjuntos de datos no etiquetados, es decir, cuando no se conoce a priori si una observación es anómala o normal.
El algoritmo utiliza un conjunto de árboles, de forma conceptualmente similar a Random Forest, aunque con un objetivo distinto: en lugar de aprender reglas predictivas, busca aislar observaciones individuales. Un modelo Isolation Forest está compuesto por múltiples árboles binarios llamados isolation trees.
Cada isolation tree se construye separando recursivamente las observaciones mediante divisiones binarias. A diferencia de los árboles de clasificación o regresión, las variables y los puntos de corte se seleccionan de forma completamente aleatoria, sin optimizar ninguna función de impureza. Las observaciones con valores poco frecuentes o extremos suelen quedar aisladas tras pocas divisiones, mientras que las observaciones normales requieren recorridos más largos dentro del árbol.
La longitud del camino (número de divisiones necesarias para aislar una observación) es, por tanto, el elemento clave del algoritmo.
Algoritmo Isolation Tree
1) Crear un nodo raíz que contiene las $N$ observaciones de entrenamiento.
2) Seleccionar aleatoriamente un atributo $i$ y un valor aleatorio $a$ dentro del rango observado de dicho atributo.
3) Crear dos nuevos nodos separando las observaciones acorde al criterio $x_i \leq a$ o $x_i > a$.
4) Repetir los pasos 2 y 3 hasta que el nodo contenga una única observación o se alcance una profundidad máxima predefinida.
Algoritmo Isolation Forest
El modelo Isolation Forest se obtiene combinando múltiples isolation trees, cada uno entrenado a partir de una muestra generada mediante bootstrapping del conjunto de datos original.
Para cada observación, el modelo calcula la longitud promedio del camino necesario para aislarla a través de todos los árboles del bosque. Cuanto menor es esta longitud promedio, mayor es la probabilidad de que la observación sea una anomalía. En la literatura, esta magnitud suele denominarse path length y, de forma informal, a veces se la llama “distancia de aislamiento”, aunque no representa una distancia en el sentido matemático.
Consideraciones prácticas
Al ser un método no supervisado, no hay forma de conocer el valor óptimo a partir del cual se debe de considerar que se trata de una anomalía. La puntuación asignada a cada observación es una medida relativa respecto al resto de observaciones. En la práctica, suelen considerarse como potenciales outliers aquellas observaciones cuya distancia predicha está por debajo de un determinado cuantil. Por ejemplo, si se considera que hay un 1% de anomalías, se utiliza como límite de decisión el cuantil 0.01 de todas las distancias calculadas.
Cuando se dispone de muchas observaciones, aislarlas todas en nodos terminales requiere de árboles con muchas ramificaciones, lo que se traduce en un coste computacional muy elevado. Una forma de aliviar este problema es determinar una profundidad máxima hasta la que se puede crecer el árbol. A aquellas observaciones que, una vez alcanzado el criterio de parada, no han llegado a nodos terminales individuales, se les suma el número de divisiones teóricas promedio $c(r)$ que se necesita para aislar mediante particiones binarias un nodo de $r$ observaciones.
$$c(r) = \log(r-1) - \frac{2(r - 1)}{r} + 0.5772$$Isolation Forest en Python
Tres de las principales implementaciones de Isolation Forest están disponibles en Scikit Learn, H2O y PyOD, todas altamente optimizadas, aunque con diferencias relevantes en su uso:
Scikit Learn: al entrenar el modelo, se tiene que especificar el porcentaje de anomalías que se espera en los datos de entrenamiento (contamination). Con este valor, el modelo aprende el valor a partir del cual una observación se considera anomalía. Al aplicar el método
predict()se obtiene -1 si es anomalía (outlier) o 1 si es un dato normal (inliers). Para recuperar la métrica de anomalía en lugar de la clasificación, hay que emplear el métodoscore_samples(). Este último devuelve el valor negativo de la distancia de aislamiento, normalizada tal como se propone en el paper original.H2O: En la implementación de H2O, el método
predict()devuelve directamente la puntuación de aislamiento. El usuario debe definir explícitamente el umbral de decisión, habitualmente mediante cuantiles calculados sobre las observaciones de entrenamiento.PyOD: incluye una implementación de Isolation Forest, basada en la de Scikit Learn, pero con algunas funcionalidades adicionales.
Librerías¶
Las librerías utilizadas en este documento son:
# Tratamiento de datos
# ==============================================================================
import numpy as np
import pandas as pd
from mat4py import loadmat
# Gráficos
# ==============================================================================
import matplotlib.pyplot as plt
plt.style.use('fivethirtyeight')
plt.rcParams['lines.linewidth'] = 1.5
plt.rcParams['font.size'] = 8
import seaborn as sns
# Preprocesado y modelado
# ==============================================================================
from sklearn.ensemble import IsolationForest
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
import h2o
from h2o.estimators import H2OExtendedIsolationForestEstimator
from h2o.estimators import H2OGeneralizedLowRankEstimator
from h2o.transforms.preprocessing import H2OScaler
# Configuración warnings
# ==============================================================================
import warnings
warnings.filterwarnings('once')
Isolation Forest¶
Datos¶
Los datos empleados se han obtenido de Outlier Detection DataSets (ODDS), un repositorio con datos comúnmente empleados para comparar la capacidad que tienen diferentes algoritmos a la hora de identificar anomalías (outliers). Shebuti Rayana (2016). ODDS Library. Stony Brook, NY: Stony Brook University, Department of Computer Science.
Todos estos conjuntos de datos están etiquetados, se conoce si las observaciones son o no anomalías (variable y). Aunque los métodos que se describen en el documento son no supervisados, es decir, no hacen uso de la variable respuesta, conocer la verdadera clasificación permite evaluar su capacidad para identificar correctamente las anomalías.
- Cardiotocography dataset link:
- Número de observaciones: 1831
- Número de variables: 21
- Número de outliers: 176 (9.6%)
- y: 1 = outliers, 0 = inliers
- Observaciones: todas las variables están centradas y escaladas (media 0, sd 1).
- Referencia: C. C. Aggarwal and S. Sathe, “Theoretical foundations and algorithms for outlier ensembles.” ACM SIGKDD Explorations Newsletter, vol. 17, no. 1, pp. 24–47, 2015. Saket Sathe and Charu C. Aggarwal. LODES: Local Density meets Spectral Outlier Detection. SIAM Conference on Data Mining, 2016.
Los datos están disponibles en formato MATLAB (.mat). Para leer su contenido se emplea la función loadmat() del paquete mat4py.
# Lectura de datos
# ==============================================================================
datos = loadmat(filename='cardio.mat')
X = pd.DataFrame(datos['X'])
X.columns = ["col_" + str(i) for i in X.columns]
y = pd.Series(np.array(datos['y']).flatten())
Modelo¶
La clase sklearn.ensemble.IsolationForest incorpora las principales funcionalidades que se necesitan a la hora de trabajar con modelos Isolation Forest. Los principales argumentos para entrenar este tipo de modelos son:
n_estimators: número de árboles que forman el modelo.max_samples: número de observaciones empleadas para entrenar cada árbol.contamination: proporción de anomalías esperadas en los datos de entrenamiento. En base a este valor, se establece el límite acorde al cual se clasifican las observaciones en normales o anómalas.random_state: semilla para garantizar la reproducibilidad de los resultados.
Se procede a entrenar un modelo asumiendo que hay un 1% de observaciones anómalas en el conjunto de entrenamiento.
# Definición y entrenamiento del modelo IsolationForest
# ==============================================================================
modelo_isof = IsolationForest(
n_estimators = 1000,
max_samples ='auto',
contamination = 0.01,
n_jobs = -1,
random_state = 123,
)
modelo_isof.fit(X=X)
IsolationForest(contamination=0.01, n_estimators=1000, n_jobs=-1,
random_state=123)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Parameters
| n_estimators | 1000 | |
| max_samples | 'auto' | |
| contamination | 0.01 | |
| max_features | 1.0 | |
| bootstrap | False | |
| n_jobs | -1 | |
| random_state | 123 | |
| verbose | 0 | |
| warm_start | False |
Predicción¶
IsolationForest tienen dos métodos de predicción con los que se obtiene distinta información. Con el método predict() se devuelve directamente la clasificación de anomalía (-1) o no anomalía (1) acorde a la proporción de contaminación que se ha indicado en la definición del modelo.
# Predicción clasificación
# ==============================================================================
clasificacion_predicha = modelo_isof.predict(X=X)
clasificacion_predicha
array([1, 1, 1, ..., 1, 1, 1], shape=(1831,))
Con el método score_samples(), en lugar de la clasificación, se obtiene el valor de anomalía predicho por el modelo. Es importante destacar que este valor no es la distancia de aislamiento promedio, sino una normalización de la misma propuesta en el paper original.
Como resultado de la normalización, y de multiplicarla por -1, los valores de anomalía quedan acotados en el rango [-1, 0]. Cuanto más próximo a -1 es el valor, mayor evidencia de anomalía. Valores entre -0.5 y 0 son los esperados para observaciones normales.
# Predicción valor anomalía
# ==============================================================================
score_anomalia = modelo_isof.score_samples(X=X)
score_anomalia
array([-0.41908062, -0.42765109, -0.45293005, ..., -0.47186108,
-0.46324072, -0.53377418], shape=(1831,))
¿Qué relación hay entre predict() y score_samples()?
Durante el entrenamiento del modelo, se indicó que la proporción de anomalías (contamination) era del 1%. Esta información se utiliza para identificar cuál es el score de anomalía con el que solo un 1% de las observaciones se considerarían anomalías, es decir, el cuantil 0.01.
cuantil_01 = np.quantile(score_anomalia, q=0.01)
cuantil_01
np.float64(-0.5864598467056028)
Este es el valor que se almacena automáticamente en el atributo .offset_ en función de la proporción de continuación indicada.
modelo_isof.offset_
np.float64(-0.5864598467056028)
# Distribución de los valores de anomalía
# ==============================================================================
fig, ax = plt.subplots(figsize=(6, 3))
sns.kdeplot(
data = score_anomalia,
fill = True,
ax = ax
)
sns.rugplot(score_anomalia, ax=ax, color='black')
ax.axvline(cuantil_01, c='red', linestyle='--', label='cuantil 0.01')
ax.set_title('Distribución de los valores de anomalía')
ax.set_xlabel('Score de anomalía')
ax.legend(loc='upper left');
Si se utiliza el valor del cuantil para clasificar las observaciones, los resultados obtenidos son equivalentes a los devueltos por predict().
all(clasificacion_predicha == np.where(score_anomalia < cuantil_01, -1, 1))
True
Detección de anomalías¶
Una vez que la distancia de separación ha sido calculada, se puede emplear como criterio para identificar anomalías. Asumiendo que las observaciones con valores atípicos en alguna de sus variables se separan del resto con mayor facilidad, aquellas observaciones con menor distancia promedio deberían ser las más atípicas. Debido a la normalización que se realiza en la implementación de Scikit Learn, esto se traduce en que, cuanto más negativo es el score predicho mayor evidencia de anomalía.
En la práctica, si se está empleando esta estrategia de detección es porque no se dispone de datos etiquetados, es decir, no se conoce qué observaciones son realmente anomalías. Sin embargo, como en este ejemplo se dispone de la clasificación real, se puede verificar si realmente los datos anómalos tienen menores distancias.
# Distribución de los valores de anomalía
# ==============================================================================
resultados = pd.DataFrame({
'score' : score_anomalia,
'anomalia' : y.astype(str)
})
fig, ax = plt.subplots(figsize=(4, 3))
sns.boxplot(
x = 'anomalia',
y = 'score',
hue = 'anomalia',
data = resultados,
ax = ax
)
ax.set_title('Score de anomalía según clasificación real')
ax.set_ylabel('Score de anomalía')
ax.set_xlabel('clasificación (0 = normal, 1 = anomalía)')
ax.get_legend();
La distribución de los valores de anomalía (score) en el grupo de las anomalías es claramente inferior (más negativo). Sin embargo, al existir solapamiento, si se clasifican las n observaciones con menor score como anomalías, se incurriría en errores de falsos positivos.
Acorde a la documentación, el set de datos Cardiotocogrpahy contiene 176 anomalías. Véase la matriz de confusión resultante si se clasifican como anomalías las 176 observaciones con menor score predicho.
# Matriz de confusión de la clasificación final
# ==============================================================================
resultados = (
resultados
.sort_values('score', ascending=True)
.reset_index(drop=True)
)
resultados['clasificacion'] = np.where(resultados.index <= 176, 1, 0)
pd.crosstab(
resultados['anomalia'],
resultados['clasificacion'],
rownames=['valor real'],
colnames=['prediccion']
)
| prediccion | 0 | 1 |
|---|---|---|
| valor real | ||
| 0.0 | 1571 | 84 |
| 1.0 | 83 | 93 |
De las 176 observaciones identificadas como anomalías, solo el 53% (93/176) lo son. El porcentaje de falsos positivos (47%) es elevado, el método de isolation forest no consigue resultados notables en este set de datos.
Combinación de PCA e Isolation Forest¶
El algoritmo de isolation forest, al igual que muchos otros, se ve perjudicado a medida que aumenta la dimensionalidad del conjunto de datos, es decir, el número de variables. A este fenómeno se le conoce como curse of dimensionality. La razón subyacente por la que aparece este problema es que, en un espacio de alta dimensionalidad, las observaciones están tan alejadas unas de otras que la diferencia entre normal y anómalo desaparece, todo son anomalías.
A esto se le une que, en la practica, muchas de las variables disponibles solo hacen que añadir ruido, no son útiles para discriminar entre observaciones.
Una forma con la que tratar de mitigar el problema es transformar los datos mediante un método de reducción de dimensionalidad antes de entrenar el modelo, por ejemplo, mediante un PCA.
Modelo¶
Mediante los Pipeline de sklearn es sencillo combinar varios modelos en un único objeto.
# Pipeline de un PCA y un IsolationForest
# ==============================================================================
modelo_PCA = PCA(n_components=0.9)
modelo_isof = IsolationForest(
n_estimators = 1000,
max_samples = 100,
contamination = 0.01,
n_jobs = -1,
random_state = 123,
)
pipeline_pca_isof = make_pipeline(StandardScaler(), modelo_PCA, modelo_isof)
pipeline_pca_isof.fit(X=X)
Pipeline(steps=[('standardscaler', StandardScaler()),
('pca', PCA(n_components=0.9)),
('isolationforest',
IsolationForest(contamination=0.01, max_samples=100,
n_estimators=1000, n_jobs=-1,
random_state=123))])In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
Parameters
| steps | [('standardscaler', ...), ('pca', ...), ...] | |
| transform_input | None | |
| memory | None | |
| verbose | False |
Parameters
| copy | True | |
| with_mean | True | |
| with_std | True |
Parameters
| n_components | 0.9 | |
| copy | True | |
| whiten | False | |
| svd_solver | 'auto' | |
| tol | 0.0 | |
| iterated_power | 'auto' | |
| n_oversamples | 10 | |
| power_iteration_normalizer | 'auto' | |
| random_state | None |
Parameters
| n_estimators | 1000 | |
| max_samples | 100 | |
| contamination | 0.01 | |
| max_features | 1.0 | |
| bootstrap | False | |
| n_jobs | -1 | |
| random_state | 123 | |
| verbose | 0 | |
| warm_start | False |
Predicción¶
# Predicción scores
# ==============================================================================
score_anomalia = pipeline_pca_isof.score_samples(X=X)
score_anomalia
array([-0.41306992, -0.42741431, -0.47682121, ..., -0.4604572 ,
-0.44811813, -0.50387696], shape=(1831,))
Detección de anomalías¶
# Matriz de confusión de la clasificación final
# ==============================================================================
resultados = pd.DataFrame({'score': score_anomalia, 'anomalia' : y})
resultados = (
resultados
.sort_values('score', ascending=True)
.reset_index(drop=True)
)
resultados['clasificacion'] = np.where(resultados.index <= 176, 1, 0)
pd.crosstab(
resultados.anomalia,
resultados.clasificacion,
rownames=['valor real'],
colnames=['prediccion']
)
| prediccion | 0 | 1 |
|---|---|---|
| valor real | ||
| 0.0 | 1569 | 86 |
| 1.0 | 85 | 91 |
En este conjunto de datos, la combinación de PCA e Isolation Forest no consigue mejorar los resultados obtenidos.
Extended isolation forest¶
El algoritmo Extended Isolation Forest es una versión mejorada de su predecesor, el Isolation Forest. El algoritmo original introdujo una manera innovadora de detectar anomalías, pero tiene un sesgo debido a la forma en que los árboles deciden cómo dividir los datos.
La versión extendida corrige este sesgo, haciendo que el algoritmo original sea solo un caso especial de la versión generalizada.
El problema del sesgo surge porque, en el algoritmo original, la división de los datos se hace siguiendo un patrón similar al de los árboles binarios de búsqueda (BST). En cada punto de ramificación se elige:
- Una característica específica.
- Un valor de corte.
Esto provoca que las divisiones estén alineadas con los ejes y generen un sesgo.
La versión extendida introduce una pendiente aleatoria en cada división. En lugar de seleccionar directamente la característica y el valor, se elige:
- Una pendiente aleatoria $n$ (un vector en el espacio de características), que define la inclinación del hiperplano de corte.
- Una intersección aleatoria $p$, que determina dónde pasa ese hiperplano por el espacio de los datos.
Cada componente de la pendiente $n$ se genera de forma independiente a partir de una distribución gaussiana $N(0,1)$, y la intersección $p$ se obtiene de una distribución uniforme dentro de los límites de los datos que se van a dividir.
Con este enfoque, un punto $x$ se asigna a un lado de la división siguiendo la regla:
$$ (x - p) \cdot n \leq 0 $$Gracias a esto:
- Las divisiones ya no están limitadas a estar paralelas a los ejes.
- El algoritmo detecta anomalías de manera más precisa y justa.
- Se mantiene la simplicidad y eficiencia del Isolation Forest original.
# Iniciación del cluster H2O y tranferencia de datos
# ==============================================================================
h2o.init(verbose=False)
h2o.remove_all()
h2o.no_progress()
X = h2o.H2OFrame(X)
# Definición y entrenamiento del modelo IsolationForest
# ==============================================================================
modelo_isof = H2OExtendedIsolationForestEstimator(
ntrees = 500,
sample_size = 100,
extension_level = X.shape[1] - 1,
seed = 2345
)
modelo_isof.train(x=X.columns, training_frame=X)
modelo_isof.summary()
| number_of_trees | size_of_subsample | extension_level | seed | number_of_trained_trees | min_depth | max_depth | mean_depth | min_leaves | max_leaves | mean_leaves | min_isolated_point | max_isolated_point | mean_isolated_point | min_not_isolated_point | max_not_isolated_point | mean_not_isolated_point | min_zero_splits | max_zero_splits | mean_zero_splits | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 500 | 100 | 20 | 2345.0 | 500.0 | 6.0 | 6.0 | 6.0 | 7.0 | 39.0 | 19.326 | 0.0 | 18.0 | 6.036 | 82.0 | 100.0 | 93.964 | 0.0 | 15.0 | 6.246 |
# Predicción clasificación
# ==============================================================================
score_anomalia = modelo_isof.predict(X)
score_anomalia = score_anomalia.as_data_frame()['anomaly_score']
# Distribución de los valores de anomalía
# ==============================================================================
fig, ax = plt.subplots(figsize=(6, 3))
sns.kdeplot(
x = score_anomalia,
fill = True,
ax = ax
)
sns.rugplot(score_anomalia, ax=ax, color='black')
ax.set_title('Distribución de los scores de anomalía')
ax.set_xlabel('Score de anomalía');
# Matriz de confusión de la clasificación final
# ==============================================================================
resultados = pd.DataFrame({'score': score_anomalia, 'anomalia': y})
resultados = (
resultados
.sort_values('score', ascending=False)
.reset_index(drop=True)
)
resultados['clasificacion'] = np.where(resultados.index <= 176, 1, 0)
pd.crosstab(
resultados['anomalia'],
resultados['clasificacion'],
rownames=['valor real'],
colnames=['prediccion']
)
| prediccion | 0 | 1 |
|---|---|---|
| valor real | ||
| 0.0 | 1579 | 76 |
| 1.0 | 75 | 101 |
La detección de anomalías con Extended Isolation Forest mejora ligeramente los resultados obtenidos con la versión original del algoritmo de Isolation Forest.
PCA y Extended isolation forest¶
# Reducción de dimensionalidad con PCA en H2O seguido de Extended Isolation Forest
# ==============================================================================
pipeline_pca = make_pipeline(StandardScaler(), PCA(n_components=0.95))
# Se convierte de nuevo X a pandas dataframe
X = X.as_data_frame()
pipeline_pca.fit(X)
proyecciones = pipeline_pca.transform(X)
proyecciones = h2o.H2OFrame(proyecciones)
modelo_isof = H2OExtendedIsolationForestEstimator(
ntrees = 500,
sample_size = 100,
extension_level = proyecciones.shape[1] - 1,
seed = 2345
)
_ = modelo_isof.train(x=proyecciones.columns, training_frame=proyecciones)
modelo_isof.summary()
| number_of_trees | size_of_subsample | extension_level | seed | number_of_trained_trees | min_depth | max_depth | mean_depth | min_leaves | max_leaves | mean_leaves | min_isolated_point | max_isolated_point | mean_isolated_point | min_not_isolated_point | max_not_isolated_point | mean_not_isolated_point | min_zero_splits | max_zero_splits | mean_zero_splits | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 500 | 100 | 13 | 2345.0 | 500.0 | 6.0 | 6.0 | 6.0 | 7.0 | 39.0 | 17.418 | 0.0 | 16.0 | 4.792 | 84.0 | 100.0 | 95.208 | 0.0 | 17.0 | 6.844 |
# Predicción score de anomalía
# ==============================================================================
score_anomalia = modelo_isof.predict(proyecciones)
score_anomalia = score_anomalia.as_data_frame()['anomaly_score']
# Matriz de confusión de la clasificación final
# ==============================================================================
resultados = pd.DataFrame({'score': score_anomalia, 'anomalia': y})
resultados = resultados.sort_values('score', ascending=False).reset_index(drop=True)
resultados['clasificacion'] = np.where(resultados.index <= 176, 1, 0)
pd.crosstab(
resultados['anomalia'],
resultados['clasificacion'],
rownames=['valor real'],
colnames=['prediccion']
)
| prediccion | 0 | 1 |
|---|---|---|
| valor real | ||
| 0.0 | 1579 | 76 |
| 1.0 | 75 | 101 |
En este caso, preprocesar los datos con PCA no parece aportar nada.
GLRM y Extended isolation forest¶
GLRM (Generalized Low Rank Models) es una técnica de reducción de dimensionalidad que generaliza métodos clásicos como PCA, SVD o NMF. Utiliza una matriz de factores de bajo rango para aproximar los datos originales, permitiendo capturar patrones complejos y manejar datos faltantes. GLRM es versátil, aplicable a diversos tipos de datos y útil en tareas como compresión, imputación y detección de anomalías.
Se procede a entrenar un modelo GLRM para reducir la dimensionalidad de los datos antes de aplicar Extended Isolation Forest.
# Modelo GLRM
# ==============================================================================
X = h2o.H2OFrame(X)
glrm_model = H2OGeneralizedLowRankEstimator(
k=min(X.shape),
loss="absolute",
transform="standardize"
)
_ = glrm_model.train(training_frame=X)
glrm_model.summary()
| number_of_iterations | final_step_size | final_objective_value | |
|---|---|---|---|
| 87.0 | 0.0000783 | 1116.9306400 |
Representando la varianza acumulada en función del número de componentes, se observa que con 13 componentes ya se explica el 90% de la varianza total.
# Selección de componentes basada en varianza acumulada
# ==============================================================================
varianza_acumulada = (
glrm_model.varimp(use_pandas=True)
.set_index("")
.transpose()
)
varianza_acumulada
| Standard deviation | Proportion of Variance | Cumulative Proportion | |
|---|---|---|---|
| pc1 | 2.350122 | 2.630036e-01 | 0.263004 |
| pc2 | 1.902645 | 1.723837e-01 | 0.435387 |
| pc3 | 1.336649 | 8.507769e-02 | 0.520465 |
| pc4 | 1.176094 | 6.586650e-02 | 0.586332 |
| pc5 | 1.112758 | 5.896335e-02 | 0.645295 |
| pc6 | 1.011128 | 4.868472e-02 | 0.693980 |
| pc7 | 0.961964 | 4.406544e-02 | 0.738045 |
| pc8 | 0.936282 | 4.174400e-02 | 0.779789 |
| pc9 | 0.836101 | 3.328878e-02 | 0.813078 |
| pc10 | 0.791265 | 2.981432e-02 | 0.842892 |
| pc11 | 0.754825 | 2.713150e-02 | 0.870024 |
| pc12 | 0.701819 | 2.345478e-02 | 0.893478 |
| pc13 | 0.576776 | 1.584144e-02 | 0.909320 |
| pc14 | 0.482282 | 1.107599e-02 | 0.920396 |
| pc15 | 0.425125 | 8.606237e-03 | 0.929002 |
| pc16 | 0.320303 | 4.885440e-03 | 0.933888 |
| pc17 | 0.260599 | 3.233893e-03 | 0.937121 |
| pc18 | 0.248042 | 2.929758e-03 | 0.940051 |
| pc19 | 0.022971 | 2.512610e-05 | 0.940076 |
| pc20 | 0.008350 | 3.319838e-06 | 0.940080 |
| pc21 | 0.000173 | 1.417900e-09 | 0.940080 |
Se procede a transformar los datos originales utilizando las 13 primeras componentes obtenidas con GLRM.
# Modelo GLRM con k=14
# ==============================================================================
glrm_model = H2OGeneralizedLowRankEstimator(
k=13,
loss="absolute",
transform="standardize"
)
_ = glrm_model.train(training_frame=X)
proyecciones = glrm_model.predict(X)
# Modelo Extended Isolation Forest
# ==============================================================================
modelo_isof = H2OExtendedIsolationForestEstimator(
ntrees = 500,
sample_size = 100,
extension_level = proyecciones.shape[1] - 1
)
_ = modelo_isof.train(x=proyecciones.columns, training_frame=proyecciones)
# Predicción
# ==============================================================================
score_anomalia = modelo_isof.predict(proyecciones)
score_anomalia = score_anomalia.as_data_frame()['anomaly_score']
# Matriz de confusión de la clasificación final
# ==============================================================================
resultados = pd.DataFrame({'score': score_anomalia,'anomalia': y})
resultados = (
resultados
.sort_values('score', ascending=False)
.reset_index(drop=True)
)
resultados['clasificacion'] = np.where(resultados.index <= 176, 1, 0)
pd.crosstab(
resultados['anomalia'],
resultados['clasificacion'],
rownames=['valor real'],
colnames=['prediccion']
)
| prediccion | 0 | 1 |
|---|---|---|
| valor real | ||
| 0.0 | 1583 | 72 |
| 1.0 | 71 | 105 |
De entre todas las combinaciones probadas, la que mejores resultados obtiene es la de GLRM y Extended Isolation Forest.
Información de sesión¶
import session_info
session_info.show(html=False)
----- h2o 3.46.0.9 mat4py 0.6.0 matplotlib 3.10.8 numpy 2.2.6 pandas 2.3.3 seaborn 0.13.2 session_info v1.0.1 sklearn 1.7.2 ----- IPython 9.8.0 jupyter_client 8.7.0 jupyter_core 5.9.1 ----- Python 3.13.11 | packaged by Anaconda, Inc. | (main, Dec 10 2025, 21:28:48) [GCC 14.3.0] Linux-6.14.0-37-generic-x86_64-with-glibc2.39 ----- Session information updated at 2026-01-22 23:27
Bibliografía¶
Outlier Analysis Aggarwal, Charu C.
Outlier Ensembles: An Introduction by Charu C. Aggarwal, Saket Sathe
https://www.h2o.ai/blog/anomaly-detection-with-isolation-forests-using-h2o/
Liu, Fei Tony, Ting, Kai Ming and Zhou, Zhi-Hua. “Isolation forest.” Data Mining, 2008. ICDM’08. Eighth IEEE International Conference on
Liu, Fei Tony, Ting, Kai Ming and Zhou, Zhi-Hua. “Isolation-based anomaly detection.” ACM Transactions on Knowledge Discovery from Data (TKDD) 6.1 (2012)
Fei Tony Liu, Kai Ming Ting, and Zhi-Hua Zhou. 2012. Isolation-Based Anomaly Detection. ACM Trans. Knowl. Discov. Data 6, 1, Article 3 (March 2012), 39 pages. DOI:https://doi.org/10.1145/2133360.2133363
Instrucciones para citar¶
¿Cómo citar este documento?
Si utilizas este documento o alguna parte de él, te agradecemos que lo cites. ¡Muchas gracias!
Detección de anomalías con Isolation Forest y python por Joaquín Amat Rodrigo, disponible bajo una licencia Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0 DEED) en https://www.cienciadedatos.net/documentos/py22-deteccion-anomalias-isolation-forest-python.html
¿Te ha gustado el artículo? Tu ayuda es importante
Tu contribución me ayudará a seguir generando contenido divulgativo gratuito. ¡Muchísimas gracias! 😊
Este documento creado por Joaquín Amat Rodrigo tiene licencia Attribution-NonCommercial-ShareAlike 4.0 International.
Se permite:
-
Compartir: copiar y redistribuir el material en cualquier medio o formato.
-
Adaptar: remezclar, transformar y crear a partir del material.
Bajo los siguientes términos:
-
Atribución: Debes otorgar el crédito adecuado, proporcionar un enlace a la licencia e indicar si se realizaron cambios. Puedes hacerlo de cualquier manera razonable, pero no de una forma que sugiera que el licenciante te respalda o respalda tu uso.
-
No-Comercial: No puedes utilizar el material para fines comerciales.
-
Compartir-Igual: Si remezclas, transformas o creas a partir del material, debes distribuir tus contribuciones bajo la misma licencia que el original.
