Abrir en Google Colab Abrir en Binder Descargar notebook

Interpretación de modelos tabulares con feature importance

Introducción

Una de las preguntas más básicas que podemos hacer sobre un modelo es qué características tienen el mayor impacto en las predicciones. Este concepto se llama importancia y se basa en la idea de que las características más importantes tienen un mayor impacto.

Sin embargo, ¿cómo podemos saber cuánto impacto tiene una feature en la predicción? Para responder a esto, tenemos que mirar un problema desde otra perspectiva: “si una característica es importante, cuando falta, la precisión del modelo disminuiría”. Este método también se llama Mean Decrease Accuracy (MDA, Breiman (2001)).

Para una introducción más detallada puede ver la entrada del blog: Model interpretability — Making your model confesses: Feature importance

Utilizando feature importance en el problema censo de la UCI

Veamos como utilizar esta técnica sobre el conjunto de datos y modelo del problema de censo de la UCI.

Instalación

Necesitaremos instalar las librerias:

[9]:
!pip install eli5 --quiet
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 216.2/216.2 kB 5.7 MB/s eta 0:00:00
  Preparing metadata (setup.py) ... done
  Building wheel for eli5 (setup.py) ... done

Sobre el conjunto de datos del censo UCI

El conjunto de datos del censo de la UCI es un conjunto de datos en el que cada registro representa a una persona. Cada registro contiene 14 columnas que describen a una una sola persona, de la base de datos del censo de Estados Unidos de 1994. Esto incluye información como la edad, el estado civil y el nivel educativo. La tarea es determinar si una persona tiene un ingreso alto (definido como ganar más de $50 mil al año). Esta tarea, dado el tipo de datos que utiliza, se usa a menudo en el estudio de equidad, en parte debido a los atributos comprensibles del conjunto de datos, incluidos algunos que contienen tipos sensibles como la edad y el género, y en parte también porque comprende una tarea claramente del mundo real.

Descargamos el conjunto de datos

[2]:
!wget https://santiagxf.blob.core.windows.net/public/datasets/uci_census.zip \
    --quiet --no-clobber
!mkdir -p datasets/uci_census
!unzip -qq uci_census.zip -d datasets/uci_census

Lo importamos

[3]:
import pandas as pd
import numpy as np

train = pd.read_csv('datasets/uci_census/data/adult-train.csv')
test = pd.read_csv('datasets/uci_census/data/adult-test.csv')

Generaremos 3 conjuntos de datos: entrenamiento, validación y testing.

[4]:
from sklearn.model_selection import train_test_split

validation = test
test, _ = train_test_split(test, test_size=0.9, random_state=1234)

Separemos los predictores de la variable a predecir:

[5]:
X_train = train.drop(['income'], axis=1)
y_train = train['income'].to_numpy()
X_test = test.drop(['income'], axis=1)
y_test = test['income'].to_numpy()
X_val = validation.drop(['income'], axis=1)
y_val = validation['income'].to_numpy()

Entrenando un modelo para explorar

Realizaremos un pequeño preprocesamiento antes de entrenar el modelo:

  • Imputaremos los valores faltantes de las caracteristicas numéricas con la media

  • Imputaremos los valores faltantes de las caracteristicas categóricas con el valor ?

  • Escalaremos los valores numericos utilizando un StandardScaler

  • Codificaremos las variables categóricas utilizando OneHotEncoder

[25]:
from typing import Tuple, List

import sklearn
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder, OrdinalEncoder
from sklearn.compose import ColumnTransformer


