15  Regresión logística: Evaluación y extensiones

Autores/as

Peña Calero, Brian Norman

Villa Almeyda, Diego

Carrasco Escobar, Gabriel

En este capítulos veremos algunas herramientas y análisis adicionales concernientes a los modelos logísticos. Continuaremos usando los mismos datos:

library(tidyverse)
diabetes <- read_csv("data/diabetes.csv")

dm_data <- diabetes %>%
  mutate(diabetes = factor(diabetes, 
                           levels = c("neg", "pos"))) %>%
  drop_na()

15.1 Diagnóstico del modelo

15.1.1 General

Para generar gráficos de diagnóstico de modelos de regresión logística, podemos usar la función check_model() del paquete performance, que pertenece a la colección de paquetes easystats:

library(performance)
lrm_0 <- glm(diabetes ~ ., family = binomial, data = dm_data)
check_model(lrm_0)

Esta función utiliza los residuales de Pearson estandarizados para elaborar los gráficos. Hay que tener en cuenta que estos residuales son adecuados solo cuando el número de observaciones es grande, ya que están basados en una aproximación normal. En otro caso, es mejor investigar los residuales de devianza.

Los gráficos que se muestran son los siguientes:

  • Posterior Predictive Check: Nos indica qué tan bien el modelo predice la data observada usando simulaciones de la data bajo el modelo ajustado.
  • Binned Residuals: Agrupa los residuales según diferentes niveles de la probabilidad estimada y calcula el promedio en cada grupo. Estos promedios no deberían salir de las bandas de error. En este caso no se aprecian los puntos que están dentro de las bandas (azules). Si esto pasara, podemos construir gráficos individuales para cada predictor.
  • Influential Observations: Muestra posibles observaciones influyentes. Las observaciones no deberían sobrepasar los contornos.
  • Collinearity: Muestra los valores del factor de inflación de la varianza (VIF) para los predictores.
  • Normality of Residuals: No es tan relevante para los modelos de regresión logística, ya que no se asume normalidad de errores. Sin embargo, sirve de referencia para evaluar la distribución de los residuales.

Lo que más podemos rescatar de estos gráficos es la presencia de residuales bastante grandes. Si bien el gráfico de Posterior Predictive Check muestra que en general hay una concordancia entre la distribución de la variable de respuesta y las predicciones de la data, la presencia de residuales demasiado grandes nos indica que el modelo no está ajustando bien para algunas observaciones.

15.1.2 Bondad de ajuste

Si un modelo de regresión logística tiene un buen ajuste, se esperaría que las probabilidades predichas del modelo coincidan en distribución con las proporciones observadas de la variable de respuesta. Una prueba de significancia para verificar esto es la prueba de Hosmer-Lemeshow. La prueba a contrastar es que no hay asociación entre las probabilidades predichas y las proporciones observadas, lo cual indicaría una falta de ajuste.

Una implementación de esta prueba se encuentra en la función performance_hosmer() del paquete performance:

# Hosmer-Lemeshow Goodness-of-Fit Test

  Chi-squared: 3.013
           df: 8    
      p-value: 0.934

Vemos que el p-valor es bastante grande, por lo que no hay evidencia para decir que no hay concordancia entre las probabilidades predichas y las proporciones observadas. Es decir, no hay evidencia para decir que no hay buen ajuste. Sin embargo, esto no quiere decir que necesariamente el modelo ajusta bien, solo que no se puede decir lo contrario.

15.1.3 Especificación de los términos

Podemos generar gráficos de binned residuals para cada predictor para detectar en qué niveles de estos hay residuales grandes en promedio. Además, un patrón no lineal muy evidente de los residuales promedios podría indicar una mala especificación del predictor.

Veamos los gráficos para las variables glucose, mass, pedigree y age, que resultaron significativos en el modelo. Primero obtengamos los gráficos individuales usando la función binned_residuals() y especificando el predictor en el parámetro term:

binned_glucose <- plot(binned_residuals(lrm_0, term = "glucose"))
binned_mass <- plot(binned_residuals(lrm_0, term = "mass"))
binned_pedigree <- plot(binned_residuals(lrm_0, term = "pedigree"))

