La regresión logística es una de las herramientas más utilizadas en estadística y aprendizaje automático cuando la variable de respuesta es binaria. Históricamente, se popularizó a mediados del siglo XX en biostatística y ciencias sociales para modelar resultados como presencia/ausencia de enfermedades, éxito/fracaso en encuestas, y en investigación educativa, para predecir aprobaciones o aprendizajes.
En este mini-curso, profundizaremos en:
mtcars
).Finalmente, se proporcionan referencias bibliográficas recomendadas para quienes deseen profundizar teóricamente.
Objetivo: Que el lector, al terminar este documento, pueda aplicar regresión logística en la práctica, interpretar coeficientes y validar modelos con confianza.
El problema de la predicción binaria
Fundamentos matemáticos
Simulaciones básicas en R
Aplicaciones en aula
mtcars
para predecir
transmisióniris
(ejercicio adicional)Diagnóstico y validación
Visualización avanzada
plotly
Resumen de pasos prácticos
Conclusiones y ejercicios adicionales
Referencias bibliográficas
En regresión clásica (lineal), la variable dependiente \(Y\) es continua, pero cuando \(Y\) solo puede tomar valores en \(\{0,1\}\), el modelo lineal ordinario presenta varias limitaciones:
Predicciones fuera de \([0,1]\): La ecuación lineal
\[ \hat{Y} \;=\; \beta_0 + \beta_1 X \]
puede generar valores negativos o mayores que uno, que no tienen sentido como probabilidad.
Suposición de varianza constante: En regresión lineal se asume homocedasticidad (\(\mathrm{Var}(Y \mid X) = \sigma^2\)), pero una variable binaria no cumple esta propiedad.
Distribución normal de errores: El modelo logístico asume una distribución de Bernoulli para los resultados, no normal.
Definición formal: Si \(Y\) es una variable binaria con \(P(Y=1) = p\) y \(P(Y=0) = 1 - p\), la regresión logística busca modelar la función \(p(X)\) en función de los predictores \(X = (X_1, \dots, X_k)\).
Ejemplo en contexto educativo: Supongamos que tenemos los datos de 200 estudiantes y queremos predecir si un alumno aprueba (\(Y = 1\)) o reprueba (\(Y = 0\)) un examen. Los predictores podrían ser:
Con regresión logística, interceptamos la relación entre \(\mathbf{X}\) y \(p = P(Y = 1 \mid X)\), asegurando que \(0 \le p \le 1\).
La base de la regresión logística es la función sigmoidea, que comprime valores reales en el rango \((0,1)\). Está definida como:
\[ \sigma(z) \;=\; \frac{1}{1 + e^{-z}}, \]
donde \(z\) puede ser cualquier número real.
Propiedades clave:
A continuación generamos una gráfica de la función sigmoidea para visualizar su forma:
x <- seq(-10, 10, length.out = 200)
y <- 1 / (1 + exp(-x))
plot(x, y, type = 'l', lwd = 2, col = 'blue',
main = 'Función sigmoidea (Logística)',
xlab = 'z = β₀ + β₁ x₁ + … + βₖ xₖ',
ylab = 'σ(z)')
abline(h = c(0, 0.5, 1), lty = 2, col = 'gray')
text(-10, 0.05, '0', adj = c(0, 0))
text(0, 0.55, '0.5', adj = c(0.5, 0))
text(10, 0.95, '1', adj = c(1, 0))
Interpretación: Si definimos el predictor lineal
\[ \eta_i \;=\; \beta_0 + \beta_1 X_{i1} + \dots + \beta_k X_{ik}, \]
entonces la probabilidad de éxito es:
\[ p_i = P(Y_i = 1 \mid \mathbf{X}_i) = \frac{1}{1 + e^{-\eta_i}}. \]
Para valores muy negativos de \(\eta_i\), \(p_i \approx 0\); para valores muy grandes, \(p_i \approx 1\). Esta flexibilidad evita problemas de predicción fuera del rango.
\(\eta\) (z) | \(\sigma(z)\) (p) |
---|---|
\(-3.0\) | \(0.0474\) |
\(-2.0\) | \(0.1192\) |
\(-1.0\) | \(0.2689\) |
\(0.0\) | \(0.5000\) |
\(1.0\) | \(0.7311\) |
\(2.0\) | \(0.8808\) |
\(3.0\) | \(0.9526\) |
Esta tabla ilustra que, por ejemplo, \(\eta = 1\) equivale a una probabilidad \(p \approx 0{,}73\).
Para trabajar con la función sigmoidea, se introduce la razón de probabilidades (odds) y el logit:
Odds:
\[ \text{odds}_i = \frac{p_i}{1 - p_i}. \]
Logit:
\[ \text{logit}(p_i) = \ln\bigl(\tfrac{p_i}{1 - p_i}\bigr). \]
En el modelo de regresión logística:
\[ \text{logit}(p_i) = \beta_0 + \beta_1 X_{i1} + \cdots + \beta_k X_{ik}. \]
De aquí se deduce que:
\(\beta_j\) es el cambio en log-odds asociado a un incremento unitario en \(X_j\), manteniendo los demás predictores constantes.
\(e^{\beta_j}\) es el odds ratio (OR) asociado a \(X_j\), es decir:
\[ OR_j = e^{\beta_j} = \frac{\text{odds}(p_i \mid X_j + 1)}{\text{odds}(p_i \mid X_j)}. \]
Ejemplo interpretativo: Si \(\beta_1 = 0{,}693\), entonces \(OR_1 = e^{0{,}693} \approx 2\). Con cada hora adicional de estudio, las probabilidades de aprobar se duplican (se multiplican por 2).
Para estimar \(\boldsymbol{\beta}\), usamos Máxima Verosimilitud. Suponiendo que las observaciones \(Y_i\) son independientes y \(Y_i \sim \mathrm{Bernoulli}(p_i)\):
Función de verosimilitud:
\[ L(\boldsymbol{\beta}) = \prod_{i=1}^n p_i^{y_i} (1 - p_i)^{1 - y_i}, \]
donde
\[ p_i = \sigma\bigl(\beta_0 + \beta_1 X_{i1} + \dots + \beta_k X_{ik}\bigr). \]
Log-verosimilitud (más conveniente numéricamente):
\[ \ell(\boldsymbol{\beta}) = \sum_{i=1}^n \Bigl[\,y_i \ln(p_i) + (1 - y_i) \ln(1 - p_i)\Bigr]. \]
R realiza la optimización para \(\hat{\boldsymbol{\beta}} = \arg\max_{\boldsymbol{\beta}} \ell(\boldsymbol{\beta})\) sin que el usuario tenga que programar algoritmos numéricos.
Aspecto | Regresión Lineal | Regresión Logística |
---|---|---|
Variable dependiente (\(Y\)) | Continua (Normal con varianza constante) | Binaria (Bernoulli) |
Función de enlace | Identidad: \(\mathbb{E}[Y \mid X] = \beta_0 + \beta_1 X\) | Logit: \(\ln\bigl(p/(1 - p)\bigr) = \beta_0 + \beta_1 X\) |
Rango de predicción | \((-\infty, +\infty)\) (no apropiado para probabilidades) | \([0,1]\) (gracias a la función sigmoidea) |
Método de estimación | Mínimos cuadrados ordinarios (OLS) | Máxima verosimilitud |
Interpretación de coeficientes | Cambio en \(Y\) por unidad en \(X\) (lineal) | Cambio en log-odds / odds ratio por unidad en \(X\) |
En esta sección, crearemos datos simulados para entender de forma práctica cómo funciona la regresión logística, desde la generación de datos hasta la interpretación de resultados.
Supongamos que queremos simular \(n = 200\) estudiantes. Definimos los parámetros:
Entonces:
Generamos las horas de estudio como \(X_i \sim \mathcal{N}(5,\,1{,}5^2).\)
Calculamos la probabilidad de aprobar:
\[ p_i = \frac{1}{1 + \exp\bigl(-\bigl(-3 + 0{,}8 \times \text{horas}_i\bigr)\bigr)}. \]
Generamos \(Y_i \sim \mathrm{Bernoulli}(p_i)\).
set.seed(123)
n <- 200
horas <- rnorm(n, mean = 5, sd = 1.5) # Horas promedio de estudio
prob <- 1 / (1 + exp(-(-3 + 0.8 * horas))) # Función sigmoidea
aprob <- rbinom(n, 1, prob) # Variable binaria (0/1)
datos1 <- tibble(horas, prob, aprob)
# Mostrar las primeras filas en una tabla
knitr::kable(head(datos1, 10),
caption = 'Primeras 10 observaciones simuladas')
horas | prob | aprob |
---|---|---|
4.159287 | 0.5811337 | 0 |
4.654734 | 0.6734404 | 1 |
7.338063 | 0.9463662 | 1 |
5.105763 | 0.7473654 | 1 |
5.193932 | 0.7604495 | 1 |
7.572597 | 0.9551300 | 1 |
5.691374 | 0.8253609 | 1 |
3.102408 | 0.3733028 | 0 |
3.969721 | 0.5438313 | 1 |
4.331507 | 0.6142484 | 0 |
hist(datos1$horas, breaks = 20, col = 'lightblue', border = 'white',
main = 'Distribución de horas de estudio', xlab = 'Horas', ylab = 'Frecuencia')
ggplot(datos1, aes(x = horas, y = aprob)) +
geom_jitter(height = 0.05, alpha = 0.6, color = 'darkblue') +
geom_smooth(method = 'glm', method.args = list(family = binomial),
se = FALSE, size = 1, color = 'red') +
labs(title = 'Probabilidad de aprobar vs Horas de estudio',
x = 'Horas de estudio',
y = 'Resultado (0 = No, 1 = Sí)') +
theme_minimal()
Nota: La función
geom_smooth(method = 'glm', method.args = list(family = binomial))
ajusta automáticamente un modelo logístico y muestra la curva de probabilidad.
m1 <- glm(aprob ~ horas, data = datos1, family = binomial)
summary(m1)
##
## Call:
## glm(formula = aprob ~ horas, family = binomial, data = datos1)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -3.0094 0.7208 -4.175 2.98e-05 ***
## horas 0.8267 0.1564 5.287 1.24e-07 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 242.63 on 199 degrees of freedom
## Residual deviance: 204.44 on 198 degrees of freedom
## AIC: 208.44
##
## Number of Fisher Scoring iterations: 5
# Odds ratios e intervalos de confianza
or_ci <- exp(cbind(Estimate = coef(m1), confint(m1)))
knitr::kable(or_ci, digits = 3,
caption = 'Odds Ratios e Intervalos de Confianza')
Estimate | 2.5 % | 97.5 % | |
---|---|---|---|
(Intercept) | 0.049 | 0.011 | 0.191 |
horas | 2.286 | 1.711 | 3.166 |
Interpretación:
coef0 <- coef(m1)[1]
coef1 <- coef(m1)[2]
curve(expr = 1 / (1 + exp(-(coef0 + coef1 * x))),
from = min(datos1$horas) - 1,
to = max(datos1$horas) + 1,
ylab = 'Probabilidad de aprobar',
xlab = 'Horas de estudio',
col = 'darkgreen',
lwd = 2)
points(datos1$horas, datos1$aprob, pch = 19, cex = 0.5, col = 'darkblue')
legend('bottomright', legend = c('Datos', 'Curva logística'),
col = c('darkblue','darkgreen'), pch = c(19, NA),
lty = c(NA,1), bty = 'n')
horas
a 2. Observa el efecto en la dispersión de puntos y
en la precisión del modelo.valores <- tibble(horas = c(2,4,6,8)) %>%
mutate(p_teorica = 1 / (1 + exp(-(-3 + 0.8 * horas))),
p_modelo = predict(m1, newdata = ., type = 'response'))
knitr::kable(valores, digits = 3,
caption = 'Comparación Teórica vs Modelo')
horas | p_teorica | p_modelo |
---|---|---|
2 | 0.198 | 0.205 |
4 | 0.550 | 0.574 |
6 | 0.858 | 0.876 |
8 | 0.968 | 0.974 |
Ahora analizaremos un caso donde el predictor principal es categórico (asistencia: sí/no). Simularemos \(n = 300\) estudiantes:
set.seed(456)
n <- 300
asistio <- rbinom(n, 1, 0.65) # 1 = asistió, 0 = no asistió
# Definimos probabilidad para cada nivel de 'asistio'
prob2 <- 1 / (1 + exp(-(-1 + 1.5 * asistio)))
aprob2 <- rbinom(n, 1, prob2)
datos2 <- tibble(
asistio = factor(asistio, levels = c(0, 1), labels = c('No', 'Sí')),
prob = prob2,
aprob = aprob2
)
knitr::kable(head(datos2, 10),
caption = 'Primeras 10 observaciones simuladas (asistencia)')
asistio | prob | aprob |
---|---|---|
Sí | 0.6224593 | 0 |
Sí | 0.6224593 | 1 |
No | 0.2689414 | 0 |
No | 0.2689414 | 0 |
No | 0.2689414 | 0 |
Sí | 0.6224593 | 0 |
Sí | 0.6224593 | 1 |
Sí | 0.6224593 | 1 |
Sí | 0.6224593 | 1 |
Sí | 0.6224593 | 0 |
datos2 %>%
group_by(asistio) %>%
summarize(
n = n(),
prop_aprob = mean(aprob),
.groups = 'drop'
) %>%
knitr::kable(digits = 3,
caption = 'Proporción de aprobación por asistencia')
asistio | n | prop_aprob |
---|---|---|
No | 111 | 0.297 |
Sí | 189 | 0.582 |
m2 <- glm(aprob ~ asistio, data = datos2, family = binomial)
summary(m2)
##
## Call:
## glm(formula = aprob ~ asistio, family = binomial, data = datos2)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -0.8602 0.2077 -4.142 3.44e-05 ***
## asistioSí 1.1912 0.2547 4.677 2.91e-06 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 415.23 on 299 degrees of freedom
## Residual deviance: 392.00 on 298 degrees of freedom
## AIC: 396
##
## Number of Fisher Scoring iterations: 4
res2 <- tidy(m2, exponentiate = TRUE, conf.int = TRUE)
knitr::kable(res2, digits = 3,
caption = 'Resultados del modelo logístico (asistencia)')
term | estimate | std.error | statistic | p.value | conf.low | conf.high |
---|---|---|---|---|---|---|
(Intercept) | 0.423 | 0.208 | -4.142 | 0 | 0.278 | 0.629 |
asistioSí | 3.291 | 0.255 | 4.677 | 0 | 2.012 | 5.472 |
datos2 %>%
group_by(asistio) %>%
summarize(p_aprob = mean(aprob), .groups = 'drop') %>%
ggplot(aes(x = asistio, y = p_aprob, fill = asistio)) +
geom_col() +
scale_y_continuous(labels = scales::percent_format(scale = 1)) +
labs(title = 'Tasa de aprobación según asistencia',
x = 'Asistencia completa',
y = 'Proporción de aprobados') +
theme_minimal() +
theme(legend.position = 'none')
Objetivo: Extender el modelo a tres niveles de asistencia: “Nunca”, “A veces” y “Siempre”.
asistio3
con valores 0, 1, 2 según
probabilidades 0.1, 0.5 y 0.4.asistio3
en factor con etiquetas
c("Nunca","A veces","Siempre")
.set.seed(987)
n <- 300
asistio3 <- sample(c(0,1,2), size = n, replace = TRUE, prob = c(0.1, 0.5, 0.4))
prob3 <- 1 / (1 + exp(-(-1 + 0.5 * (asistio3 == 1) + 1.2 * (asistio3 == 2))))
aprob3 <- rbinom(n, 1, prob3)
datos2b <- tibble(
asistio3 = factor(asistio3, levels = c(0,1,2), labels = c('Nunca','A veces','Siempre')),
prob3,
aprob3
)
# Ajuste modelo multinivel
m2b <- glm(aprob3 ~ asistio3, data = datos2b, family = binomial)
tidy(m2b, exponentiate = TRUE, conf.int = TRUE) %>%
knitr::kable(digits = 3,
caption = 'Resultados del modelo logístico con 3 niveles de asistencia')
term | estimate | std.error | statistic | p.value | conf.low | conf.high |
---|---|---|---|---|---|---|
(Intercept) | 0.440 | 0.362 | -2.269 | 0.023 | 0.208 | 0.872 |
asistio3A veces | 1.136 | 0.399 | 0.320 | 0.749 | 0.530 | 2.567 |
asistio3Siempre | 2.146 | 0.411 | 1.858 | 0.063 | 0.978 | 4.959 |
Combinaremos horas de estudio, asistencia (binaria) y calificación de tarea previa para simular \(n = 500\) estudiantes.
Generamos las variables:
horas
\(\sim\)
Normal(5, 1.3).asistio2
\(\sim\)
Bernoulli(0.7).tarea_prev
\(\sim\)
Normal(70, 10).Definimos el predictor lineal:
\[ \eta_i = -6 + 0.6 \times \text{horas}_i + 1.2 \times \text{asistio2}_i + 0.04 \times \text{tarea\_prev}_i. \]
Calculamos \(p_i = 1 / [1 + \exp(-\eta_i)]\) y simulamos \(Y_i \sim \mathrm{Bernoulli}(p_i)\).
set.seed(789)
n <- 500
horas <- rnorm(n, 5, 1.3)
asistio2 <- rbinom(n, 1, 0.7)
tarea_prev <- rnorm(n, 70, 10)
lin_pred <- -6 + 0.6 * horas + 1.2 * asistio2 + 0.04 * tarea_prev
prob3 <- 1 / (1 + exp(-lin_pred))
aprob3 <- rbinom(n, 1, prob3)
datos3 <- tibble(
horas,
asistio = factor(asistio2, levels = c(0,1), labels = c('No','Sí')),
tarea_prev,
prob3,
aprob = aprob3
)
knitr::kable(head(datos3, 10),
caption = 'Primeras 10 observaciones simuladas (modelo múltiple)')
horas | asistio | tarea_prev | prob3 | aprob |
---|---|---|---|---|
5.681326 | Sí | 68.86410 | 0.7963052 | 0 |
2.061002 | Sí | 65.45533 | 0.2798540 | 0 |
4.974416 | Sí | 76.06943 | 0.7733679 | 1 |
5.238082 | No | 70.32088 | 0.4889229 | 0 |
4.530243 | Sí | 69.67922 | 0.6693650 | 1 |
4.370171 | No | 63.67319 | 0.3034401 | 1 |
4.133793 | Sí | 57.62722 | 0.4963413 | 0 |
4.773190 | Sí | 61.46886 | 0.6277715 | 1 |
3.685752 | Sí | 44.68542 | 0.3097835 | 0 |
5.961605 | Sí | 58.78982 | 0.7555722 | 0 |
datos3 %>%
summarize(
media_horas = mean(horas), sd_horas = sd(horas),
media_tarea = mean(tarea_prev), sd_tarea = sd(tarea_prev),
prop_asisten = mean(asistio == 'Sí'), prop_aproban = mean(aprob)
) %>%
knitr::kable(digits = 3,
caption = 'Estadísticos resumidos de variables simuladas')
media_horas | sd_horas | media_tarea | sd_tarea | prop_asisten | prop_aproban |
---|---|---|---|---|---|
4.999 | 1.306 | 69.515 | 10.249 | 0.708 | 0.632 |
m3 <- glm(aprob ~ horas + asistio + tarea_prev, data = datos3, family = binomial)
summary(m3)
##
## Call:
## glm(formula = aprob ~ horas + asistio + tarea_prev, family = binomial,
## data = datos3)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -5.185832 0.863986 -6.002 1.95e-09 ***
## horas 0.515869 0.082985 6.216 5.09e-10 ***
## asistioSí 0.923692 0.215565 4.285 1.83e-05 ***
## tarea_prev 0.037030 0.009912 3.736 0.000187 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 657.88 on 499 degrees of freedom
## Residual deviance: 583.68 on 496 degrees of freedom
## AIC: 591.68
##
## Number of Fisher Scoring iterations: 3
res3 <- tidy(m3, exponentiate = TRUE, conf.int = TRUE)
knitr::kable(res3, digits = 3,
caption = 'Odds Ratios e Intervalos de Confianza (modelo múltiple)')
term | estimate | std.error | statistic | p.value | conf.low | conf.high |
---|---|---|---|---|---|---|
(Intercept) | 0.006 | 0.864 | -6.002 | 0 | 0.001 | 0.029 |
horas | 1.675 | 0.083 | 6.216 | 0 | 1.429 | 1.979 |
asistioSí | 2.519 | 0.216 | 4.285 | 0 | 1.654 | 3.855 |
tarea_prev | 1.038 | 0.010 | 3.736 | 0 | 1.018 | 1.058 |
Interpretación:
horas
indica el cambio en la razón de probabilidades por
cada hora adicional.asistio = Sí
vs No
indica cuántas veces se
multiplican las probabilidades si un estudiante asistió a clases en
comparación con no asistir.tarea_prev
significa el efecto de un punto adicional en la
calificación de tarea previa.Para entender mejor la interacción de predictores, creamos una tabla
bidimensional de probabilidades promedio agrupadas por categorías de
asistio
y cuartiles en tarea_prev
:
datos3 %>%
mutate(corte_tarea = ntile(tarea_prev, 4)) %>%
group_by(asistio, corte_tarea) %>%
summarize(
p_empirica = mean(aprob),
.groups = 'drop'
) %>%
pivot_wider(names_from = corte_tarea, values_from = p_empirica) %>%
knitr::kable(digits = 3,
caption = 'Probabilidad de aprobar por asistencia y cuartiles de tarea previa')
asistio | 1 | 2 | 3 | 4 |
---|---|---|---|---|
No | 0.389 | 0.474 | 0.424 | 0.641 |
Sí | 0.573 | 0.713 | 0.717 | 0.767 |
roc3 <- roc(datos3$aprob, fitted(m3))
plot(roc3, print.auc = TRUE, main = 'Curva ROC - Modelo Múltiple')
Interpretación: El valor de AUC (Área Bajo la Curva) cuantifica la capacidad discriminativa del modelo. Un AUC cercano a 1 indica excelente separación entre ambos grupos.
# Predicciones con umbral 0.5
y_pred <- if_else(fitted(m3) >= 0.5, 1, 0)
mc3 <- table(Real = datos3$aprob, Predicho = y_pred)
knitr::kable(mc3,
caption = 'Matriz de Confusión (umbral = 0.5)')
0 | 1 | |
---|---|---|
0 | 71 | 113 |
1 | 39 | 277 |
# Exactitud global
acc3 <- mean(y_pred == datos3$aprob)
paste('Exactitud:', round(acc3, 3))
## [1] "Exactitud: 0.696"
Para explorar distintos umbrales y hallar el óptimo:
calc_metrics <- function(thr) {
pred_thr <- if_else(fitted(m3) >= thr, 1, 0)
tp <- sum(pred_thr == 1 & datos3$aprob == 1)
tn <- sum(pred_thr == 0 & datos3$aprob == 0)
fp <- sum(pred_thr == 1 & datos3$aprob == 0)
fn <- sum(pred_thr == 0 & datos3$aprob == 1)
sensibilidad <- tp / (tp + fn)
especificidad <- tn / (tn + fp)
tibble(Umbral = thr, Sensibilidad = sensibilidad, Especificidad = especificidad)
}
bind_rows(calc_metrics(0.4), calc_metrics(0.5), calc_metrics(0.6)) %>%
knitr::kable(digits = 3,
caption = 'Sensibilidad y Especificidad para distintos umbrales')
Umbral | Sensibilidad | Especificidad |
---|---|---|
0.4 | 0.940 | 0.266 |
0.5 | 0.877 | 0.386 |
0.6 | 0.728 | 0.576 |
En el aula, es útil mostrar ejemplos con datasets conocidos de R y ejercicios prácticos. A continuación, mostramos dos casos:
mtcars
para predecir
transmisiónEl dataset mtcars
contiene características de
automóviles. Queremos predecir si un auto tiene transmisión
manual (\(am = 1\)) o
automática (\(am =
0\)) usando hp
(caballos de fuerza) y
wt
(peso en miles de libras).
data(mtcars)
mt <- mtcars %>%
mutate(am = factor(am, labels = c('Auto', 'Manual')))
# Estadísticos descriptivos
mt %>%
group_by(am) %>%
summarize(media_hp = mean(hp), media_wt = mean(wt), .groups = 'drop') %>%
knitr::kable(digits = 2,
caption = 'Resumen de HP y WT por tipo de transmisión')
am | media_hp | media_wt |
---|---|---|
Auto | 160.26 | 3.77 |
Manual | 126.85 | 2.41 |
hp10 <- mt$hp / 10 # para interpretar coeficiente por cada 10 HP
mod_mtcars <- glm(am ~ hp10 + wt, data = mt, family = binomial)
summary(mod_mtcars)
##
## Call:
## glm(formula = am ~ hp10 + wt, family = binomial, data = mt)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) 18.8663 7.4436 2.535 0.01126 *
## hp10 0.3626 0.1773 2.044 0.04091 *
## wt -8.0835 3.0687 -2.634 0.00843 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 43.230 on 31 degrees of freedom
## Residual deviance: 10.059 on 29 degrees of freedom
## AIC: 16.059
##
## Number of Fisher Scoring iterations: 8
# Odds ratios e intervalos de confianza
res_mtcars <- exp(cbind(Estimate = coef(mod_mtcars), confint(mod_mtcars)))
knitr::kable(res_mtcars, digits = 3,
caption = 'Odds Ratios e IC del modelo mtcars')
Estimate | 2.5 % | 97.5 % | |
---|---|---|---|
(Intercept) | 1.561455e+08 | 4342.767 | 4.083258e+17 |
hp10 | 1.437000e+00 | 1.112 | 2.473000e+00 |
wt | 0.000000e+00 | 0.000 | 2.300000e-02 |
Interpretación:
hp10
: efecto de 10 caballos de fuerza
adicionales.wt
: cambio en odds por cada 1000 libras
adicionales.# Secuencia de valores de hp manteniendo wt en su media
grid_mtcars <- tibble(
hp10 = seq(min(mt$hp/10), max(mt$hp/10), length.out = 100),
wt = mean(mt$wt)
)
grid_mtcars$prob_manual <- predict(mod_mtcars, newdata = grid_mtcars, type = 'response')
ggplot(grid_mtcars, aes(x = hp10 * 10, y = prob_manual)) +
geom_line(color = 'blue', size = 1) +
labs(title = 'Probabilidad de Transmisión Manual vs HP',
x = 'Caballos de Fuerza (HP)',
y = 'Probabilidad Manual') +
theme_minimal()
Imaginemos un escenario realista: tenemos un curso y recolectamos las siguientes variables:
asistencias
: número de clases asistidas por el
estudiante (0 a 30).nota_parcial
: calificación en el primer examen parcial
(0 a 100).aprueba
: \(1\) si el
estudiante aprobó el curso (evaluación final), \(0\) en otro caso.Vamos a simular datos y luego ajustar un modelo logístico.
set.seed(100)
n <- 150
asistencias <- rpois(n, lambda = 25) # Promedio de 25 clases asistidas
nota_parcial <- rnorm(n, mean = 75, sd = 10)
lin_pred_aula <- -5 + 0.1 * asistencias + 0.05 * nota_parcial
prob_aula <- 1 / (1 + exp(-lin_pred_aula))
aprueba <- rbinom(n, 1, prob_aula)
datos_aula <- tibble(asistencias, nota_parcial, prob_aula, aprueba)
knitr::kable(head(datos_aula, 10), digits = 3,
caption = 'Primeras 10 observaciones simuladas (ejercicio aula)')
asistencias | nota_parcial | prob_aula | aprueba |
---|---|---|---|
22 | 62.603 | 0.582 | 1 |
17 | 80.899 | 0.678 | 1 |
29 | 76.240 | 0.847 | 0 |
25 | 69.763 | 0.729 | 0 |
26 | 81.202 | 0.840 | 1 |
22 | 82.082 | 0.787 | 1 |
27 | 74.068 | 0.803 | 1 |
23 | 72.048 | 0.711 | 1 |
27 | 64.142 | 0.712 | 1 |
27 | 68.752 | 0.757 | 0 |
datos_aula %>%
summarize(
media_asist = mean(asistencias), sd_asist = sd(asistencias),
media_nota = mean(nota_parcial), sd_nota = sd(nota_parcial),
prop_aprob = mean(aprueba)
) %>%
knitr::kable(digits = 3,
caption = 'Estadísticos resumidos (ejercicio aula)')
media_asist | sd_asist | media_nota | sd_nota | prop_aprob |
---|---|---|---|---|
24.647 | 4.695 | 75.094 | 9.912 | 0.753 |
mod_aula <- glm(aprueba ~ asistencias + nota_parcial,
data = datos_aula, family = binomial)
summary(mod_aula)
##
## Call:
## glm(formula = aprueba ~ asistencias + nota_parcial, family = binomial,
## data = datos_aula)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -4.04948 1.86441 -2.172 0.0299 *
## asistencias 0.05466 0.04391 1.245 0.2132
## nota_parcial 0.05189 0.02058 2.522 0.0117 *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 167.59 on 149 degrees of freedom
## Residual deviance: 159.14 on 147 degrees of freedom
## AIC: 165.14
##
## Number of Fisher Scoring iterations: 4
# Odds ratios e intervalos de confianza
res_aula <- exp(cbind(Estimate = coef(mod_aula), confint(mod_aula)))
knitr::kable(res_aula, digits = 3,
caption = 'Odds Ratios e IC (ejercicio aula)')
Estimate | 2.5 % | 97.5 % | |
---|---|---|---|
(Intercept) | 0.017 | 0.000 | 0.617 |
asistencias | 1.056 | 0.971 | 1.154 |
nota_parcial | 1.053 | 1.013 | 1.099 |
Interpretación:
asistencias
: cambio en odds de aprobar por cada
clase adicional.nota_parcial
: cambio en odds por cada punto
adicional en la calificación.# Estudiantes hipotéticos:
evaluar <- tibble(
asistencias = c(20, 30, 15),
nota_parcial = c(80, 90, 60)
)
evaluar$prob <- predict(mod_aula, newdata = evaluar, type = 'response')
knitr::kable(evaluar, digits = 3,
caption = 'Probabilidades predichas para ejemplos específicos')
asistencias | nota_parcial | prob |
---|---|---|
20 | 80 | 0.768 |
30 | 90 | 0.906 |
15 | 60 | 0.471 |
iris
Cargar iris
y crear una variable binaria:
\[ \text{es\_setosa} = \begin{cases} 1, & \text{si Species == 'setosa'};\\ 0, & \text{en otro caso}. \end{cases} \]
Ajustar el modelo
\[ \texttt{glm(es\_setosa ~ Sepal.Length + Sepal.Width, data = iris2, family = binomial)}. \]
Interpretar coeficientes y calcular OR.
Graficar curvas de probabilidad para rangos de
Sepal.Length
y Sepal.Width
.
# Paso 1: Preparar datos
iris2 <- iris %>%
mutate(es_setosa = if_else(Species == 'setosa', 1, 0))
# Paso 2: Ajuste del modelo
mod_iris <- glm(es_setosa ~ Sepal.Length + Sepal.Width,
data = iris2, family = binomial)
summary(mod_iris)
##
## Call:
## glm(formula = es_setosa ~ Sepal.Length + Sepal.Width, family = binomial,
## data = iris2)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) 437.2 128737.9 0.003 0.997
## Sepal.Length -163.4 45394.8 -0.004 0.997
## Sepal.Width 137.9 44846.1 0.003 0.998
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 1.9095e+02 on 149 degrees of freedom
## Residual deviance: 2.7060e-08 on 147 degrees of freedom
## AIC: 6
##
## Number of Fisher Scoring iterations: 25
res_iris <- exp(cbind(Estimate = coef(mod_iris), confint(mod_iris)))
knitr::kable(res_iris, digits = 3,
caption = 'Odds Ratios e IC (iris)')
Estimate | 2.5 % | 97.5 % | |
---|---|---|---|
(Intercept) | 7.547721e+189 | 0 | Inf |
Sepal.Length | 0.000000e+00 | 0 | Inf |
Sepal.Width | 8.025273e+59 | 0 | Inf |
Una vez ajustado un modelo, es crucial evaluar su desempeño y verificar supuestos.
La matriz de confusión compara valores observados vs predichos usando un umbral de probabilidad (p. ej., 0{,}5). Se calculan:
Métricas:
# Ejemplo usando mod_aula
y_prob <- predict(mod_aula, type = 'response')
y_pred <- if_else(y_prob >= 0.5, 1, 0)
mc <- table(Observado = datos_aula$aprueba, Predicho = y_pred)
knitr::kable(mc, caption = 'Matriz de Confusión (umbral = 0.5)')
0 | 1 | |
---|---|---|
0 | 4 | 33 |
1 | 1 | 112 |
# Calcular métricas
tp <- mc[2,2]; tn <- mc[1,1]; fp <- mc[1,2]; fn <- mc[2,1]
exactitud <- (tp + tn) / sum(mc)
sensibilidad <- tp / (tp + fn)
especificidad <- tn / (tn + fp)
tibble(Métrica = c('Exactitud','Sensibilidad','Especificidad'),
Valor = c(exactitud, sensibilidad, especificidad)) %>%
knitr::kable(digits = 3,
caption = 'Métricas de desempeño')
Métrica | Valor |
---|---|
Exactitud | 0.773 |
Sensibilidad | 0.991 |
Especificidad | 0.108 |
Para determinar el umbral óptimo, evaluamos sensibilidad y especificidad en varios puntos:
nums <- seq(0.1, 0.9, by = 0.1)
metrics <- map_dfr(nums, function(u) {
pred_thr <- if_else(y_prob >= u, 1, 0)
tp <- sum(pred_thr == 1 & datos_aula$aprueba == 1)
tn <- sum(pred_thr == 0 & datos_aula$aprueba == 0)
fp <- sum(pred_thr == 1 & datos_aula$aprueba == 0)
fn <- sum(pred_thr == 0 & datos_aula$aprueba == 1)
sensibilidad <- tp / (tp + fn)
especificidad <- tn / (tn + fp)
tibble(Umbral = u, Sensibilidad = sensibilidad, Especificidad = especificidad)
})
knitr::kable(metrics, digits = 3,
caption = 'Sensibilidad y Especificidad en diferentes umbrales')
Umbral | Sensibilidad | Especificidad |
---|---|---|
0.1 | 1.000 | 0.000 |
0.2 | 1.000 | 0.000 |
0.3 | 1.000 | 0.000 |
0.4 | 1.000 | 0.027 |
0.5 | 0.991 | 0.108 |
0.6 | 0.938 | 0.162 |
0.7 | 0.761 | 0.378 |
0.8 | 0.407 | 0.703 |
0.9 | 0.053 | 1.000 |
La Curva ROC (Receiver Operating Characteristic) muestra la relación entre la tasa de verdaderos positivos (sensibilidad) y la tasa de falsos positivos \((1 - \text{especificidad})\) para todos los umbrales posibles.
roc_obj <- roc(datos_aula$aprueba, y_prob)
plot(roc_obj, print.auc = TRUE, main = 'Curva ROC (Ejercicio Aula)')
AUC (Área Bajo la Curva) mide la capacidad global del modelo para distinguir entre clases.
En modelos lineales, usamos \(R^2\) para medir la proporción de varianza explicada. En regresión logística, empleamos alternativas:
Deviance:
Pseudo-\(R^2\) de McFadden:
\[ R^2_{\text{McF}} = 1 - \frac{\ell(\hat{\boldsymbol{\beta}})}{\ell(\hat{\boldsymbol{\beta}}_0)}, \]
donde \(\ell(\hat{\boldsymbol{\beta}})\) es la log-verosimilitud del modelo completo y \(\ell(\hat{\boldsymbol{\beta}}_0)\) la del modelo solo con intercepto.
ll_full <- logLik(mod_aula)
mod_null <- update(mod_aula, . ~ 1)
ll_null <- logLik(mod_null)
pseudoR2 <- 1 - as.numeric(ll_full / ll_null)
pseudoR2
## [1] 0.05044384
# Prueba de razón de verosimilitud entre modelos
test_lr <- anova(mod_null, mod_aula, test = 'Chisq')
test_lr
## Analysis of Deviance Table
##
## Model 1: aprueba ~ 1
## Model 2: aprueba ~ asistencias + nota_parcial
## Resid. Df Resid. Dev Df Deviance Pr(>Chi)
## 1 149 167.59
## 2 147 159.14 2 8.454 0.0146 *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
par(mfrow = c(1,2))
# Residuos deviance vs fitted
def_resid <- residuals(mod_aula, type = 'deviance')
fitted_vals <- fitted(mod_aula)
plot(fitted_vals, def_resid,
xlab = 'Predicción (fitted)', ylab = 'Residuos deviance',
main = 'Residuos deviance vs fitted')
abline(h = 0, lty = 2)
# Diagrama de influencia (distancia de Cook)
cook_d <- cooks.distance(mod_aula)
plot(cook_d, type = 'h',
xlab = 'Índice de observación', ylab = 'Distancia de Cook',
main = 'Distancia de Cook')
abline(h = 4 / length(cook_d), col = 'red', lty = 2)
Más allá de los gráficos básicos, podemos aprovechar
plotly
para crear visualizaciones interactivas.
# Creamos una malla de valores para asistencias y nota_parcial (ejemplo aula)
grid <- expand.grid(
asistencias = seq(min(datos_aula$asistencias), max(datos_aula$asistencias), length.out = 50),
nota_parcial = seq(min(datos_aula$nota_parcial), max(datos_aula$nota_parcial), length.out = 50)
)
grid$prob <- predict(mod_aula, newdata = grid, type = 'response')
# Convertimos en matriz
mat_prob <- matrix(grid$prob, nrow = 50, ncol = 50)
# Gráfico 3D interactivo
plot_ly(x = ~unique(grid$asistencias),
y = ~unique(grid$nota_parcial),
z = ~mat_prob) %>%
add_surface() %>%
layout(title = 'Superficie de Probabilidad - Ejercicio Aula',
scene = list(
xaxis = list(title = 'Asistencias'),
yaxis = list(title = 'Nota parcial'),
zaxis = list(title = 'Probabilidad de aprobar')
))
plotly
Verificar variable dependiente: Asegurarse de que sea binaria.
Exploración inicial: Tablas de frecuencia, histogramas para variables continuas, boxplots.
Construir modelo:
glm(Y ~ X1 + X2 + ..., family = binomial)
Interpretar coeficientes:
Validación interna:
Diagnóstico:
Visualización:
Comunicación:
Ejercicios adicionales: Proponer a los estudiantes que experimenten con diferentes conjuntos de datos y escenarios.
plotly
enriquecen el aprendizaje en el aula.Ejercicios adicionales sugeridos:
Sepal.Length
y
Sepal.Width
. Compara AUC y matriz de confusión.glmnet
para un modelo con penalización L1. Comparar rendimiento vs modelo sin
penalización.nnet::multinom
).