Abrir en Google Colab Abrir en Binder Descargar notebook

Ejemplo: Comparando modelos utilizando una prueba de McNemar

Introducción

Instalamos la librerias necesarias

[ ]:
!wget https://raw.githubusercontent.com/santiagxf/E72102/master/docs/develop/modeling/selection/code/mcnemar.txt \
    --quiet --no-clobber
!pip install -r mcnemar.txt --quiet

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

[ ]:
!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

Preparando nuestros conjuntos de datos

[1]:
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')

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()

Preparación de los datos para el ejemplo

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

[3]:
from typing import Tuple, List

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


def prepare(X: pd.DataFrame) -> Tuple[np.ndarray, sklearn.compose.ColumnTransformer]:
    pipe_cfg = {
        'num_cols': X.dtypes[X.dtypes == 'int64'].index.values.tolist(),
        'cat_cols': X.dtypes[X.dtypes == 'object'].index.values.tolist(),
    }

    num_pipe = Pipeline([
        ('num_imputer', SimpleImputer(strategy='median')),
        ('num_scaler', StandardScaler())
    ])

    cat_pipe = Pipeline([
        ('cat_imputer', SimpleImputer(strategy='constant', fill_value='?')),
        ('cat_encoder', OneHotEncoder(handle_unknown='ignore', sparse=False))
    ])

    transformations = ColumnTransformer([
        ('num_pipe', num_pipe, pipe_cfg['num_cols']),
        ('cat_pipe', cat_pipe, pipe_cfg['cat_cols'])
    ])
    X = transformations.fit_transform(X)

    return X, transformations


X_train_transformed, transformations = prepare(X_train)
X_test_transformed = transformations.transform(X_test)

Definiendo nuestros modelos a comparar

Para demostrar la técnica, utilizaremos dos clasificadores basados en LightGBM

[14]:
from lightgbm import LGBMClassifier

clf1 = LGBMClassifier(n_estimators=100, n_jobs=2)
clf1.fit(X_train_transformed, y_train)
clf2 = LGBMClassifier(n_estimators=100, reg_alpha=1, min_split_gain=2, n_jobs=2)
clf2.fit(X_train_transformed, y_train)
[14]:
LGBMClassifier(min_split_gain=2, n_jobs=2, reg_alpha=1)

Evaluemos su performance:

[15]:
clf1_pred = clf1.predict(X_test_transformed)
clf2_pred = clf2.predict(X_test_transformed)
[22]:
from sklearn.metrics import accuracy_score

print(f'Modelo 1: {accuracy_score(clf1_pred, y_test):.3g}')
print(f'Modelo 2: {accuracy_score(clf2_pred, y_test):.3g}')
Modelo 1: 0.875
Modelo 2: 0.874

Pareciera que el modelo 1 tiene ligeramente una mejor performance. Verifiquemos si vale la pena:

Procedimiento de McNemar

Construimos una tabla de contingencia

La prueba de McNemar se base en una matrix de contingencia de 2x2 donde en las filas tenemos las diferentes instancias de datos, y en las columnas tenemos un idicador mencionando si el modelo realizó una predicción correcta o no. Esto es equivalente a generar una matriz de confusión entre los ambos modelos.

[16]:
from sklearn.metrics import confusion_matrix

cont_table = confusion_matrix(clf1_pred, clf2_pred)

Computamos el valor estadístico

En base a esta tabla, el valor estádistico de McNemar se calcula, para dos modelos:

[17]:
from statsmodels.stats.contingency_tables import mcnemar

results = mcnemar(cont_table, exact=False)
print(results)
pvalue      0.1581559805552949
statistic   1.991769547325103

Tomamos una decisión

[18]:
if results.pvalue <= 0.05:
    print("Rechazamos la hipótesis nula en favor de la alternativa para concluir que los modelos no cometen mismos errores.")
else:
    print("No podemos rechazar la hipotesis de que ambos modelos cometen los mismos errores.")
No podemos rechazar la hipotesis de que ambos modelos cometen los mismos errores.