Ahora, usemos la función wrap_plots() del paquete patchwork para juntar los gráficos en uno solo. Incluiremos los argumentos ncol = 2 y nrow = 2 para que hayan dos filas y dos columnas de gráficos, y el argumento guides = "collect" para que coleccione las leyendas y muestre solo una:

library(patchwork)
wrap_plots(
  binned_glucose, binned_mass, binned_pedigree, ncol = 3, nrow = 1, 
  guides = "collect"
)

Una mejor forma de analizar si existe un una mala especificación de un predictor en el modelo es usando gráficos de efectos marginales junto con residuales parciales. Los efectos marginales son valores predichos de la respuesta que se calculan al margen de los valores o niveles de un predictor específico, es decir, manteniendo constantes los valores o niveles de los demás predictores en el modelo. Por otro lado, los residuales parciales de un predictor son residuales que se calculan luego de haber “retirado” el efecto de las otros predictores en el modelo. Por lo tanto, son ideales para identificar si es que la asociación entre la variable de respuesta y un predictor se está capturando correctamente en el modelo.

Para calcular los efectos marginales de un predictor, podemos usar la función ggpredict() del paquete ggeffects. A esta función le pasamos el modelo y el predictor para el cual queremos calcular los efectos marginales. Para especificar el predictor usamos el parámetro terms y añandimos [all] al nombre del predictor para que se calculen los valores predichos para todos los valores del predictor. Por ejemplo, veamos para glucose:

library(ggeffects)
pred_glucose <- ggpredict(lrm_0, terms = "glucose [all]")

Esta función nos devuelve los valores predichos (efectos marginales) y los valores para los cuales se han mantenido constantes los demás predictores.

Para generar el gráfico de efectos marginales, simplemente usamos la función plot() sobre el objeto que contiene las predicciones:

plot(pred_glucose)

Agregaremos ahora los argumentos residuals = TRUE y residuals.line = TRUE para que se grafiquen los residuales parciales y una curva de tendencia para ellos:

plot_pred_glucose <- plot(pred_glucose, residuals = TRUE, residuals.line = TRUE)
plot_pred_glucose

A mayor discrepancia entre la curva de tendencia de los residuales parciales y la curva de los efectos marginales, mayor evidencia de que el predictor puede estar mal especificado en el modelo. En este caso, vemos que no existe mucha discrepancia.

Veamos los gráficos para mass, pedigree y age:

pred_mass <- ggpredict(lrm_0, terms = "mass [all]")
pred_pedigree <- ggpredict(lrm_0, terms = "pedigree [all]")
pred_age <- ggpredict(lrm_0, terms = "age [all]")

plot_pred_mass <- plot(pred_mass, residuals = TRUE, residuals.line = TRUE)
Data points may overlap. Use the `jitter` argument to add some amount of
  random variation to the location of data points and avoid overplotting.
plot_pred_pedigree <- plot(pred_pedigree, residuals = TRUE, residuals.line = TRUE)
Data points may overlap. Use the `jitter` argument to add some amount of
  random variation to the location of data points and avoid overplotting.
plot_pred_age <- plot(pred_age, residuals = TRUE, residuals.line = TRUE)
Data points may overlap. Use the `jitter` argument to add some amount of
  random variation to the location of data points and avoid overplotting.

Ahora, juntemos todos:

wrap_plots(
  plot_pred_glucose, plot_pred_mass, plot_pred_pedigree, plot_pred_age, 
  ncol = 2, nrow = 2, guides = "collect"
)

En los gráficos de las variables mass, pedigree y age se evidencia que existen valores extremos que distorsionan o fuerzan demasiado los efectos marginales de estos predictores y, por tanto, su asociación con la variable de respuesta en el modelo. Esto pone en tela de juicio los efectos significativos encontrados para estos predictores en el modelo. La magnitud de estos valores extremos nos hace pensar que tal vez estas variables no se han recolectado correctamente o ha habido mucho error en las mediciones, entre otras cosas.

