library(ISLR)
library(MASS)
library(mlbench)
library(car)
library(pROC)
library(corrplot)
library(ggplot2)
library(dplyr)
library(tidyr)
library(knitr)
library(kableExtra)
library(plotly)
library(DT)
library(lmtest)SmarketSe trabaja con el dataset Smarket
(ISLR), que contiene 1250 registros diarios del índice
S&P 500 entre 2001 y 2005. La variable de respuesta es \(Y = \texttt{Today}\).
#> 'data.frame': 1250 obs. of 9 variables:
#> $ Year : num 2001 2001 2001 2001 2001 ...
#> $ Lag1 : num 0.381 0.959 1.032 -0.623 0.614 ...
#> $ Lag2 : num -0.192 0.381 0.959 1.032 -0.623 ...
#> $ Lag3 : num -2.624 -0.192 0.381 0.959 1.032 ...
#> $ Lag4 : num -1.055 -2.624 -0.192 0.381 0.959 ...
#> $ Lag5 : num 5.01 -1.055 -2.624 -0.192 0.381 ...
#> $ Volume : num 1.19 1.3 1.41 1.28 1.21 ...
#> $ Today : num 0.959 1.032 -0.623 0.614 0.213 ...
#> $ Direction: Factor w/ 2 levels "Down","Up": 2 2 1 2 2 2 1 2 2 2 ...
Smarket %>%
select(-Direction) %>%
pivot_longer(everything(), names_to = "Variable", values_to = "Valor") %>%
group_by(Variable) %>%
summarise(
n = n(),
Media = round(mean(Valor), 4),
DE = round(sd(Valor), 4),
Min = round(min(Valor), 4),
Q1 = round(quantile(Valor, 0.25), 4),
Mediana = round(median(Valor), 4),
Q3 = round(quantile(Valor, 0.75), 4),
Max = round(max(Valor), 4)
) %>%
kable(caption = "Resumen estadístico de las variables numéricas") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| Variable | n | Media | DE | Min | Q1 | Mediana | Q3 | Max |
|---|---|---|---|---|---|---|---|---|
| Lag1 | 1250 | 0.0038 | 1.1363 | -4.9220 | -0.6395 | 0.0390 | 0.5968 | 5.7330 |
| Lag2 | 1250 | 0.0039 | 1.1363 | -4.9220 | -0.6395 | 0.0390 | 0.5968 | 5.7330 |
| Lag3 | 1250 | 0.0017 | 1.1387 | -4.9220 | -0.6400 | 0.0385 | 0.5968 | 5.7330 |
| Lag4 | 1250 | 0.0016 | 1.1388 | -4.9220 | -0.6400 | 0.0385 | 0.5968 | 5.7330 |
| Lag5 | 1250 | 0.0056 | 1.1476 | -4.9220 | -0.6400 | 0.0385 | 0.5970 | 5.7330 |
| Today | 1250 | 0.0031 | 1.1363 | -4.9220 | -0.6395 | 0.0385 | 0.5968 | 5.7330 |
| Volume | 1250 | 1.4783 | 0.3604 | 0.3561 | 1.2574 | 1.4230 | 1.6417 | 3.1525 |
| Year | 1250 | 2003.0160 | 1.4090 | 2001.0000 | 2002.0000 | 2003.0000 | 2004.0000 | 2005.0000 |
Smarket %>%
select(-Direction) %>%
pivot_longer(everything(), names_to = "variable", values_to = "valor") %>%
ggplot(aes(x = valor)) +
geom_histogram(aes(y = after_stat(density)), bins = 30,
fill = "#004B93", alpha = 0.65) +
geom_density(color = "#5A6C7D", linewidth = 0.8) +
facet_wrap(~ variable, scales = "free", ncol = 3) +
labs(x = "Valor", y = "Densidad") +
theme_minimal(base_size = 11)cor_matrix <- cor(Smarket %>% select(-Direction))
corrplot(cor_matrix, method = "color", type = "upper",
tl.col = "#002D5A", tl.srt = 45, addCoef.col = "#002D5A",
number.cex = 0.7,
col = colorRampPalette(c("#8DA0B0", "white", "#004B93"))(200))Smarket %>%
pivot_longer(cols = c(Lag1:Lag5, Volume),
names_to = "variable", values_to = "valor") %>%
ggplot(aes(x = Direction, y = valor, fill = Direction)) +
geom_boxplot(alpha = 0.65, outlier.alpha = 0.4) +
facet_wrap(~ variable, scales = "free_y", ncol = 3) +
scale_fill_manual(values = c("Down" = "#5A6C7D", "Up" = "#004B93")) +
theme_minimal(base_size = 11) +
theme(legend.position = "bottom")Las variables Lag1–Lag5 no muestran diferencias apreciables entre días de subida y bajada. Lo más notable es la correlación entre
VolumeyYear(≈ 0.54), lo cual es esperable dado el crecimiento secular del volumen de operaciones.
TodaySe ajusta primero el modelo completo con todas las variables
(excluyendo Direction) y luego se aplica selección stepwise
bidireccional con criterio AIC.
#>
#> Call:
#> lm(formula = Today ~ . - Direction, data = Smarket)
#>
#> Residuals:
#> Min 1Q Median 3Q Max
#> -4.8923 -0.6573 0.0176 0.5904 5.6333
#>
#> Coefficients:
#> Estimate Std. Error t value Pr(>|t|)
#> (Intercept) -58.170527 54.726633 -1.063 0.288
#> Year 0.029057 0.027365 1.062 0.289
#> Lag1 -0.027620 0.028367 -0.974 0.330
#> Lag2 -0.012707 0.028446 -0.447 0.655
#> Lag3 -0.005425 0.028397 -0.191 0.849
#> Lag4 -0.009786 0.028413 -0.344 0.731
#> Lag5 -0.036272 0.028118 -1.290 0.197
#> Volume -0.018156 0.107157 -0.169 0.865
#>
#> Residual standard error: 1.138 on 1242 degrees of freedom
#> Multiple R-squared: 0.003211, Adjusted R-squared: -0.002407
#> F-statistic: 0.5715 on 7 and 1242 DF, p-value: 0.7795
#>
#> Call:
#> lm(formula = Today ~ 1, data = Smarket)
#>
#> Residuals:
#> Min 1Q Median 3Q Max
#> -4.9251 -0.6426 0.0354 0.5936 5.7299
#>
#> Coefficients:
#> Estimate Std. Error t value Pr(>|t|)
#> (Intercept) 0.003138 0.032140 0.098 0.922
#>
#> Residual standard error: 1.136 on 1249 degrees of freedom
anova(modelo_step) %>%
kable(digits = 4, caption = "ANOVA del modelo seleccionado") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| Df | Sum Sq | Mean Sq | F value | Pr(>F) | |
|---|---|---|---|---|---|
| Residuals | 1249 | 1612.778 | 1.2913 | NA | NA |
Se utilizó selección stepwise bidireccional con AIC. El modelo resultante y sus indicadores se muestran arriba.
DirectionAhora se considera \(Y = \texttt{Direction}\) como variable respuesta y se ajusta un modelo logístico con los rezagos Lag1–Lag5 como predictores.
glm_full <- glm(Direction ~ Lag1 + Lag2 + Lag3 + Lag4 + Lag5,
data = Smarket, family = binomial)
summary(glm_full)#>
#> Call:
#> glm(formula = Direction ~ Lag1 + Lag2 + Lag3 + Lag4 + Lag5, family = binomial,
#> data = Smarket)
#>
#> Coefficients:
#> Estimate Std. Error z value Pr(>|z|)
#> (Intercept) 0.074163 0.056674 1.309 0.191
#> Lag1 -0.071325 0.050104 -1.424 0.155
#> Lag2 -0.044136 0.050025 -0.882 0.378
#> Lag3 0.009229 0.049879 0.185 0.853
#> Lag4 0.007211 0.049898 0.145 0.885
#> Lag5 0.009311 0.049490 0.188 0.851
#>
#> (Dispersion parameter for binomial family taken to be 1)
#>
#> Null deviance: 1731.2 on 1249 degrees of freedom
#> Residual deviance: 1728.3 on 1244 degrees of freedom
#> AIC: 1740.3
#>
#> Number of Fisher Scoring iterations: 3
#>
#> Call:
#> glm(formula = Direction ~ 1, family = binomial, data = Smarket)
#>
#> Coefficients:
#> Estimate Std. Error z value Pr(>|z|)
#> (Intercept) 0.07363 0.05661 1.301 0.193
#>
#> (Dispersion parameter for binomial family taken to be 1)
#>
#> Null deviance: 1731.2 on 1249 degrees of freedom
#> Residual deviance: 1731.2 on 1249 degrees of freedom
#> AIC: 1733.2
#>
#> Number of Fisher Scoring iterations: 3
glm_best <- if (length(coef(glm_step)) <= 1) glm_full else glm_step
cat("Variables en el modelo final:", length(coef(glm_best)) - 1, "\n")#> Variables en el modelo final: 5
Se aplicó stepwise con AIC. Dado que los retornos pasados tienen escaso poder predictivo sobre la dirección futura (resultado consistente con la hipótesis de mercados eficientes), el procedimiento puede reducir el modelo al intercepto. En tal caso, se reportan los odds ratios del modelo completo para facilitar la interpretación.
ci_raw <- confint(glm_best)
ci_matrix <- matrix(ci_raw, ncol = 2)
rownames(ci_matrix) <- names(coef(glm_best))
data.frame(
Variable = names(coef(glm_best)),
Log_odds = round(coef(glm_best), 4),
OR = round(exp(coef(glm_best)), 4),
IC_2.5 = round(exp(ci_matrix[, 1]), 4),
IC_97.5 = round(exp(ci_matrix[, 2]), 4)
) %>%
kable(col.names = c("Variable", "Coef (log-odds)", "OR", "IC 2.5%", "IC 97.5%"),
caption = "Odds Ratios con intervalos de confianza al 95%") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| Variable | Coef (log-odds) | OR | IC 2.5% | IC 97.5% | |
|---|---|---|---|---|---|
| (Intercept) | (Intercept) | 0.0742 | 1.0770 | 0.9638 | 1.2036 |
| Lag1 | Lag1 | -0.0713 | 0.9312 | 0.8436 | 1.0270 |
| Lag2 | Lag2 | -0.0441 | 0.9568 | 0.8671 | 1.0553 |
| Lag3 | Lag3 | 0.0092 | 1.0093 | 0.9152 | 1.1131 |
| Lag4 | Lag4 | 0.0072 | 1.0072 | 0.9133 | 1.1109 |
| Lag5 | Lag5 | 0.0093 | 1.0094 | 0.9160 | 1.1124 |
Un OR mayor a 1 indica que un aumento unitario en el predictor incrementa las probabilidades de que el mercado suba. Cuando el intervalo de confianza incluye el 1, el efecto no es significativo al 5%. En este caso, ninguno de los Lags muestra significancia, lo cual es coherente con la dificultad de predecir retornos bursátiles a partir de información pasada.
BostonEl dataset Boston (librería MASS) contiene
información sobre 506 vecindarios del área de Boston. Se busca predecir
medv, el valor mediano de las viviendas (en miles de
USD).
#>
#> Call:
#> lm(formula = medv ~ ., data = Boston)
#>
#> Residuals:
#> Min 1Q Median 3Q Max
#> -15.595 -2.730 -0.518 1.777 26.199
#>
#> Coefficients:
#> Estimate Std. Error t value Pr(>|t|)
#> (Intercept) 3.646e+01 5.103e+00 7.144 3.28e-12 ***
#> crim -1.080e-01 3.286e-02 -3.287 0.001087 **
#> zn 4.642e-02 1.373e-02 3.382 0.000778 ***
#> indus 2.056e-02 6.150e-02 0.334 0.738288
#> chas 2.687e+00 8.616e-01 3.118 0.001925 **
#> nox -1.777e+01 3.820e+00 -4.651 4.25e-06 ***
#> rm 3.810e+00 4.179e-01 9.116 < 2e-16 ***
#> age 6.922e-04 1.321e-02 0.052 0.958229
#> dis -1.476e+00 1.995e-01 -7.398 6.01e-13 ***
#> rad 3.060e-01 6.635e-02 4.613 5.07e-06 ***
#> tax -1.233e-02 3.760e-03 -3.280 0.001112 **
#> ptratio -9.527e-01 1.308e-01 -7.283 1.31e-12 ***
#> black 9.312e-03 2.686e-03 3.467 0.000573 ***
#> lstat -5.248e-01 5.072e-02 -10.347 < 2e-16 ***
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#>
#> Residual standard error: 4.745 on 492 degrees of freedom
#> Multiple R-squared: 0.7406, Adjusted R-squared: 0.7338
#> F-statistic: 108.1 on 13 and 492 DF, p-value: < 2.2e-16
anova(lm_boston) %>%
kable(digits = 4, caption = "Tabla ANOVA del modelo completo") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| Df | Sum Sq | Mean Sq | F value | Pr(>F) | |
|---|---|---|---|---|---|
| crim | 1 | 6440.7831 | 6440.7831 | 286.0300 | 0.0000 |
| zn | 1 | 3554.3362 | 3554.3362 | 157.8452 | 0.0000 |
| indus | 1 | 2551.2364 | 2551.2364 | 113.2984 | 0.0000 |
| chas | 1 | 1529.8479 | 1529.8479 | 67.9393 | 0.0000 |
| nox | 1 | 76.2476 | 76.2476 | 3.3861 | 0.0664 |
| rm | 1 | 10938.1166 | 10938.1166 | 485.7530 | 0.0000 |
| age | 1 | 90.2679 | 90.2679 | 4.0087 | 0.0458 |
| dis | 1 | 1779.5011 | 1779.5011 | 79.0262 | 0.0000 |
| rad | 1 | 34.1343 | 34.1343 | 1.5159 | 0.2188 |
| tax | 1 | 329.5541 | 329.5541 | 14.6352 | 0.0001 |
| ptratio | 1 | 1309.3093 | 1309.3093 | 58.1454 | 0.0000 |
| black | 1 | 593.3376 | 593.3376 | 26.3496 | 0.0000 |
| lstat | 1 | 2410.8387 | 2410.8387 | 107.0634 | 0.0000 |
| Residuals | 492 | 11078.7846 | 22.5179 | NA | NA |
El estadístico \(F\) global contrasta \(H_0\): todos los coeficientes son cero simultáneamente. Con un p-valor extremadamente bajo, rechazamos \(H_0\) — al menos un predictor tiene relación lineal significativa con
medv. A nivel individual, variables comorm,lstat,disyptratiopresentan p-valores muy por debajo de 0.05.
#>
#> Call:
#> lm(formula = medv ~ crim + zn + chas + nox + rm + dis + rad +
#> tax + ptratio + black + lstat, data = Boston)
#>
#> Residuals:
#> Min 1Q Median 3Q Max
#> -15.5984 -2.7386 -0.5046 1.7273 26.2373
#>
#> Coefficients:
#> Estimate Std. Error t value Pr(>|t|)
#> (Intercept) 36.341145 5.067492 7.171 2.73e-12 ***
#> crim -0.108413 0.032779 -3.307 0.001010 **
#> zn 0.045845 0.013523 3.390 0.000754 ***
#> chas 2.718716 0.854240 3.183 0.001551 **
#> nox -17.376023 3.535243 -4.915 1.21e-06 ***
#> rm 3.801579 0.406316 9.356 < 2e-16 ***
#> dis -1.492711 0.185731 -8.037 6.84e-15 ***
#> rad 0.299608 0.063402 4.726 3.00e-06 ***
#> tax -0.011778 0.003372 -3.493 0.000521 ***
#> ptratio -0.946525 0.129066 -7.334 9.24e-13 ***
#> black 0.009291 0.002674 3.475 0.000557 ***
#> lstat -0.522553 0.047424 -11.019 < 2e-16 ***
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#>
#> Residual standard error: 4.736 on 494 degrees of freedom
#> Multiple R-squared: 0.7406, Adjusted R-squared: 0.7348
#> F-statistic: 128.2 on 11 and 494 DF, p-value: < 2.2e-16
coef_tbl <- summary(lm_step)$coefficients %>%
as.data.frame() %>%
mutate(Variable = rownames(.)) %>%
select(Variable, everything())
rownames(coef_tbl) <- NULL
coef_tbl %>%
kable(digits = 4, caption = "Coeficientes del modelo seleccionado (stepwise AIC)") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| Variable | Estimate | Std. Error | t value | Pr(>|t|) |
|---|---|---|---|---|
| (Intercept) | 36.3411 | 5.0675 | 7.1714 | 0.0000 |
| crim | -0.1084 | 0.0328 | -3.3074 | 0.0010 |
| zn | 0.0458 | 0.0135 | 3.3902 | 0.0008 |
| chas | 2.7187 | 0.8542 | 3.1826 | 0.0016 |
| nox | -17.3760 | 3.5352 | -4.9151 | 0.0000 |
| rm | 3.8016 | 0.4063 | 9.3562 | 0.0000 |
| dis | -1.4927 | 0.1857 | -8.0370 | 0.0000 |
| rad | 0.2996 | 0.0634 | 4.7255 | 0.0000 |
| tax | -0.0118 | 0.0034 | -3.4925 | 0.0005 |
| ptratio | -0.9465 | 0.1291 | -7.3337 | 0.0000 |
| black | 0.0093 | 0.0027 | 3.4746 | 0.0006 |
| lstat | -0.5226 | 0.0474 | -11.0187 | 0.0000 |
Interpretación de dos coeficientes seleccionados:
rm: cada habitación adicional en promedio se asocia con un aumento de aproximadamente 3.8 miles de USD en el valor mediano, manteniendo el resto constante. Es el predictor con mayor efecto positivo.lstat: un punto porcentual más de población de bajos ingresos reducemedven cerca de 0.52 miles de USD. Esto refleja la relación inversa entre estatus socioeconómico y precio de vivienda.
vif_vals <- vif(lm_step)
vif_df <- data.frame(Variable = names(vif_vals), VIF = round(vif_vals, 2))
rownames(vif_df) <- NULL
vif_df %>%
arrange(desc(VIF)) %>%
ggplot(aes(x = reorder(Variable, VIF), y = VIF)) +
geom_col(fill = "#004B93", alpha = 0.75, width = 0.65) +
geom_hline(yintercept = 5, linetype = "dashed", color = "#5A6C7D") +
geom_hline(yintercept = 10, linetype = "dotted", color = "#333") +
coord_flip() +
labs(x = NULL, y = "VIF",
caption = "Línea discontinua: VIF = 5 | Línea punteada: VIF = 10") +
theme_minimal(base_size = 11)VIF > 5 sugiere multicolinealidad moderada y VIF > 10 multicolinealidad severa. Las variables
radytaxsuelen mostrar valores elevados por su fuerte asociación mutua; sin embargo, el modelo puede mantenerse si el objetivo es predicción más que inferencia causal.
par(mfrow = c(1, 2))
plot(lm_step, which = 1, col = "#004B93", pch = 16, cex = 0.6)
plot(lm_step, which = 3, col = "#004B93", pch = 16, cex = 0.6)#> Test de Breusch-Pagan
#> BP = 59.9073
#> p = 9.647e-09
El p-valor del test de Breusch-Pagan es inferior a 0.05, por lo que se rechaza la hipótesis nula de homocedasticidad. Esto indica que la varianza de los residuos no es constante, lo cual podría afectar la inferencia sobre los coeficientes.
cook_d <- cooks.distance(lm_step)
n <- nrow(Boston)
umbral <- 4 / n
data.frame(Obs = 1:n, Cook = cook_d) %>%
ggplot(aes(x = Obs, y = Cook)) +
geom_segment(aes(xend = Obs, yend = 0),
color = ifelse(cook_d > umbral, "#004B93", "#CBD5E0"), linewidth = 0.4) +
geom_point(color = ifelse(cook_d > umbral, "#004B93", "#CBD5E0"), size = 0.8) +
geom_hline(yintercept = umbral, linetype = "dashed", color = "#5A6C7D") +
labs(x = "Observación", y = "Distancia de Cook",
caption = paste0("Umbral 4/n = ", round(umbral, 4))) +
theme_minimal(base_size = 11)top5 <- sort(cook_d, decreasing = TRUE)[1:5]
data.frame(
Observación = as.integer(names(top5)),
Cook_D = round(top5, 5)
) %>%
kable(caption = "Las 5 observaciones con mayor distancia de Cook") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| Observación | Cook_D | |
|---|---|---|
| 369 | 369 | 0.16121 |
| 373 | 373 | 0.10858 |
| 365 | 365 | 0.07896 |
| 366 | 366 | 0.07400 |
| 370 | 370 | 0.06142 |
Las observaciones listadas arriba combinan residuos grandes y/o alto leverage, lo que les otorga una influencia desproporcionada sobre las estimaciones. Conviene verificar si corresponden a registros atípicos genuinos o a errores de medición.
PimaIndiansDiabetesEste dataset (mlbench) contiene 768 registros de mujeres
de la comunidad Pima con variables clínicas. La variable respuesta
diabetes indica presencia (pos) o ausencia (neg) de la
enfermedad.
#>
#> Call:
#> glm(formula = diabetes ~ ., family = binomial, data = PimaIndiansDiabetes)
#>
#> Coefficients:
#> Estimate Std. Error z value Pr(>|z|)
#> (Intercept) -8.4046964 0.7166359 -11.728 < 2e-16 ***
#> pregnant 0.1231823 0.0320776 3.840 0.000123 ***
#> glucose 0.0351637 0.0037087 9.481 < 2e-16 ***
#> pressure -0.0132955 0.0052336 -2.540 0.011072 *
#> triceps 0.0006190 0.0068994 0.090 0.928515
#> insulin -0.0011917 0.0009012 -1.322 0.186065
#> mass 0.0897010 0.0150876 5.945 2.76e-09 ***
#> pedigree 0.9451797 0.2991475 3.160 0.001580 **
#> age 0.0148690 0.0093348 1.593 0.111192
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#>
#> (Dispersion parameter for binomial family taken to be 1)
#>
#> Null deviance: 993.48 on 767 degrees of freedom
#> Residual deviance: 723.45 on 759 degrees of freedom
#> AIC: 741.45
#>
#> Number of Fisher Scoring iterations: 5
coef_pima <- summary(glm_pima)$coefficients %>%
as.data.frame() %>%
mutate(Variable = rownames(.), OR = exp(Estimate)) %>%
select(Variable, Estimate, OR, `Std. Error`, `z value`, `Pr(>|z|)`)
rownames(coef_pima) <- NULL
coef_pima %>%
kable(digits = 4,
col.names = c("Variable", "Log-odds", "OR", "Err. Std.", "z", "p-valor"),
caption = "Coeficientes del modelo logístico") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| Variable | Log-odds | OR | Err. Std. | z | p-valor |
|---|---|---|---|---|---|
| (Intercept) | -8.4047 | 0.0002 | 0.7166 | -11.7280 | 0.0000 |
| pregnant | 0.1232 | 1.1311 | 0.0321 | 3.8401 | 0.0001 |
| glucose | 0.0352 | 1.0358 | 0.0037 | 9.4814 | 0.0000 |
| pressure | -0.0133 | 0.9868 | 0.0052 | -2.5404 | 0.0111 |
| triceps | 0.0006 | 1.0006 | 0.0069 | 0.0897 | 0.9285 |
| insulin | -0.0012 | 0.9988 | 0.0009 | -1.3223 | 0.1861 |
| mass | 0.0897 | 1.0938 | 0.0151 | 5.9453 | 0.0000 |
| pedigree | 0.9452 | 2.5733 | 0.2991 | 3.1596 | 0.0016 |
| age | 0.0149 | 1.0150 | 0.0093 | 1.5929 | 0.1112 |
glucose: cada unidad adicional de glucosa incrementa el log-odds en 0.0352, lo que equivale a multiplicar los odds por 1.0358. Aunque el efecto unitario parece pequeño, el rango de glucosa es amplio (44–199), por lo que el impacto acumulado es considerable.mass(IMC): OR = 1.0938. Un punto más de IMC aumenta los odds de diabetes en un 9.38%.Respecto a la significancia: se compara cada p-valor contra \(\alpha = 0.05\).
dev_nula <- glm_pima$null.deviance
dev_resid <- glm_pima$deviance
pct_exp <- (1 - dev_resid / dev_nula) * 100
data.frame(
Indicador = c("Devianza nula (solo intercepto)", "Devianza residual (modelo completo)", "% Devianza explicada"),
Valor = c(round(dev_nula, 2), round(dev_resid, 2), paste0(round(pct_exp, 2), "%"))
) %>%
kable(caption = "Comparación de devianzas") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| Indicador | Valor |
|---|---|
| Devianza nula (solo intercepto) | 993.48 |
| Devianza residual (modelo completo) | 723.45 |
| % Devianza explicada | 27.18% |
El modelo explica aproximadamente un 27.2% de la devianza total. Este valor puede parecer modesto, pero es habitual en estudios biomédicos donde la variabilidad entre individuos es alta y factores no capturados (genéticos, ambientales) juegan un rol importante. La reducción es estadísticamente significativa, lo cual se confirma con el test de razón de verosimilitudes implícito en la diferencia de devianzas.
prob_pima <- predict(glm_pima, type = "response")
roc_pima <- roc(PimaIndiansDiabetes$diabetes, prob_pima, levels = c("neg", "pos"))
coords_opt <- coords(roc_pima, x = "best", best.method = "youden",
ret = c("threshold", "sensitivity", "specificity"))
roc_df <- data.frame(
FPR = 1 - roc_pima$specificities,
TPR = roc_pima$sensitivities
)
ggplot(roc_df, aes(x = FPR, y = TPR)) +
geom_line(color = "#004B93", linewidth = 1) +
geom_abline(slope = 1, intercept = 0, linetype = "dashed", color = "#CBD5E0") +
geom_point(aes(x = 1 - coords_opt$specificity, y = coords_opt$sensitivity),
color = "#5A6C7D", size = 3.5, shape = 18) +
annotate("text", x = 0.55, y = 0.3,
label = paste0("AUC = ", round(auc(roc_pima), 4)),
size = 4.5, color = "#004B93") +
annotate("text", x = 1 - coords_opt$specificity + 0.12,
y = coords_opt$sensitivity - 0.04,
label = paste0("Youden (", round(coords_opt$threshold, 3), ")"),
size = 3.2, color = "#5A6C7D") +
labs(x = "1 - Especificidad", y = "Sensibilidad") +
theme_minimal(base_size = 11)#> AUC: 0.8394
#> Punto de corte óptimo (Youden): 0.3537
#> Sensibilidad: 0.7388
#> Especificidad: 0.784
El AUC de 0.839 indica una capacidad discriminativa aceptable. El índice de Youden (\(J = \text{Sens} + \text{Esp} - 1\)) identifica el umbral que balancea mejor la detección de positivos y negativos. En este caso, el punto de corte óptimo resulta ser 0.354, distinto del 0.5 habitual, lo que refleja el desbalance entre clases y el trade-off inherente a la detección temprana.
umbral_opt <- coords_opt$threshold
pred_class <- factor(ifelse(prob_pima >= umbral_opt, "pos", "neg"), levels = c("neg", "pos"))
cm <- table(Predicho = pred_class, Real = PimaIndiansDiabetes$diabetes)as.data.frame(cm) %>%
ggplot(aes(x = Real, y = Predicho, fill = Freq)) +
geom_tile(color = "white", linewidth = 1.5) +
geom_text(aes(label = Freq), size = 7, fontface = "bold", color = "white") +
scale_fill_gradient(low = "#8DA0B0", high = "#004B93") +
labs(title = paste0("Matriz de confusión (umbral = ", round(umbral_opt, 4), ")")) +
theme_minimal(base_size = 11) +
theme(legend.position = "none",
axis.text = element_text(size = 11))TP <- cm["pos", "pos"]; TN <- cm["neg", "neg"]
FP <- cm["pos", "neg"]; FN <- cm["neg", "pos"]
sens <- TP / (TP + FN)
espec <- TN / (TN + FP)
acc <- (TP + TN) / sum(cm)
data.frame(
Métrica = c("Sensibilidad", "Especificidad", "Accuracy"),
Valor = round(c(sens, espec, acc), 4)
) %>%
kable(caption = "Métricas de clasificación") %>%
kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)| Métrica | Valor |
|---|---|
| Sensibilidad | 0.7388 |
| Especificidad | 0.7840 |
| Accuracy | 0.7682 |
En un problema de detección de enfermedades, generalmente conviene priorizar la sensibilidad por sobre la especificidad. Un falso negativo implica que un paciente diabético no recibe diagnóstico ni tratamiento oportuno, con riesgo de complicaciones como retinopatía, neuropatía o enfermedad cardiovascular. Un falso positivo, en cambio, se resuelve con exámenes confirmatorios adicionales. Por esta razón, un punto de corte menor a 0.5 (como el obtenido por Youden) puede ser clínicamente preferible.