def prepare(X: pd.DataFrame, transformations: sklearn.compose.ColumnTransformer = None) -> Tuple[pd.DataFrame, sklearn.compose.ColumnTransformer]:
    """
    Escala y codifica los deferentes valores de un conjunto de datos.

    Parameters
    ----------
    X: pd.DataFrame:
        Connto de datos a transformar
    transformations: sklearn.compose.ComlumnTransformer
        Transformaciones que se deben aplicar al conjunto de datos. Si no son indicadas, las mismas son aprendidas desde el conjunto de datos.

    Returns: Tuple[pd.DataFrame, sklearn.compose.ColumnTransformer]
        Una tupla donde el primer component es el conjunto de datos transformado y el segundo las transformaciones que se aplicaron.
    """
    features = {
        'discrete': X.dtypes[X.dtypes == 'object' ].index.tolist(),
        'continuous': X.dtypes[X.dtypes != 'object'].index.tolist(),
    }

    num_pipe = Pipeline([
        ('imputer', SimpleImputer(strategy='median')),
        ('scaler', StandardScaler())
    ])

    cat_pipe = Pipeline([
        ('imputer', SimpleImputer(strategy='constant', fill_value='NA')),
        ('encoder', OrdinalEncoder(handle_unknown='use_encoded_value', unknown_value=-1))
    ])

    if transformations is None:
        transformations = ColumnTransformer(
            [
              ('continuous_pipe', num_pipe, features['continuous']),
              ('discrete_pipe', cat_pipe, features['discrete']),
            ],
            remainder='passthrough')

        X = transformations.fit_transform(X)
    else:
        X = transformations.transform(X)

    transformed_discrete_features = transformations.transformers_[1][1].named_steps['encoder'].get_feature_names_out(features['discrete'])
    all_features = features['continuous'] + list(transformed_discrete_features)

    return pd.DataFrame(X, columns=all_features), transformations


X_train_transformed, transformations = prepare(X_train)
X_test_transformed, _ = prepare(X_test, transformations)

Entrenamos un modelo basado en lightgbm

[32]:
from lightgbm import LGBMClassifier

clf = LGBMClassifier(n_estimators=10000)
model = clf.fit(X_train_transformed, y_train)

Computando los valores de importancia:

Computemos algunos datos que serán útiles luego:

  • classes contiene los nombres de las clases que vamos a predecir

  • features contiene el nombre de todas las columnas del conjunto de datos.

  • categorical_features contiene un dictionario que tiene como índice

[33]:
classes = train['income'].unique().tolist()
features = X_train_transformed.columns.values.tolist()
categorical_features = {idx: X_train_transformed.columns[idx] for idx, col in enumerate(X_train_transformed.dtypes) if col == object}

Utilizando la librería eli5 computaremos la importancia de las características utilizando un conjunto de datos de validación:

[34]:
import eli5
from eli5.sklearn import PermutationImportance

perm = PermutationImportance(model, random_state=123).fit(X_test_transformed, y_test)

Una vez que computamos las permutaciones, podemos visualizarlas:

[35]:
eli5.show_weights(perm, feature_names = features)
[35]:
Weight Feature
0.0491 ± 0.0041 capital-gain
0.0201 ± 0.0082 marital-status
0.0123 ± 0.0030 capital-loss
0.0104 ± 0.0097 education-num
0.0070 ± 0.0051 age
0.0041 ± 0.0094 occupation
0.0012 ± 0.0044 race
0.0000 ± 0.0050 relationship
-0.0004 ± 0.0044 gender
-0.0017 ± 0.0021 native-country
-0.0020 ± 0.0125 hours-per-week
-0.0028 ± 0.0076 fnlwgt
-0.0032 ± 0.0115 education
-0.0066 ± 0.0052 workclass

Cómo interpretar los números

La tabla anterior muestra la importancia de las características de cada una de las columnas. La columna de peso representa la importancia de la característica. El signo +/- representa la desviación estándar de la importancia calculada anteriormente. Este valor trata de medir la cantidad de aleatoriedad en nuestro cálculo de la importancia de la permutación repitiendo el proceso con múltiples mezclas. La columna de peso representa entonces la media del error acumulado en las múltiples mezclas en lugar de en una sola prueba.

Curiosamente, puede ver valores negativos para la importancia. En esos casos, las predicciones sobre los datos mezclados (o ruidosos) resultaron ser más precisas que los datos reales. Esto sucede cuando la característica no importaba y debería haber tenido una importancia cercana a 0, pero la probabilidad aleatoria hizo que las predicciones sobre los datos mezclados fueran más precisas.