15.2 Modelos anidados

Al igual que en el análisis de regresión lineal, podemos usar la función anova() para generar una tabla de significancia de los modelos anidados. Usemos los modelos lrm_3, lrm_4 y lrm_5:

lrm_2 <- glm(diabetes ~ ., family = binomial, data = dm_std)
lrm_3 <- glm(
  diabetes ~ glucose + mass + pedigree + age, 
  family = binomial, data = dm_std
)
lrm_4 <- glm(
  diabetes ~ glucose*mass + pedigree + age, 
  family = binomial, data = dm_std
)
anova(lrm_2, lrm_3, lrm_4, test = "Chisq")
Analysis of Deviance Table

Model 1: diabetes ~ pregnant + glucose + pressure + triceps + insulin + 
    mass + pedigree + age
Model 2: diabetes ~ glucose + mass + pedigree + age
Model 3: diabetes ~ glucose * mass + pedigree + age
  Resid. Df Resid. Dev Df Deviance Pr(>Chi)
1       383     344.02                     
2       387     347.23 -4  -3.2138   0.5227
3       386     344.80  1   2.4349   0.1187

Vemos que no existen diferencias estadísticamente significativas entre el modelo lrm_3 sobre lrm_2 y tampoco de lrm_4 sobre lrm_3.

15.3 Selección de variables

Para realizar la selección por pasos, podemos usar la función stepAIC() del paquete MASS:

Esta función utiliza el Akaike information criterion (AIC) para seleccionar el mejor modelo.

15.3.0.1 Búsqueda hacia adelante

Para realizar la búsqueda hacia adelante con stepAIC(), tenemos que primero crear el modelo nulo que servirá como modelo inicial para la función:

lrm_null <- glm(diabetes ~ 1, family = binomial, data = dm_std)

Luego, en la función stepAIC() añadimos el argumento direction = "forward" y en scope definimos el rango de modelos que queremos probar:

lrm_forward <- stepAIC(
  lrm_null, direction = "forward", 
  scope = list(lower = diabetes ~ 1, upper = diabetes ~ glucose * mass * pedigree * age)
)
Start:  AIC=500.1
diabetes ~ 1

           Df Deviance    AIC
+ glucose   1   386.67 390.67
+ age       1   450.67 454.67
+ mass      1   469.03 473.03
+ pedigree  1   481.37 485.37
<none>          498.10 500.10

Step:  AIC=390.67
diabetes ~ glucose

           Df Deviance    AIC
+ age       1   370.69 376.69
+ mass      1   372.12 378.12
+ pedigree  1   376.34 382.34
<none>          386.67 390.67

Step:  AIC=376.69
diabetes ~ glucose + age

              Df Deviance    AIC
+ mass         1   354.37 362.37
+ pedigree     1   361.73 369.73
<none>             370.69 376.69
+ glucose:age  1   370.17 378.17

Step:  AIC=362.37
diabetes ~ glucose + age + mass

               Df Deviance    AIC
+ pedigree      1   347.23 357.23
<none>              354.37 362.37
+ glucose:mass  1   352.76 362.76
+ mass:age      1   353.55 363.55
+ glucose:age   1   354.27 364.27

Step:  AIC=357.23
diabetes ~ glucose + age + mass + pedigree

                   Df Deviance    AIC
+ glucose:pedigree  1   340.80 352.80
+ glucose:mass      1   344.80 356.80
<none>                  347.23 357.23
+ mass:pedigree     1   345.75 357.75
+ mass:age          1   346.50 358.50
+ pedigree:age      1   347.06 359.06
+ glucose:age       1   347.18 359.18

Step:  AIC=352.8
diabetes ~ glucose + age + mass + pedigree + glucose:pedigree

                Df Deviance    AIC
<none>               340.80 352.80
+ glucose:mass   1   339.04 353.04
+ mass:age       1   339.99 353.99
+ mass:pedigree  1   340.49 354.49
+ glucose:age    1   340.60 354.60
+ pedigree:age   1   340.64 354.64

Para ver los resultados del ajuste usamos la función summary():

summary(lrm_forward)

Call:
glm(formula = diabetes ~ glucose + age + mass + pedigree + glucose:pedigree, 
    family = binomial, data = dm_std)

Coefficients:
                 Estimate Std. Error z value Pr(>|z|)    
(Intercept)       -1.0249     0.1465  -6.996 2.63e-12 ***
glucose            1.1883     0.1608   7.390 1.47e-13 ***
age                0.5373     0.1389   3.869 0.000109 ***
mass               0.5533     0.1448   3.822 0.000132 ***
pedigree           0.4787     0.1399   3.423 0.000620 ***
glucose:pedigree  -0.3224     0.1164  -2.769 0.005620 ** 
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

(Dispersion parameter for binomial family taken to be 1)

    Null deviance: 498.1  on 391  degrees of freedom
Residual deviance: 340.8  on 386  degrees of freedom
AIC: 352.8

Number of Fisher Scoring iterations: 5

En este caso, el algoritmo selecciona el modelo con solo los efectos principales de glucose, age, mass, pedigree y la interacción entre glucose y pedigree.

15.3.0.2 Búsqueda hacia atrás

Para realizar la búsqueda hacia atrás, primero tenemos que construir el modelo completo:

lrm_full <- glm(
  diabetes ~ glucose * mass * pedigree * age, family = binomial, data = dm_std
)

Para hacer la selección para atrás necesitamos incluir direction = "backward":

lrm_backward <- stepAIC(
  lrm_full, direction = "backward", 
  scope = list(lower = diabetes ~ 1, upper = diabetes ~ glucose * mass * pedigree * age)
)
Start:  AIC=358.35
diabetes ~ glucose * mass * pedigree * age

                            Df Deviance    AIC
- glucose:mass:pedigree:age  1   328.29 358.29
<none>                           326.35 358.35

Step:  AIC=358.29
diabetes ~ glucose + mass + pedigree + age + glucose:mass + glucose:pedigree + 
    mass:pedigree + glucose:age + mass:age + pedigree:age + glucose:mass:pedigree + 
    glucose:mass:age + glucose:pedigree:age + mass:pedigree:age

                        Df Deviance    AIC
- glucose:mass:age       1   328.43 356.43
- glucose:mass:pedigree  1   328.70 356.70
<none>                       328.29 358.29
- glucose:pedigree:age   1   331.40 359.40
- mass:pedigree:age      1   331.70 359.70

Step:  AIC=356.43
diabetes ~ glucose + mass + pedigree + age + glucose:mass + glucose:pedigree + 
    mass:pedigree + glucose:age + mass:age + pedigree:age + glucose:mass:pedigree + 
    glucose:pedigree:age + mass:pedigree:age

                        Df Deviance    AIC
- glucose:mass:pedigree  1   328.94 354.94
<none>                       328.43 356.43
- glucose:pedigree:age   1   331.40 357.40
- mass:pedigree:age      1   331.73 357.73

Step:  AIC=354.94
diabetes ~ glucose + mass + pedigree + age + glucose:mass + glucose:pedigree + 
    mass:pedigree + glucose:age + mass:age + pedigree:age + glucose:pedigree:age + 
    mass:pedigree:age

                       Df Deviance    AIC
- glucose:mass          1   330.71 354.71
<none>                      328.94 354.94
- glucose:pedigree:age  1   331.77 355.77
- mass:pedigree:age     1   332.74 356.74

Step:  AIC=354.71
diabetes ~ glucose + mass + pedigree + age + glucose:pedigree + 
    mass:pedigree + glucose:age + mass:age + pedigree:age + glucose:pedigree:age + 
    mass:pedigree:age

                       Df Deviance    AIC
<none>                      330.71 354.71
- glucose:pedigree:age  1   334.13 356.13
- mass:pedigree:age     1   334.39 356.39

Veamos el resumen:

summary(lrm_backward)

Call:
glm(formula = diabetes ~ glucose + mass + pedigree + age + glucose:pedigree + 
    mass:pedigree + glucose:age + mass:age + pedigree:age + glucose:pedigree:age + 
    mass:pedigree:age, family = binomial, data = dm_std)

Coefficients:
                     Estimate Std. Error z value Pr(>|z|)    
(Intercept)           -1.0325     0.1491  -6.923 4.41e-12 ***
glucose                1.2438     0.1674   7.430 1.09e-13 ***
mass                   0.6396     0.1520   4.208 2.58e-05 ***
pedigree               0.5630     0.1487   3.786 0.000153 ***
age                    0.6163     0.1582   3.895 9.83e-05 ***
glucose:pedigree      -0.3498     0.1270  -2.756 0.005859 ** 
mass:pedigree         -0.1879     0.1454  -1.292 0.196348    
glucose:age            0.1074     0.1919   0.560 0.575535    
mass:age               0.3003     0.1672   1.796 0.072508 .  
pedigree:age           0.1180     0.1860   0.634 0.525890    
glucose:pedigree:age  -0.3227     0.1851  -1.744 0.081205 .  
mass:pedigree:age     -0.3408     0.1845  -1.847 0.064722 .  
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

(Dispersion parameter for binomial family taken to be 1)

    Null deviance: 498.10  on 391  degrees of freedom
Residual deviance: 330.71  on 380  degrees of freedom
AIC: 354.71

Number of Fisher Scoring iterations: 5

15.3.0.3 Búsqueda en ambas direcciones

Para hacer una búsqueda en ambas direcciones, usamos el argumento direction = "both":

lrm_both <- stepAIC(
  lrm_null, direction = "both", 
  scope = list(lower = diabetes ~ 1, upper = diabetes ~ glucose * mass * pedigree * age)
)
Start:  AIC=500.1
diabetes ~ 1

           Df Deviance    AIC
+ glucose   1   386.67 390.67
+ age       1   450.67 454.67
+ mass      1   469.03 473.03
+ pedigree  1   481.37 485.37
<none>          498.10 500.10

Step:  AIC=390.67
diabetes ~ glucose

           Df Deviance    AIC
+ age       1   370.69 376.69
+ mass      1   372.12 378.12
+ pedigree  1   376.34 382.34
<none>          386.67 390.67
- glucose   1   498.10 500.10

Step:  AIC=376.69
diabetes ~ glucose + age

              Df Deviance    AIC
+ mass         1   354.37 362.37
+ pedigree     1   361.73 369.73
<none>             370.69 376.69
+ glucose:age  1   370.17 378.17
- age          1   386.67 390.67
- glucose      1   450.67 454.67

Step:  AIC=362.37
diabetes ~ glucose + age + mass

               Df Deviance    AIC
+ pedigree      1   347.23 357.23
<none>              354.37 362.37
+ glucose:mass  1   352.76 362.76
+ mass:age      1   353.55 363.55
+ glucose:age   1   354.27 364.27
- mass          1   370.69 376.69
- age           1   372.12 378.12
- glucose       1   422.65 428.65

Step:  AIC=357.23
diabetes ~ glucose + age + mass + pedigree

                   Df Deviance    AIC
+ glucose:pedigree  1   340.80 352.80
+ glucose:mass      1   344.80 356.80
<none>                  347.23 357.23
+ mass:pedigree     1   345.75 357.75
+ mass:age          1   346.50 358.50
+ pedigree:age      1   347.06 359.06
+ glucose:age       1   347.18 359.18
- pedigree          1   354.37 362.37
- mass              1   361.73 369.73
- age               1   363.70 371.70
- glucose           1   413.23 421.23

Step:  AIC=352.8
diabetes ~ glucose + age + mass + pedigree + glucose:pedigree

                   Df Deviance    AIC
<none>                  340.80 352.80
+ glucose:mass      1   339.04 353.04
+ mass:age          1   339.99 353.99
+ mass:pedigree     1   340.49 354.49
+ glucose:age       1   340.60 354.60
+ pedigree:age      1   340.64 354.64
- glucose:pedigree  1   347.23 357.23
- age               1   356.68 366.68
- mass              1   356.73 366.73

Veamos el resumen:

summary(lrm_both)

Call:
glm(formula = diabetes ~ glucose + age + mass + pedigree + glucose:pedigree, 
    family = binomial, data = dm_std)

Coefficients:
                 Estimate Std. Error z value Pr(>|z|)    
(Intercept)       -1.0249     0.1465  -6.996 2.63e-12 ***
glucose            1.1883     0.1608   7.390 1.47e-13 ***
age                0.5373     0.1389   3.869 0.000109 ***
mass               0.5533     0.1448   3.822 0.000132 ***
pedigree           0.4787     0.1399   3.423 0.000620 ***
glucose:pedigree  -0.3224     0.1164  -2.769 0.005620 ** 
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

(Dispersion parameter for binomial family taken to be 1)

    Null deviance: 498.1  on 391  degrees of freedom
Residual deviance: 340.8  on 386  degrees of freedom
AIC: 352.8

Number of Fisher Scoring iterations: 5

15.4 Selección de modelos

Ahora veremos cómo evaluar un conjunto de modelos y los criterios para seleccionar un modelo final basados en medidas de performance, es decir, qué tan buena es la bondad de ajuste del modelo y qué tan bien predice.

Podemos usar la función compare_performance() del paquete performance para comparar múltiples modelos en base a diferentes indicadores de ajuste:

compare_performance(lrm_2, lrm_3, lrm_4, lrm_both)
# Comparison of Model Performance Indices

Name     | Model | AIC (weights) | AICc (weights) | BIC (weights) | Tjur's R2 |  RMSE | Sigma | Log_loss | Score_log | Score_spherical |   PCP
----------------------------------------------------------------------------------------------------------------------------------------------
lrm_2    |   glm | 362.0 (0.008) |  362.5 (0.007) | 397.8 (<.001) |     0.364 | 0.376 | 1.000 |    0.439 |   -74.015 |           0.009 | 0.718
lrm_3    |   glm | 357.2 (0.087) |  357.4 (0.089) | 377.1 (0.411) |     0.359 | 0.377 | 1.000 |    0.443 |   -73.555 |           0.008 | 0.716
lrm_4    |   glm | 356.8 (0.108) |  357.0 (0.108) | 380.6 (0.070) |     0.361 | 0.376 | 1.000 |    0.440 |   -72.291 |           0.010 | 0.717
lrm_both |   glm | 352.8 (0.797) |  353.0 (0.796) | 376.6 (0.519) |     0.367 | 0.375 | 1.000 |    0.435 |   -72.230 |           0.010 | 0.719

Los indicadores más comunes son el AIC, BIC y el pseudo-R2 (Tjur’s R2 o R2 de Tjur). En este caso vemos que el modelo lrm_2 tiene un un mayor AIC y BIC. . El R2 de Tjur (también llamado coeficiente de discriminación) nos permite ver que tan bien los valores predichos del modelo discriminan las clases de respuesta. Mientras más se acerque a 1, mayor es la discriminación del modelo. Vemos que el modelo lrm_both tiene un mayor R2 de Tjur, seguido del modelo lrm_2.

Otra forma de medir el performance de los modelos de regresión logística es viendo qué tan bien clasifican las observaciones. Luego de hallar las probabilidades predichas, se toma la decisión de clasificar a una observación dependiendo de un valor de probabilidad de corte, que comúnmente es 0.5. Por ejemplo, en este caso, si una observación tiene una probabilidad predicha mayor a 0.5, entonces se le clasifica como positivo (tiene diabetes), de lo contrario se le clasifica como negativo (no tiene diabetes). Podemos evaluar que tan bien clasifica el modelo comparando la clase dada con las probabilidades predicha con la clase observada.

Para evaluar que tan bien clasifica el modelo a través de todos los posibles valores de corte, se utiliza el área bajo la curva ROC (Receiver Operating Characteristic), o AUC (Area Under ROC curve). A mayor el AUC, mayor el performance de clasificación del modelo.

Para calcular el AUC, primero tenemos que usar la función performance_roc() para obtener los valores de sensibilidad y especificidad por cada modelo y convertir el output en un data frame:

roc_values <- performance_roc(lrm_2, lrm_3, lrm_4, lrm_both) %>% 
  as_tibble()

Luego, podemos usar la función area_under_curve() del paquete bayestestR para calcular el AUC por modelo:

library(bayestestR)

roc_values %>% 
  group_by(Model) %>% 
  summarise(auc = area_under_curve(Specificity, Sensitivity))
# A tibble: 4 × 2
  Model      auc
  <chr>    <dbl>
1 lrm_2    0.862
2 lrm_3    0.860
3 lrm_4    0.860
4 lrm_both 0.862

Vemos que el modelo lrm_both tiene mayor AUC, seguido de lrm_2.

También podemos graficar las curvas ROC usando plot():

plot(performance_roc(lrm_2, lrm_3, lrm_4, lrm_both))

Para ver más métricas de evaluación de clasificación podemos usar las funciones del paquete yardstick. Para esto, debemos obtener las clases predichas y las clases observadas. Para mayor practicidad, podemos crear una función que extraiga las probabilidades predichas de un modelo con la función predict() y que luego transforme esas probabilidades en clases dependiendo si son mayores a 0.5 (pos) o no (neg). Por último, podemos extraer las clases observadas de la data del modelo y juntar las clases predichas y observadas en un data frame:

get_classes <- function(model) {
  pred <- predict(model, type = "response")
  pred_class <- ifelse(pred > 0.5, "pos", "neg")
  truth_class <- model$model$diabetes
  classes <- tibble(truth_class, pred_class)
  classes <- classes %>% 
    mutate(
      pred_class = factor(pred_class, levels = c("neg", "pos")),
      truth_class = factor(truth_class, levels = c("neg", "pos"))
    )
  classes
}

Por ejemplo, extraigamos las clases predichas y observadas de los modelos lrm_2 y lrm_both:

classes_lrm_2 <- get_classes(lrm_2)
classes_lrm_both <- get_classes(lrm_both)

Ahora, pongamos estos data frames en una lista con el nombre de cada modelo:

models_classes_list <- list("lrm_2" = classes_lrm_2, "lrm_both" = classes_lrm_both)

Por último, unamos los data frames usando la función bind_rows y agreguemos el argumento .id = "model" para que se cree una columna llamada model con los nombres de los modelos:

models_classes <- bind_rows(models_classes_list, .id = "model")

Podemos definir múltiples métricas de evaluación usando la función metric_set(). Por ejemplo, incluyamos la exactitud (accuracy), el coeficiente Kappa (kap), la especificidad (spec) y la sensibilidad (sens):


Attaching package: 'yardstick'
The following objects are masked from 'package:performance':

    mae, rmse
The following object is masked from 'package:readr':

    spec
multi_metric <- metric_set(accuracy, kap, spec, sens)

Ahora, para calcular estas métricas para ambos modelos, agrupamos por cada uno de ellos y pasamos las clases observadas (truth) y predichas (estimate) a multi_metric. Además, le decimos que el evento de interés es el segundo nivel (yes) de la variable de respuesta agregando el argumento event_level = "second":

models_classes %>% 
  group_by(model) %>% 
  multi_metric(truth = truth_class, estimate = pred_class, event_level = "second")
# A tibble: 8 × 4
  model    .metric  .estimator .estimate
  <chr>    <chr>    <chr>          <dbl>
1 lrm_2    accuracy binary         0.783
2 lrm_both accuracy binary         0.793
3 lrm_2    kap      binary         0.484
4 lrm_both kap      binary         0.518
5 lrm_2    spec     binary         0.889
6 lrm_both spec     binary         0.878
7 lrm_2    sens     binary         0.569
8 lrm_both sens     binary         0.623

Para ver qué otras métricas se pueden utilizar, podemos revisar la referencia de funciones para métricas de clasificación del paquete yardstick.

La función diag_test() del paquete pubh también permite obtener un conjunto de métricas más sus intervalos de confianza para cada uno de los modelos. Por ejemplo, para el modelo lrm_2:

library(pubh)
classes_lrm_2 %>% 
  diag_test(truth_class ~ pred_class)
          Outcome +    Outcome -      Total
Test +           74           29        103
Test -           56          233        289
Total           130          262        392

Point estimates and 95% CIs:
--------------------------------------------------------------
Apparent prevalence *                  0.26 (0.22, 0.31)
True prevalence *                      0.33 (0.29, 0.38)
Sensitivity *                          0.57 (0.48, 0.66)
Specificity *                          0.89 (0.84, 0.92)
Positive predictive value *            0.72 (0.62, 0.80)
Negative predictive value *            0.81 (0.76, 0.85)
Positive likelihood ratio              5.14 (3.54, 7.48)
Negative likelihood ratio              0.48 (0.40, 0.59)
False T+ proportion for true D- *      0.11 (0.08, 0.16)
False T- proportion for true D+ *      0.43 (0.34, 0.52)
False T+ proportion for T+ *           0.28 (0.20, 0.38)
False T- proportion for T- *           0.19 (0.15, 0.24)
Correctly classified proportion *      0.78 (0.74, 0.82)
--------------------------------------------------------------
* Exact CIs

Por último, podemos usar la función confusionMatrix() del paquete caret. A esta función hay que pasarle la tabulación cruzada de las clases predichas y observadas. Por ejemplo, para el modelo lrm_2:

tbl_lrm_2 <- table(classes_lrm_2$pred_class,
                   classes_lrm_2$truth_class)
tbl_lrm_2
     
      neg pos
  neg 233  56
  pos  29  74

Ahora, pasamos esta tabla a la función confusionMatrix() e incluimos el argumento positive = "pos" para que tome la clase pos como el evento de interés:

library(caret)
confusionMatrix(tbl_lrm_2, positive = "pos")
Confusion Matrix and Statistics

     
      neg pos
  neg 233  56
  pos  29  74
                                        
               Accuracy : 0.7832        
                 95% CI : (0.739, 0.823)
    No Information Rate : 0.6684        
    P-Value [Acc > NIR] : 3.836e-07     
                                        
                  Kappa : 0.4839        
                                        
 Mcnemar's Test P-Value : 0.004801      
                                        
            Sensitivity : 0.5692        
            Specificity : 0.8893        
         Pos Pred Value : 0.7184        
         Neg Pred Value : 0.8062        
             Prevalence : 0.3316        
         Detection Rate : 0.1888        
   Detection Prevalence : 0.2628        
      Balanced Accuracy : 0.7293        
                                        
       'Positive' Class : pos           
                                        

Podemos utilizar la función tidy() del paquete broom para generar un tibble con los resultados:

library(broom)
conf_mat_lrm_2 <- confusionMatrix(tbl_lrm_2, positive = "pos")

tidy(conf_mat_lrm_2)
# A tibble: 14 × 6
   term                 class estimate conf.low conf.high      p.value
   <chr>                <chr>    <dbl>    <dbl>     <dbl>        <dbl>
 1 accuracy             <NA>     0.783    0.739     0.823  0.000000384
 2 kappa                <NA>     0.484   NA        NA     NA          
 3 mcnemar              <NA>    NA       NA        NA      0.00480    
 4 sensitivity          pos      0.569   NA        NA     NA          
 5 specificity          pos      0.889   NA        NA     NA          
 6 pos_pred_value       pos      0.718   NA        NA     NA          
 7 neg_pred_value       pos      0.806   NA        NA     NA          
 8 precision            pos      0.718   NA        NA     NA          
 9 recall               pos      0.569   NA        NA     NA          
10 f1                   pos      0.635   NA        NA     NA          
11 prevalence           pos      0.332   NA        NA     NA          
12 detection_rate       pos      0.189   NA        NA     NA          
13 detection_prevalence pos      0.263   NA        NA     NA          
14 balanced_accuracy    pos      0.729   NA        NA     NA