Cómo usar este documento. Está pensado para ejecutarse de principio a fin (Knit). El archivo
trees.csvdebe estar en la misma carpeta que este.Rmd. No hay rutas absolutas ni instalación de paquetes dentro del documento.
Solo tres paquetes son imprescindibles
(ggplot2, dplyr, broom). Los
demás (car, lmtest, GGally,
ggfortify) se usan si están instalados; si no lo están, el
documento sigue funcionando porque más adelante programamos las
alternativas a mano —lo cual, además, es pedagógicamente útil:
muestra qué está calculando el paquete.
library(ggplot2)
library(dplyr)
library(broom)
hay <- function(p) requireNamespace(p, quietly = TRUE)
opcionales <- c("car", "lmtest", "GGally", "ggfortify")
data.frame(paquete = opcionales, instalado = sapply(opcionales, hay))
Antes de poner dos predictoras en el mismo modelo conviene tener aceitado el vocabulario de la regresión simple. Usamos un ejemplo pequeño y conocido.
| Concepto | Qué es |
|---|---|
| Variable respuesta (\(Y\)) | Lo que queremos explicar o predecir. Es aleatoria. |
| Variable predictora (\(X\)) | Lo que usamos para explicar. En el modelo clásico se trata como fija y medida sin error. |
| Correlación (\(r\)) | Mide la fuerza y el sentido de la asociación lineal entre dos variables. Va de \(-1\) a \(1\) y no tiene unidades. |
| Asociación vs. causalidad | Que \(X\) prediga bien a \(Y\) no significa que la cause. La
causalidad viene del diseño del estudio, no del lm(). |
| Recta de regresión | \(\hat y = \hat\beta_0 + \hat\beta_1 x\): el valor promedio de \(Y\) estimado para cada \(x\). |
| Valor observado (\(y_i\)) | El dato real. |
| Valor estimado / ajustado (\(\hat y_i\)) | Lo que el modelo predice para ese individuo. |
| Residual (\(e_i = y_i - \hat y_i\)) | Lo que el modelo no logró explicar en esa observación. |
| Mínimos cuadrados | El criterio para elegir \(\hat\beta\): minimizar \(\sum e_i^2\). |
| \(R^2\) | Proporción de la variabilidad de \(Y\) que el modelo explica. |
| Error estándar residual (\(\hat\sigma\)) | En las unidades de \(Y\): cuánto se desvía típicamente una observación de la recta. |
| Prueba \(t\) | ¿Este coeficiente es distinguible de cero, dado el resto del modelo? |
| Intervalo de confianza | Rango de valores del parámetro compatibles con los datos. |
| Predicción | Un valor de \(Y\) para una combinación nueva de predictoras. |
Datos representativos de los reportados en el Journal of Sound and Vibration (Vol. 151, 1991, pp. 383–394): \(x\) es el nivel de presión acústica (decibeles) y \(y\) el aumento de la presión arterial (mm Hg).
ruido <- data.frame(
db = c(60, 63, 65, 70, 70, 70, 80, 90, 80, 80, 85, 89, 90, 90, 90, 90, 94, 100, 100, 100),
pa = c( 1, 0, 1, 2, 5, 1, 4, 6, 2, 3, 5, 4, 6, 8, 4, 5, 7, 9, 7, 6)
)
head(ruido)
Pregunta → Datos. ¿A mayor exposición al ruido, mayor aumento de presión arterial?
ggplot(ruido, aes(db, pa)) +
geom_point(size = 2.4, colour = "steelblue") +
geom_smooth(method = "lm", se = FALSE, colour = "firebrick") +
labs(title = "Presión arterial vs. nivel de ruido",
x = "Nivel de presión acústica (dB)", y = "Aumento de presión arterial (mm Hg)")
cor(ruido$db, ruido$pa)
## [1] 0.86502
Una correlación de \(\approx 0.865\): asociación lineal positiva y fuerte. Fuerte no es lo mismo que causal: este es un estudio observacional y la edad, el tiempo de exposición o la ocupación podrían estar detrás de ambas variables.
Modelo.
m_ruido <- lm(pa ~ db, data = ruido)
summary(m_ruido)
##
## Call:
## lm(formula = pa ~ db, data = ruido)
##
## Residuals:
## Min 1Q Median 3Q Max
## -1.812 -0.904 -0.133 0.502 2.931
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -10.1315 1.9949 -5.08 0.00007832 ***
## db 0.1743 0.0238 7.31 0.00000086 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 1.32 on 18 degrees of freedom
## Multiple R-squared: 0.748, Adjusted R-squared: 0.734
## F-statistic: 53.5 on 1 and 18 DF, p-value: 0.000000857
Resultados → lectura. La recta estimada es
\[\widehat{pa} = -10.132 + 0.174\,\cdot\,db\]
⚠️ Corrección respecto al material anterior. En las versiones previas de esta clase el intercepto de este ejemplo aparecía interpretado como “34.5538 unidades”. Ese número pertenece al ejemplo de
Boston(medv ~ lstat) y se copió por error: el intercepto de este modelo es \(-10.132\). Es un buen recordatorio de que toda interpretación debe leerse de la salida del modelo que se acaba de ajustar.
Intervalo de confianza y prueba \(t\).
confint(m_ruido, level = 0.95)
## 2.5 % 97.5 %
## (Intercept) -14.32267 -5.94041
## db 0.12423 0.22436
La prueba \(t\) de la pendiente contrasta \(H_0:\beta_1 = 0\) (el ruido no aporta información lineal sobre la presión) contra \(H_1:\beta_1\neq 0\). Con \(p < 0.001\) rechazamos \(H_0\): los datos son muy poco compatibles con la idea de que no hay relación lineal. El IC del 95 % para \(\beta_1\) no contiene el cero, lo cual dice lo mismo, pero además informa de la magnitud plausible del efecto —que es la información que realmente sirve para decidir.
Todo lo anterior vive en el modelo
\[Y = \beta_0 + \beta_1 X + \epsilon\]
La regresión múltiple simplemente admite varias predictoras a la vez:
\[Y = \beta_0 + \beta_1X_1 + \beta_2X_2 + \cdots + \beta_kX_k + \epsilon\]
Los conceptos (residual, mínimos cuadrados, \(R^2\), prueba \(t\), IC, predicción) sobreviven intactos. Lo que cambia —y es el corazón de esta clase— es la interpretación de cada coeficiente.
Pregunta orientadora: ¿podemos explicar y predecir el volumen de madera de un árbol usando simultáneamente su diámetro y su altura?
Medir el volumen de madera de un árbol en pie es caro: hay que talarlo. Medir el diámetro es trivial (una cinta a la altura del pecho) y la altura es medible con un clinómetro. Si un modelo predice bien el volumen a partir de esas dos medidas, el inventario forestal completo cambia de costo.
trees31 cerezos negros (black cherry) talados.
| Variable | Descripción | Unidad |
|---|---|---|
Girth |
Diámetro del árbol medido a 4 pies 6 pulgadas del suelo. El nombre original (girth = circunferencia) está mal puesto en el conjunto de datos: los valores son diámetros. | pulgadas |
Height |
Altura del árbol | pies |
Volume |
Volumen de madera | pies cúbicos |
# Opción usada: leer el CSV que acompaña a este documento.
# (R también trae el conjunto 'trees' incorporado: data(trees). Usamos el CSV
# para que R y Python partan EXACTAMENTE del mismo archivo.)
if (file.exists("trees.csv")) {
trees <- read.csv("trees.csv")
# la primera columna del CSV es el índice de fila exportado desde R
if (names(trees)[1] %in% c("X", "X.1", "")) trees <- trees[, -1]
} else {
message("No se encontró trees.csv; se usa el conjunto incorporado de R.")
data(trees)
}
str(trees)
## 'data.frame': 31 obs. of 3 variables:
## $ Girth : num 8.3 8.6 8.8 10.5 10.7 10.8 11 11 11.1 11.2 ...
## $ Height: int 70 65 63 72 81 83 66 75 80 75 ...
## $ Volume: num 10.3 10.3 10.2 16.4 18.8 19.7 15.6 18.2 22.6 19.9 ...
dim(trees)
## [1] 31 3
colSums(is.na(trees)) # valores faltantes
## Girth Height Volume
## 0 0 0
summary(trees)
## Girth Height Volume
## Min. : 8.3 Min. :63 Min. :10.2
## 1st Qu.:11.1 1st Qu.:72 1st Qu.:19.4
## Median :12.9 Median :76 Median :24.2
## Mean :13.2 Mean :76 Mean :30.2
## 3rd Qu.:15.2 3rd Qu.:80 3rd Qu.:37.3
## Max. :20.6 Max. :87 Max. :77.0
No hay valores faltantes. Antes de modelar, miramos.
trees_largo <- stack(trees) # columnas: values, ind
ggplot(trees_largo, aes(values)) +
geom_histogram(bins = 10, fill = "steelblue", colour = "white") +
facet_wrap(~ ind, scales = "free") +
labs(title = "Distribución de las tres variables", x = NULL, y = "Frecuencia")
ggplot(trees_largo, aes(ind, values)) +
geom_boxplot(fill = "steelblue", alpha = .5, outlier.colour = "firebrick") +
facet_wrap(~ ind, scales = "free") +
labs(title = "Diagramas de caja", x = NULL, y = NULL)
Volume es claramente asimétrica a la derecha y tiene un
árbol muy grande (77 pies³). Anotemos esa observación
mentalmente: volveremos a ella en el análisis de
influencia.
round(cor(trees), 4)
## Girth Height Volume
## Girth 1.0000 0.5193 0.9671
## Height 0.5193 1.0000 0.5982
## Volume 0.9671 0.5982 1.0000
if (hay("GGally")) {
GGally::ggpairs(trees, title = "Matriz de dispersión y correlaciones")
} else {
pairs(trees, pch = 19, col = "steelblue4",
main = "Matriz de dispersión")
}
Tres lecturas:
Volume–Girth: \(r = 0.967\). Relación muy fuerte.
El diámetro por sí solo ya explicaría casi todo.Volume–Height: \(r = 0.598\). Relación moderada.
Los árboles altos tienden a tener más volumen, pero con mucha
dispersión.Girth–Height: \(r = 0.519\). ⚠️ Las dos
predictoras están correlacionadas entre sí. Este hecho es
el motivo por el que un coeficiente en regresión múltiple no
significa lo mismo que en regresión simple. Lo trabajamos en la sección
3.p1 <- ggplot(trees, aes(Girth, Volume)) + geom_point(colour = "steelblue", size = 2) +
geom_smooth(method = "lm", se = FALSE, colour = "firebrick") + labs(title = "Volume ~ Girth")
p2 <- ggplot(trees, aes(Height, Volume)) + geom_point(colour = "steelblue", size = 2) +
geom_smooth(method = "lm", se = FALSE, colour = "firebrick") + labs(title = "Volume ~ Height")
p3 <- ggplot(trees, aes(Girth, Height)) + geom_point(colour = "darkgreen", size = 2) +
geom_smooth(method = "lm", se = FALSE, colour = "firebrick") + labs(title = "Height ~ Girth")
print(p1); print(p2); print(p3)
🎓 Pausa para el estudiante. Antes de ajustar nada, escriba su respuesta:
- ¿Qué signo espera para el coeficiente de
Girth? ¿Y paraHeight?- Un árbol es aproximadamente un cono o un cilindro. Si \(V \propto d^2 h\), ¿espera que la relación entre
VolumeyGirthsea exactamente una recta? Guarde su respuesta: la retomamos en la sección 18.
\[Y_i = \beta_0 + \beta_1 X_{1i} + \beta_2 X_{2i} + \epsilon_i, \qquad i = 1,\dots,n\]
Para nuestros datos:
\[Volume_i = \beta_0 + \beta_1\,Girth_i + \beta_2\,Height_i + \epsilon_i\]
Geométricamente ya no ajustamos una recta sino un plano en el espacio \((Girth, Height, Volume)\) —y con \(k\) predictoras, un hiperplano en \(k+1\) dimensiones.
Es el valor promedio de \(Y\) cuando
todas las predictoras valen cero. En trees
eso sería un árbol de 0 pulgadas de diámetro y 0 pies de altura:
no existe, y además está muy lejos del rango observado
(\(Girth\in[8.3, 20.6]\), \(Height\in[63, 87]\)). El intercepto aquí es
un parámetro de ajuste, no un resultado interpretable. (Si se quiere un
intercepto interpretable, se centran las predictoras en su media;
entonces \(\beta_0\) es el valor
esperado de \(Y\) para un árbol
promedio.)
\(\beta_1\) es el cambio promedio esperado en \(Y\) por un aumento de una unidad en \(X_1\), manteniendo constantes las demás variables del modelo.
Esa última cláusula lo cambia todo. En regresión simple, \(\beta_1\) mezcla dos cosas: el efecto directo de \(X_1\) y el efecto de todo aquello que varía junto con \(X_1\). En regresión múltiple, al incluir \(X_2\), el coeficiente \(\beta_1\) se “limpia” del efecto de \(X_2\): es un efecto parcial o ajustado.
Ejemplo numérico anticipado (lo verificamos en la sección 5):
| Modelo | Coeficiente de Girth |
Lectura |
|---|---|---|
Volume ~ Girth |
\(5.066\) | Comparando árboles que difieren 1 pulgada de diámetro sin importar su altura, el volumen difiere en promedio 5.07 pies³. Parte de esa diferencia se debe a que los árboles más gruesos también tienden a ser más altos. |
Volume ~ Girth + Height |
\(4.708\) | Comparando árboles que difieren 1 pulgada de diámetro pero con la misma altura, el volumen difiere en promedio 4.71 pies³. |
La diferencia \(5.066 - 4.708 = 0.358\) es justamente la parte del “efecto” del diámetro que en realidad era altura disfrazada.
Esta sección merece detenerse. “Manteniendo las demás constantes” no es una fórmula retórica: es una operación concreta que podemos ver.
m_simple <- lm(Volume ~ Girth, data = trees)
m_multiple <- lm(Volume ~ Girth + Height, data = trees)
comparacion <- data.frame(
modelo = c("Volume ~ Girth", "Volume ~ Girth + Height"),
coef_Girth = c(coef(m_simple)["Girth"], coef(m_multiple)["Girth"]),
ee_Girth = c(summary(m_simple)$coefficients["Girth", 2],
summary(m_multiple)$coefficients["Girth", 2]),
R2_ajustado = c(summary(m_simple)$adj.r.squared,
summary(m_multiple)$adj.r.squared)
)
comparacion
El coeficiente de Girth en el modelo múltiple se puede
obtener en dos pasos —esto es lo que significa
“controlar por Height”:
# Paso 1: quitarle a Girth todo lo que Height puede explicar de él
girth_residual <- residuals(lm(Girth ~ Height, data = trees))
# Paso 2: quitarle a Volume todo lo que Height puede explicar de él
volume_residual <- residuals(lm(Volume ~ Height, data = trees))
# Paso 3: regresar un residuo contra el otro
coef(lm(volume_residual ~ girth_residual))["girth_residual"]
## girth_residual
## 4.7082
# ...y comparar con el coeficiente del modelo múltiple:
coef(m_multiple)["Girth"]
## Girth
## 4.7082
Son idénticos (teorema de Frisch–Waugh–Lovell). El
coeficiente parcial de Girth es la relación entre la
parte de Volume que Height no explica y
la parte de Girth que Height no
explica. Es decir: la relación que queda después de
descontar la altura de ambos lados.
ggplot(data.frame(girth_residual, volume_residual), aes(girth_residual, volume_residual)) +
geom_point(size = 2.2, colour = "steelblue") +
geom_smooth(method = "lm", se = FALSE, colour = "firebrick") +
geom_hline(yintercept = 0, linetype = 2, colour = "grey50") +
geom_vline(xintercept = 0, linetype = 2, colour = "grey50") +
labs(title = "Gráfico de regresión parcial: Girth | Height",
subtitle = "La pendiente de esta recta ES el coeficiente parcial de Girth",
x = "Girth ajustado por Height (residuos)",
y = "Volume ajustado por Height (residuos)")
Girth puede volver a cambiar. No es “el efecto del
diámetro”: es “el efecto del diámetro en este
modelo”.Height no convierte a \(\beta_1\) en un efecto causal. La
causalidad requiere supuestos sobre el diseño (aleatorización, ausencia
de confusores no medidos), no un + Height en la
fórmula.Entre todos los planos posibles, elegimos el que hace mínima la suma de los cuadrados de los errores:
\[SCE = \sum_{i=1}^{n}\left(y_i - \hat y_i\right)^2 = \sum_{i=1}^{n}\left(y_i - \beta_0 - \beta_1 x_{1i} - \beta_2 x_{2i}\right)^2\]
¿Por qué los cuadrados? Porque (a) penaliza más los errores grandes, (b) evita que los errores positivos y negativos se cancelen, y (c) tiene solución algebraica cerrada.
\[\mathbf{y} = \mathbf{X}\boldsymbol{\beta} + \boldsymbol{\epsilon}, \qquad \mathbf{X} = \begin{pmatrix} 1 & x_{11} & x_{12}\\ 1 & x_{21} & x_{22}\\ \vdots & \vdots & \vdots\\ 1 & x_{n1} & x_{n2} \end{pmatrix}\]
Derivando \(S(\boldsymbol\beta) = (\mathbf y - \mathbf X\boldsymbol\beta)'(\mathbf y - \mathbf X\boldsymbol\beta)\) e igualando a cero se llega a las ecuaciones normales \(\mathbf{X'X}\hat{\boldsymbol\beta} = \mathbf{X'y}\), de donde
\[\boxed{\;\hat{\boldsymbol{\beta}} = (\mathbf{X}'\mathbf{X})^{-1}\mathbf{X}'\mathbf{y}\;}\]
siempre que \((\mathbf{X'X})^{-1}\) exista (volveremos a esto en multicolinealidad).
y <- trees$Volume # vector n x 1
X <- cbind(Intercepto = 1, # matriz n x p (p = 3)
Girth = trees$Girth,
Height = trees$Height)
dim(X)
## [1] 31 3
XtX <- t(X) %*% X ; XtX
## Intercepto Girth Height
## Intercepto 31.0 410.7 2356
## Girth 410.7 5736.6 31525
## Height 2356.0 31524.7 180274
XtX_inv <- solve(XtX) ; round(XtX_inv, 6)
## Intercepto Girth Height
## Intercepto 4.951943 0.028680 -0.069732
## Girth 0.028680 0.004635 -0.001185
## Height -0.069732 -0.001185 0.001124
Xty <- t(X) %*% y ; Xty
## [,1]
## Intercepto 935.3
## Girth 13887.9
## Height 72962.6
beta_hat <- XtX_inv %*% Xty
beta_hat
## [,1]
## Intercepto -57.98766
## Girth 4.70816
## Height 0.33925
# ¿Coincide con lm()?
cbind(manual = as.vector(beta_hat), lm = coef(m_multiple))
## manual lm
## (Intercept) -57.98766 -57.98766
## Girth 4.70816 4.70816
## Height 0.33925 0.33925
all.equal(as.vector(beta_hat), unname(coef(m_multiple)))
## [1] TRUE
Coinciden. El objetivo de este bloque no es enseñar
álgebra lineal avanzada sino desmitificar lm(): no hay
magia, hay una inversa y dos productos matriciales.
# La matriz sombrero H = X(X'X)^{-1}X' proyecta y sobre el espacio de las columnas de X
H <- X %*% XtX_inv %*% t(X)
ajustados_manual <- as.vector(H %*% y)
head(cbind(manual = ajustados_manual, lm = fitted(m_multiple)))
## manual lm
## 1 4.8377 4.8377
## 2 4.5539 4.5539
## 3 4.8170 4.8170
## 4 15.8741 15.8741
## 5 19.8690 19.8690
## 6 21.0183 21.0183
# Los elementos de la diagonal de H son los 'leverages' (sección 16)
round(head(diag(H)), 4)
## [1] 0.1158 0.1472 0.1769 0.0592 0.1207 0.1558
modelo <- lm(Volume ~ Girth + Height, data = trees)
summary(modelo)
##
## Call:
## lm(formula = Volume ~ Girth + Height, data = trees)
##
## Residuals:
## Min 1Q Median 3Q Max
## -6.406 -2.649 -0.288 2.200 8.485
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -57.988 8.638 -6.71 0.00000027 ***
## Girth 4.708 0.264 17.82 < 0.0000000000000002 ***
## Height 0.339 0.130 2.61 0.014 *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 3.88 on 28 degrees of freedom
## Multiple R-squared: 0.948, Adjusted R-squared: 0.944
## F-statistic: 255 on 2 and 28 DF, p-value: <0.0000000000000002
\[\widehat{Volume} = -57.988 + 4.708\,Girth + 0.339\,Height\]
tidy(modelo, conf.int = TRUE) |>
mutate(across(where(is.numeric), function(x) round(x, 4)))
Intercepto (\(\hat\beta_0 = -57.988\)). Volumen promedio de un árbol de 0 pulgadas de diámetro y 0 pies de altura. Sin interpretación práctica: es una extrapolación absurda y de hecho da negativo. Cumple la función de posicionar el plano.
Girth (\(\hat\beta_1 =
4.708\)). Entre árboles de la misma
altura, uno con una pulgada más de diámetro tiene en promedio
4.71 pies³ más de volumen.
Height (\(\hat\beta_2
= 0.339\)). Entre árboles del mismo
diámetro, uno con un pie más de altura tiene en promedio
0.34 pies³ más de volumen.
Girth.Error estándar residual \(\hat\sigma = 3.882\) pies³ con 28 grados de libertad. Esa es la escala típica del error de este modelo. Como el volumen promedio es 30.17 pies³, el error típico es \(\approx 13\%\) del valor medio.
confint(modelo, level = 0.95)
## 2.5 % 97.5 %
## (Intercept) -75.682262 -40.29306
## Girth 4.166839 5.24948
## Height 0.072649 0.60585
Regla de oro de interpretación: nunca reporte solo el signo y el p-valor. Reporte magnitud + unidades + intervalo + la cláusula “manteniendo constante lo demás”. “
Girthes significativa” no le sirve a un ingeniero forestal; “una pulgada más de diámetro, a igual altura, son entre 4.2 y 5.2 pies³ más de madera” sí.
Pregunta: ¿el conjunto de predictoras aporta información sobre
Volume, o el modelo completo no es mejor que predecir siempre la media?
\[H_0:\ \beta_1 = \beta_2 = \cdots = \beta_k = 0 \qquad\text{vs.}\qquad H_1:\ \text{al menos un } \beta_j \neq 0\]
\[\underbrace{\sum(y_i-\bar y)^2}_{SCT} =\underbrace{\sum(\hat y_i-\bar y)^2}_{SCR\ (\text{regresión})} +\underbrace{\sum(y_i-\hat y_i)^2}_{SCE\ (\text{error})}\]
y_barra <- mean(trees$Volume)
SCT <- sum((trees$Volume - y_barra)^2)
SCR <- sum((fitted(modelo) - y_barra)^2)
SCE <- sum(residuals(modelo)^2)
n <- nrow(trees); k <- 2; p <- k + 1
tabla_anova <- data.frame(
Fuente = c("Regresión", "Error", "Total"),
SC = c(SCR, SCE, SCT),
gl = c(k, n - p, n - 1),
CM = c(SCR / k, SCE / (n - p), NA),
F = c((SCR / k) / (SCE / (n - p)), NA, NA),
p_valor= c(pf((SCR / k) / (SCE / (n - p)), k, n - p, lower.tail = FALSE), NA, NA)
)
tabla_anova
# La misma información con anova(): descomposición SECUENCIAL (tipo I)
anova(modelo)
# Comparación explícita contra el modelo nulo = prueba F global
anova(lm(Volume ~ 1, data = trees), modelo)
Lectura. \(F = 254.97\) con 2 y 28 grados de libertad, \(p \approx 1.07\times10^{-18}\). La variabilidad explicada por el modelo es 255 veces mayor (por grado de libertad) que la variabilidad residual. El conjunto \(\{Girth, Height\}\) contiene información sustancial sobre el volumen.
⚠️ Ojo con
anova().anova(modelo)da sumas de cuadrados secuenciales: la línea deHeightmide lo queHeightaporta después deGirth. Si invierte el orden en la fórmula, esos números cambian. Las pruebas \(t\) delsummary(), en cambio, son siempre “esta variable dada todas las demás” y no dependen del orden.
🎓 Pregunta conceptual: ¿puede el modelo global ser significativo y ninguna variable individual serlo?
Sí, y ocurre con frecuencia. Son preguntas distintas:
Si dos predictoras están muy correlacionadas entre sí, ambas pueden ser prescindibles por separado (porque la otra la sustituye) y sin embargo el par ser imprescindible. La \(F\) las evalúa en bloque; las \(t\) las evalúan al margen.
También ocurre lo contrario: con muchas predictoras irrelevantes, alguna \(t\) puede salir significativa por azar mientras la \(F\) global no lo es. Por eso se mira primero la \(F\), después las \(t\).
# Verificación numérica: con 1 grado de libertad en el numerador, F = t^2.
# Comparar el modelo con y sin Height equivale a la prueba t de Height.
anova(lm(Volume ~ Girth, data = trees), modelo)
summary(modelo)$coefficients["Height", "t value"]^2
## [1] 6.7943
\[R^2 = 1-\frac{SCE}{SCT} = \frac{SCR}{SCT} \qquad\qquad R^2_{aj} = 1-\frac{SCE/(n-p)}{SCT/(n-1)}\]
c(R2 = summary(modelo)$r.squared,
R2_ajustado = summary(modelo)$adj.r.squared,
R2_manual = 1 - SCE/SCT)
## R2 R2_ajustado R2_manual
## 0.94795 0.94423 0.94795
\(R^2 = 0.948\): el modelo explica el 94.8 % de la variabilidad del volumen.
\(R^2\) nunca baja al agregar predictoras, aunque sean ruido puro: siempre hay algún ajuste espurio que reduce un poco el \(SCE\). Demostrémoslo:
set.seed(2026)
trees_ruido <- trees
trees_ruido$basura1 <- rnorm(n) # tres variables sin ninguna relación con Volume
trees_ruido$basura2 <- rnorm(n)
trees_ruido$basura3 <- rnorm(n)
m_basura <- lm(Volume ~ Girth + Height + basura1 + basura2 + basura3, data = trees_ruido)
data.frame(
modelo = c("Girth + Height", "Girth + Height + 3 variables aleatorias"),
R2 = c(summary(modelo)$r.squared, summary(m_basura)$r.squared),
R2_ajustado = c(summary(modelo)$adj.r.squared, summary(m_basura)$adj.r.squared),
sigma = c(summary(modelo)$sigma, summary(m_basura)$sigma)
)
\(R^2\) subió al agregar tres columnas de números aleatorios. El \(R^2\) ajustado penaliza por el número de parámetros y baja, que es la respuesta correcta.
No use \(R^2\) como única medida de calidad. Un \(R^2\) alto es compatible con supuestos violados, con una observación influyente que domina el ajuste y con un modelo inútil fuera del rango de los datos. El \(R^2\) mide cuánto se acerca el modelo a estos datos, no si el modelo es correcto. Por eso el siguiente bloque grande de la clase es el diagnóstico.
arboles_nuevos <- data.frame(
Girth = c(11.0, 13.2, 16.0),
Height = c(70, 76, 82)
)
arboles_nuevos
# Predicción puntual
predict(modelo, newdata = arboles_nuevos)
## 1 2 3
## 17.550 29.943 45.162
ic_media <- predict(modelo, newdata = arboles_nuevos, interval = "confidence", level = 0.95)
ip_nueva <- predict(modelo, newdata = arboles_nuevos, interval = "prediction", level = 0.95)
data.frame(
arboles_nuevos,
estimacion = ic_media[, "fit"],
IC_inf = ic_media[, "lwr"], IC_sup = ic_media[, "upr"],
IP_inf = ip_nueva[, "lwr"], IP_sup = ip_nueva[, "upr"]
) |> mutate(across(where(is.numeric), function(x) round(x, 3)))
Ambos intervalos están centrados en la misma predicción puntual, pero responden a preguntas distintas:
| Intervalo de confianza | Intervalo de predicción | |
|---|---|---|
| Pregunta | ¿Dónde está el volumen promedio de todos los árboles con \(Girth=13.2\) y \(Height=76\)? | ¿Dónde caerá el volumen de un árbol concreto con esas medidas? |
| Objeto estimado | Un parámetro: \(E[Y\mid x_0]\) | Una variable aleatoria: \(Y_0\) |
| Error estándar | \(\hat\sigma\sqrt{\mathbf x_0'(\mathbf{X'X})^{-1}\mathbf x_0}\) | \(\hat\sigma\sqrt{1+\mathbf x_0'(\mathbf{X'X})^{-1}\mathbf x_0}\) |
| Ancho (árbol 2) | \([28.5,\ 31.4]\) → 2.9 pies³ | \([21.9,\ 38.0]\) → 16.2 pies³ |
El “+1” bajo la raíz es toda la diferencia: al predecir un individuo cargamos además con su variabilidad propia \(\sigma^2\), que no desaparece aunque tuviéramos infinitos datos. Por eso:
Error frecuente. Reportar el IC de la media cuando lo que el usuario quiere es predecir un árbol. Si un ingeniero forestal pregunta “¿cuánta madera saco de este árbol?”, la respuesta honesta es el intervalo de predicción.
malla <- data.frame(Girth = seq(min(trees$Girth), max(trees$Girth), length.out = 60),
Height = mean(trees$Height))
bandas <- cbind(malla,
as.data.frame(predict(modelo, malla, interval = "confidence")),
setNames(as.data.frame(predict(modelo, malla, interval = "prediction"))[, 2:3],
c("p_lwr", "p_upr")))
ggplot(bandas, aes(Girth)) +
geom_ribbon(aes(ymin = p_lwr, ymax = p_upr), fill = "grey85") +
geom_ribbon(aes(ymin = lwr, ymax = upr), fill = "steelblue", alpha = .45) +
geom_line(aes(y = fit), colour = "firebrick", linewidth = 1) +
geom_point(data = trees, aes(Girth, Volume), colour = "grey25", size = 1.8) +
labs(title = "Bandas de confianza (azul) y de predicción (gris)",
subtitle = "Height fijada en su media (76 pies)",
y = "Volume (pies cúbicos)")
⚠️ Extrapolación. El modelo solo es creíble dentro del rango observado (\(Girth \in [8.3, 20.6]\), \(Height \in [63, 87]\)) y en combinaciones plausibles de ambas. Predecir un árbol de \(Girth = 20\) y \(Height = 63\) es extrapolar aunque cada valor esté dentro de su rango individual: esa combinación no existe en los datos.
Obtener coeficientes, \(R^2\) y p-valores no significa que el análisis haya terminado. Significa que apenas empieza la parte interesante.
lm() siempre devuelve números. Devuelve coeficientes
aunque la relación sea una parábola, devuelve p-valores aunque la
varianza explote y devuelve \(R^2 =
0.99\) aunque un solo dato esté sosteniendo todo el ajuste.
Los supuestos son las condiciones bajo las cuales esos números
significan lo que creemos que significan.
| # | Supuesto | Enunciado | Si falla, ¿qué se rompe? |
|---|---|---|---|
| 1 | Linealidad (media) | \(E[Y \mid \mathbf X] = \beta_0+\beta_1X_1+\cdots+\beta_kX_k\) | Todo. Los coeficientes estiman algo que no existe; las predicciones son sesgadas de forma sistemática. |
| 2 | Homocedasticidad (varianza) | \(Var(\epsilon_i) = \sigma^2\) constante | Los \(\hat\beta\) siguen siendo insesgados, pero los errores estándar, las \(t\), la \(F\) y los intervalos son incorrectos. |
| 3 | Independencia | \(Cov(\epsilon_i, \epsilon_j)=0,\ i\neq j\) | Igual que arriba: la inferencia se vuelve optimista (errores estándar demasiado pequeños). |
| 4 | Normalidad | \(\epsilon_i \sim N(0,\sigma^2)\) | Solo afecta a la inferencia exacta en muestras pequeñas (pruebas \(t\)/\(F\) e intervalos). Con \(n\) grande el TLC ayuda. |
| 5 | Observaciones comparables | Ninguna observación domina el ajuste | Las conclusiones dependen de uno o dos datos. No es un supuesto distribucional sino de robustez. |
| 6 | Predictoras no colineales | \((\mathbf{X'X})^{-1}\) existe y está bien condicionada | Los coeficientes individuales se vuelven inestables y sin interpretación separable. |
Conviene tener clara la jerarquía: 1 es de vida o muerte; 2 y 3 dañan la inferencia; 4 es la menos crítica de todas (y la que más se sobre-enfatiza en los cursos); 5 y 6 no son supuestos sobre \(\epsilon\) sino sobre la estructura de los datos.
\[e_i = y_i - \hat y_i\]
Problema: no tienen varianza constante aunque el modelo sea perfecto. Se demuestra que \(\mathbf e = (\mathbf I - \mathbf H)\boldsymbol\epsilon\), de donde
\[Var(e_i) = \sigma^2\,(1 - h_{ii})\]
donde \(h_{ii}\) es el \(i\)-ésimo elemento diagonal de \(\mathbf H = \mathbf X(\mathbf{X'X})^{-1}\mathbf X'\) (el leverage). Las observaciones con predictoras extremas tienen \(h_{ii}\) alto y, por construcción, residuos más pequeños: el modelo se “estira” para alcanzarlas. Comparar residuos ordinarios entre sí es comparar peras con manzanas.
\[z_i = \frac{e_i}{\sigma\sqrt{1-h_{ii}}}\]
Correcto en teoría, inútil en la práctica: \(\sigma\) es desconocido.
rstandard()Sustituimos \(\sigma\) por \(\hat\sigma = \sqrt{SCE/(n-p)}\):
\[r_i = \frac{e_i}{\hat\sigma\sqrt{1-h_{ii}}}\]
Inconveniente sutil: si la observación \(i\) es un dato atípico, infla su propio \(\hat\sigma\) y por tanto se disimula a sí misma.
rstudent()Estimamos \(\sigma\) excluyendo la observación \(i\):
\[r_i^{*} = \frac{e_i}{\hat\sigma_{(i)}\sqrt{1-h_{ii}}}, \qquad \hat\sigma_{(i)}^2 = \frac{SCE_{(i)}}{n-p-1}\]
Ahora el numerador y el denominador son independientes y \(r_i^{*} \sim t_{n-p-1}\) bajo el modelo. Este es el residuo que se usa para detectar atípicos: un dato malo ya no puede esconderse inflando el error estándar con el que se lo juzga.
En una frase: los residuos studentizados ponen a todas las observaciones en la misma escala, corrigiendo por el hecho de que el modelo se ajusta más a unos puntos que a otros.
diagnostico <- data.frame(
obs = seq_len(n),
Girth = trees$Girth,
Height = trees$Height,
observado = trees$Volume,
estimado = round(fitted(modelo), 3),
residual = round(residuals(modelo), 3),
r_estandarizado= round(rstandard(modelo), 3),
r_studentizado = round(rstudent(modelo), 3),
leverage = round(hatvalues(modelo), 4),
cook = round(cooks.distance(modelo), 4)
)
diagnostico
umbral_h <- 2 * p / n # leverage alto
umbral_cook <- 4 / (n - p) # influencia (una de varias reglas)
c(umbral_leverage = umbral_h, umbral_cook = umbral_cook,
umbral_residuo = 2)
## umbral_leverage umbral_cook umbral_residuo
## 0.19355 0.14286 2.00000
diagnostico |>
filter(abs(r_studentizado) > 2 | leverage > umbral_h | cook > umbral_cook) |>
select(obs, Girth, Height, observado, estimado, r_studentizado, leverage, cook)
Cinco observaciones se levantan la mano. La 31 aparece en las tres listas: es el árbol grande que vimos en el boxplot. Volvemos a ella en la sección 16.
¿Qué esperamos ver? Una nube sin forma: los residuos repartidos al azar alrededor de cero, con la misma dispersión a lo largo de todo el eje horizontal. Cualquier patrón sistemático es información que el modelo no capturó.
ggplot(data.frame(ajustados = fitted(modelo), r = rstudent(modelo)),
aes(ajustados, r)) +
geom_point(size = 2.2, colour = "steelblue") +
geom_hline(yintercept = 0, colour = "grey40") +
geom_hline(yintercept = c(-2, 2), linetype = 2, colour = "firebrick") +
geom_smooth(se = FALSE, colour = "darkorange", method = "loess", span = 1) +
labs(title = "Residuos studentizados vs. valores ajustados",
x = "Valores ajustados", y = "Residuo studentizado")
par(mfrow = c(1, 2), mar = c(4, 4, 2, 1))
plot(trees$Girth, rstudent(modelo), pch = 19, col = "steelblue",
xlab = "Girth", ylab = "Residuo studentizado", main = "Residuos vs. Girth")
abline(h = 0); abline(h = c(-2, 2), lty = 2, col = "red")
lines(lowess(trees$Girth, rstudent(modelo)), col = "darkorange", lwd = 2)
plot(trees$Height, rstudent(modelo), pch = 19, col = "steelblue",
xlab = "Height", ylab = "Residuo studentizado", main = "Residuos vs. Height")
abline(h = 0); abline(h = c(-2, 2), lty = 2, col = "red")
lines(lowess(trees$Height, rstudent(modelo)), col = "darkorange", lwd = 2)
par(mfrow = c(1, 1))
Diagnóstico. Los gráficos contra los ajustados y
contra Girth muestran una curvatura en U
clarísima: residuos positivos en los extremos, negativos en el
centro. No es ruido: es una señal.
Un patrón de curvatura puede significar tres cosas:
En trees sabemos cuál es: el volumen de un sólido no es
una función lineal de sus dimensiones. Lo resolvemos en la sección
18.
Aíslan la relación de cada predictora con \(Y\) una vez descontadas las demás. Son la herramienta correcta para decidir cuál predictora necesita transformarse.
if (hay("car")) {
car::avPlots(modelo, pch = 19, col = "steelblue", col.lines = "firebrick")
} else {
op <- par(mfrow = c(1, 2), mar = c(4, 4, 2, 1))
for (v in c("Girth", "Height")) {
otras <- setdiff(c("Girth", "Height"), v)
rx <- residuals(lm(reformulate(otras, response = v), data = trees))
ry <- residuals(lm(reformulate(otras, response = "Volume"), data = trees))
plot(rx, ry, pch = 19, col = "steelblue", main = paste("Parcial:", v),
xlab = paste(v, "| otras"), ylab = "Volume | otras")
abline(lm(ry ~ rx), col = "firebrick", lwd = 2)
}
par(op)
}
\[H_0:\ \text{los residuos son compatibles con una distribución normal}\] \[H_1:\ \text{los residuos no son compatibles con la normalidad}\]
r_stud <- rstudent(modelo)
par(mfrow = c(1, 2), mar = c(4, 4, 2.5, 1))
hist(r_stud, breaks = 8, col = "steelblue", border = "white", freq = FALSE,
main = "Histograma y densidad", xlab = "Residuo studentizado")
lines(density(r_stud), lwd = 2, col = "darkorange")
curve(dnorm(x), add = TRUE, lwd = 2, lty = 2, col = "firebrick")
legend("topright", c("Densidad residuos", "N(0,1)"), lty = c(1, 2),
col = c("darkorange", "firebrick"), bty = "n", cex = .8)
qqnorm(r_stud, pch = 19, col = "steelblue", main = "Q-Q normal")
qqline(r_stud, col = "firebrick", lwd = 2)
par(mfrow = c(1, 1))
shapiro.test(rstudent(modelo))
##
## Shapiro-Wilk normality test
##
## data: rstudent(modelo)
## W = 0.973, p-value = 0.61
Lectura. \(W = 0.973\), \(p = 0.612\). No hay evidencia contra la normalidad. En el Q-Q plot los puntos siguen la línea salvo en el extremo superior derecho (la observación 31, otra vez).
Y lo más importante: el supuesto es sobre los errores, no sobre \(Y\) ni sobre las predictoras. Que
Volumesea asimétrica no viola nada por sí mismo.
\[Var(\epsilon_i) = \sigma^2 \quad \text{para todo } i\]
par(mfrow = c(1, 2), mar = c(4, 4, 2.5, 1))
plot(fitted(modelo), rstudent(modelo), pch = 19, col = "steelblue",
main = "Residuos vs. ajustados", xlab = "Ajustados", ylab = "Residuo studentizado")
abline(h = 0, col = "grey40"); abline(h = c(-2, 2), lty = 2, col = "firebrick")
plot(fitted(modelo), sqrt(abs(rstandard(modelo))), pch = 19, col = "steelblue",
main = "Scale-Location", xlab = "Ajustados", ylab = expression(sqrt(abs(r[estandarizado]))))
lines(lowess(fitted(modelo), sqrt(abs(rstandard(modelo)))), col = "darkorange", lwd = 2)
par(mfrow = c(1, 1))
El gráfico Scale-Location usa \(\sqrt{|r_i|}\) porque elimina el signo (nos interesa la magnitud) y estabiliza la escala. Queremos una línea horizontal con puntos repartidos por igual.
\[H_0:\ \text{varianza constante (homocedasticidad)}\] \[H_1:\ \text{la varianza depende de los predictores (heterocedasticidad)}\]
La lógica: si la varianza fuese constante, los residuos al cuadrado no deberían poder predecirse a partir de las \(X\). La prueba regresa \(e_i^2\) contra las predictoras y mira si esa regresión auxiliar explica algo.
if (hay("lmtest")) {
print(lmtest::bptest(modelo))
} else {
# Versión studentizada (Koenker), que es la que usa lmtest::bptest por defecto
e2 <- residuals(modelo)^2
aux <- lm(e2 ~ Girth + Height, data = trees)
LM <- n * summary(aux)$r.squared
cat("Breusch-Pagan (Koenker) LM =", round(LM, 4),
" gl =", k, " p-valor =", round(pchisq(LM, k, lower.tail = FALSE), 4), "\n")
}
##
## studentized Breusch-Pagan test
##
## data: modelo
## BP = 2.47, df = 2, p-value = 0.29
Lectura. \(BP = 2.468\), gl \(= 2\), \(p = 0.291\). No rechazamos la homocedasticidad. Ojo: con \(n = 31\) la prueba tiene poca potencia; “no rechazar” no es “confirmar”.
La dispersión crece con el nivel: típico cuando \(Y\) es un conteo, un monto, una concentración o cualquier cantidad positiva con efecto multiplicativo. La solución habitual es transformar \(Y\) (log, raíz) o usar errores estándar robustos.
\[H_0:\ \rho_1 = 0 \ \ (\text{ausencia de autocorrelación de orden 1})\]
El estadístico de Durbin-Watson
\[d = \frac{\sum_{t=2}^{n}(e_t - e_{t-1})^2}{\sum_{t=1}^{n}e_t^2}\]
toma valores entre 0 y 4: \(d\approx 2\) sin autocorrelación, \(d\approx 0\) autocorrelación positiva, \(d\approx 4\) negativa.
if (hay("lmtest")) {
print(lmtest::dwtest(modelo))
} else {
e <- residuals(modelo)
cat("Durbin-Watson d =", round(sum(diff(e)^2) / sum(e^2), 4), "\n")
}
##
## Durbin-Watson test
##
## data: modelo
## DW = 1.27, p-value = 0.0092
## alternative hypothesis: true autocorrelation is greater than 0
\(d = 1.27\) sugeriría
autocorrelación positiva. Pero el estadístico de Durbin-Watson
depende del orden de las filas, y en trees ese
orden no tiene significado sustantivo: el archivo está
ordenado por Girth creciente. Lo que DW está detectando es
la curvatura que ya vimos en la sección 11 — residuos
consecutivos parecidos porque siguen una U— y no una dependencia
temporal.
set.seed(99)
trees_barajado <- trees[sample(n), ]
m_barajado <- lm(Volume ~ Girth + Height, data = trees_barajado)
e1 <- residuals(modelo); e2 <- residuals(m_barajado)
c(DW_orden_original = sum(diff(e1)^2)/sum(e1^2),
DW_orden_aleatorio = sum(diff(e2)^2)/sum(e2^2))
## DW_orden_original DW_orden_aleatorio
## 1.2665 2.0321
Reordenando las filas al azar el estadístico cambia por completo, y el modelo es exactamente el mismo. Eso demuestra que aquí DW no mide una propiedad del modelo sino un artefacto del orden del archivo.
Cuándo SÍ tiene sentido Durbin-Watson: series de tiempo (observaciones indexadas por fecha), mediciones repetidas sobre el mismo sujeto, datos recogidos secuencialmente por un mismo operario o instrumento, o cualquier situación en la que exista un orden natural y real. En un corte transversal de árboles talados, no.
Regla: aplique DW solo si puede responder la pregunta “¿qué significa que la observación \(i\) venga antes que la \(i+1\)?”. Si no hay respuesta, no aplique la prueba.
En regresión simple no existe. En regresión múltiple es un problema central, y por eso lo agregamos aunque no estuviera desarrollado en el material anterior.
Multicolinealidad = las predictoras están (fuertemente) correlacionadas entre sí. Aparece porque:
La varianza del coeficiente \(j\) es
\[Var(\hat\beta_j) = \frac{\sigma^2}{\sum(x_{ij}-\bar x_j)^2}\cdot\underbrace{\frac{1}{1-R_j^2}}_{VIF_j}\]
donde \(R_j^2\) es el \(R^2\) de regresar \(X_j\) contra las demás predictoras. Si \(R_j^2 \to 1\), el VIF explota y el error estándar con él. Intuitivamente: el coeficiente parcial de \(X_j\) se estima con la parte de \(X_j\) que las otras no explican; si esa parte es minúscula, se está estimando con casi nada de información.
if (hay("car")) {
print(car::vif(modelo))
} else {
vif_manual <- sapply(c("Girth", "Height"), function(v) {
otras <- setdiff(c("Girth", "Height"), v)
1 / (1 - summary(lm(reformulate(otras, response = v), data = trees))$r.squared)
})
print(round(vif_manual, 4))
}
## Girth Height
## 1.3692 1.3692
# Con dos predictoras, VIF = 1/(1 - r^2)
r_gh <- cor(trees$Girth, trees$Height)
c(r = r_gh, r2 = r_gh^2, VIF = 1 / (1 - r_gh^2))
## r r2 VIF
## 0.51928 0.26965 1.36921
Lectura. \(VIF =
1.37\) para ambas: Height explica solo el 27 % de la
variabilidad de Girth. Sobra información independiente.
No hay problema de multicolinealidad en este
modelo.
Se citan umbrales de 5 o de 10. No son leyes. Considere:
car::vif() con
type = "predictor").\[\text{outlier} \neq \text{leverage} \neq \text{influencia}\]
| Concepto | Qué es | Se mide con | Dónde es raro |
|---|---|---|---|
| Outlier | Valor inusual en la respuesta, dado el modelo. El modelo lo predice mal. | \(\lvert r_i^{*}\rvert\) grande | Eje \(Y\) |
| Leverage alto | Valores inusuales en las predictoras. El modelo está obligado a prestarle atención. | \(h_{ii}\) grande | Eje(s) \(X\) |
| Observación influyente | Su presencia cambia el modelo. Suele requerir ambas cosas a la vez. | Distancia de Cook | Efecto sobre \(\hat\beta\) |
La intuición geométrica: un punto con leverage alto es un niño en el extremo de un balancín. Si además está fuera de la recta (outlier), mueve el balancín entero. Si está sobre la recta, tiene mucho leverage y cero influencia.
\[D_i = \frac{r_i^2}{p}\cdot\frac{h_{ii}}{1-h_{ii}} \qquad \leftarrow \text{residuo} \times \text{leverage}\]
La fórmula es la afirmación: influencia = ser atípico y estar en una posición que importa.
par(mfrow = c(1, 3), mar = c(4, 4, 3, 1))
plot(rstudent(modelo), pch = 19, col = "steelblue", ylim = c(-3.5, 3.5),
main = "Outliers en la respuesta", xlab = "Índice", ylab = "Residuo studentizado")
abline(h = 0); abline(h = c(-2, 2), lty = 2, col = "firebrick")
text(which(abs(rstudent(modelo)) > 2), rstudent(modelo)[abs(rstudent(modelo)) > 2],
labels = which(abs(rstudent(modelo)) > 2), pos = 4, col = "firebrick")
plot(hatvalues(modelo), pch = 19, col = "steelblue", ylim = c(0, max(hatvalues(modelo))*1.15),
main = "Leverage (h)", xlab = "Índice", ylab = "h")
abline(h = umbral_h, lty = 2, col = "firebrick")
text(which(hatvalues(modelo) > umbral_h), hatvalues(modelo)[hatvalues(modelo) > umbral_h],
labels = which(hatvalues(modelo) > umbral_h), pos = 2, col = "firebrick")
plot(cooks.distance(modelo), type = "h", lwd = 2, col = "steelblue",
main = "Distancia de Cook", xlab = "Índice", ylab = "D")
abline(h = umbral_cook, lty = 2, col = "firebrick")
text(which.max(cooks.distance(modelo)), max(cooks.distance(modelo)),
labels = which.max(cooks.distance(modelo)), pos = 2, col = "firebrick")
par(mfrow = c(1, 1))
plot(modelo, which = 5, pch = 19, col = "steelblue")
Este gráfico —Residuals vs Leverage— es el resumen: leverage en el eje horizontal, residuo estandarizado en el vertical, y las curvas punteadas rojas marcan contornos de distancia de Cook. Lo preocupante es la esquina superior o inferior derecha: residuo grande y leverage alto.
diagnostico |>
mutate(
outlier = abs(r_studentizado) > 2,
lev_alto = leverage > umbral_h,
influyente = cook > umbral_cook
) |>
filter(outlier | lev_alto | influyente) |>
select(obs, Girth, Height, observado, estimado,
r_studentizado, leverage, cook, outlier, lev_alto, influyente)
Lectura de la tabla:
# Las reglas de referencia NO coinciden entre sí. Son orientación, no veredicto.
data.frame(
regla = c("D > 4/(n-p)", "D > 4/n", "D > 1", "D > 3*media(D)"),
umbral = c(4/(n-p), 4/n, 1, 3*mean(cooks.distance(modelo))),
marcadas = sapply(c(4/(n-p), 4/n, 1, 3*mean(cooks.distance(modelo))),
function(u) paste(which(cooks.distance(modelo) > u), collapse = ", "))
)
Según la regla elegida, “las observaciones influyentes” son cuatro, cuatro, ninguna o una. Ningún umbral es la verdad. Lo que sí es informativo es la separación relativa: la observación 31 está sola, muy por encima de todas las demás, con cualquier criterio.
La pregunta no es “¿hay observaciones influyentes?” sino “¿cambia mi conclusión si no estuvieran?”. Eso se responde ajustando el modelo sin ellas y comparando.
modelo_sin31 <- lm(Volume ~ Girth + Height, data = trees[-31, ])
comparar <- function(m1, m2, n1 = "Con obs. 31", n2 = "Sin obs. 31") {
f <- function(m) c(
Intercepto = unname(coef(m)[1]),
Girth = unname(coef(m)[2]),
ee_Girth = summary(m)$coefficients[2, 2],
p_Girth = summary(m)$coefficients[2, 4],
Height = unname(coef(m)[3]),
ee_Height = summary(m)$coefficients[3, 2],
p_Height = summary(m)$coefficients[3, 4],
R2_ajustado = summary(m)$adj.r.squared,
sigma = summary(m)$sigma
)
out <- rbind(f(m1), f(m2))
rownames(out) <- c(n1, n2)
out <- rbind(out, "Cambio %" = 100 * (out[2, ] - out[1, ]) / abs(out[1, ]))
round(t(out), 4)
}
comparar(modelo, modelo_sin31)
## Con obs. 31 Sin obs. 31 Cambio %
## Intercepto -57.9877 -52.2362 9.9185
## Girth 4.7082 4.4773 -4.9039
## ee_Girth 0.2643 0.2518 -4.7154
## p_Girth 0.0000 0.0000 138.7576
## Height 0.3393 0.2992 -11.8168
## ee_Height 0.1302 0.1179 -9.4175
## p_Height 0.0145 0.0172 19.0212
## R2_ajustado 0.9442 0.9395 -0.4978
## sigma 3.8818 3.4896 -10.1048
# ¿Cambian las predicciones?
data.frame(
arboles_nuevos,
con_obs31 = round(predict(modelo, arboles_nuevos), 3),
sin_obs31 = round(predict(modelo_sin31, arboles_nuevos), 3)
) |> mutate(diferencia = round(sin_obs31 - con_obs31, 3),
dif_pct = round(100 * (sin_obs31 - con_obs31) / con_obs31, 2))
No. Al quitar la observación 31:
Girth baja de \(4.708\) a \(4.478\) (−4.9 %), y el nuevo valor sigue
dentro del IC 95 % del modelo original (\([4.167, 5.250]\)).Height baja de \(0.339\) a \(0.299\) (−11.8 %), y sigue siendo
significativo (\(p = 0.017\) frente a
\(p = 0.015\)).Conclusión sustantiva: la observación 31 es el punto que más tira del modelo, pero el modelo no depende de ella. Ninguna decisión cambiaría. Por lo tanto la conservamos.
Nunca elimine una observación automáticamente porque tenga Cook alto, leverage alto o residuo grande. Esos estadísticos son señales para investigar, no veredictos.
Antes de siquiera considerar una exclusión hay que preguntarse:
Y si, tras todo eso, se excluye una observación: se reporta, se explica por qué, y se muestran los resultados con y sin ella. Un análisis que esconde una exclusión no es reproducible.
No basta con decir “el supuesto no se cumple”. Hay que detectar, entender y tratar, y luego volver a diagnosticar.
| Problema | Cómo detectarlo | Posibles causas | Posibles tratamientos |
|---|---|---|---|
| No linealidad | Residuos vs. ajustados con curvatura; gráficos parciales con forma; loess no horizontal | Forma funcional incorrecta; variable omitida; relación multiplicativa | Transformar \(X\) (\(\log X\), \(\sqrt X\), \(X^2\)); términos polinómicos; transformar \(Y\); incluir la variable faltante; replantear el modelo (GAM, splines) |
| Heterocedasticidad | Embudo en residuos vs. ajustados; Scale-Location creciente; Breusch-Pagan significativo | \(Y\) es un conteo, monto o concentración; efecto multiplicativo; varianza que crece con la media | Transformar \(Y\) (\(\log Y\), \(\sqrt Y\)); errores estándar robustos (HC); mínimos cuadrados ponderados; modelos GLM (Poisson, Gamma) |
| No normalidad | Q-Q plot con colas desviadas; histograma asimétrico; Shapiro-Wilk significativo | Outliers; asimetría de \(Y\); modelo mal especificado; \(Y\) discreta o acotada | Revisar outliers primero; transformar; considerar el tamaño muestral (TLC); revisar la especificación; GLM si \(Y\) no es continua |
| Dependencia | DW lejos de 2 con orden con sentido; ACF de residuos; patrón por grupo | Serie de tiempo; medidas repetidas; conglomerados (colegio, municipio, operario) | Modelos con estructura de correlación (AR); errores estándar por conglomerado; modelos mixtos |
| Influencia | Cook alto; leverage alto; Residuals vs Leverage | Error de datos; caso excepcional; población distinta; modelo insuficiente | Verificar el dato; análisis de sensibilidad; comparar con y sin; regresión robusta; justificar toda exclusión |
| Multicolinealidad | VIF alto; correlaciones altas entre predictoras; coeficientes con signo “imposible”; EE enormes con \(R^2\) alto | Variables redundantes; muestra pequeña; diseño no separó las variables | Conocimiento del dominio; retirar una redundante con justificación; combinar variables en un índice; centrar (para polinomios/interacciones); regularización (ridge/lasso) como extensión |
trees: tratamos la no linealidadNuestro diagnóstico detectó un problema real: curvatura. Y tenemos una teoría sobre su origen. Un árbol se parece a un cilindro o a un cono:
\[V = \pi\left(\frac{d}{2}\right)^2 h \cdot c \qquad\Longrightarrow\qquad V \propto d^2\,h\]
Esto no es aditivo, es multiplicativo. Y un modelo multiplicativo se vuelve lineal tomando logaritmos:
\[\log V = \log c' + 2\log d + 1\log h + \epsilon\]
Predicción teórica antes de ajustar: el coeficiente de \(\log(Girth)\) debería salir cerca de 2 y el de \(\log(Height)\) cerca de 1.
modelo_log <- lm(log(Volume) ~ log(Girth) + log(Height), data = trees)
summary(modelo_log)
##
## Call:
## lm(formula = log(Volume) ~ log(Girth) + log(Height), data = trees)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.16856 -0.04849 0.00243 0.06364 0.12922
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -6.632 0.800 -8.29 0.0000000051 ***
## log(Girth) 1.983 0.075 26.43 < 0.0000000000000002 ***
## log(Height) 1.117 0.204 5.46 0.0000078053 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.0814 on 28 degrees of freedom
## Multiple R-squared: 0.978, Adjusted R-squared: 0.976
## F-statistic: 613 on 2 and 28 DF, p-value: <0.0000000000000002
confint(modelo_log)
## 2.5 % 97.5 %
## (Intercept) -8.26991 -4.9933
## log(Girth) 1.82900 2.1363
## log(Height) 0.69835 1.5359
El resultado es notable:
| Coeficiente | Estimación | IC 95 % | Valor teórico | ¿Compatible? |
|---|---|---|---|---|
| \(\log(Girth)\) | \(1.983\) | \([1.829,\ 2.136]\) | \(2\) | ✅ el IC contiene 2 |
| \(\log(Height)\) | \(1.117\) | \([0.698,\ 1.536]\) | \(1\) | ✅ el IC contiene 1 |
Los datos recuperan la geometría del sólido. Esto es lo que se ve cuando un modelo está bien especificado: los coeficientes dejan de ser números arbitrarios y adquieren significado sustantivo.
par(mfrow = c(2, 2))
plot(modelo_log, pch = 19, col = "steelblue")
par(mfrow = c(1, 1))
shapiro.test(rstudent(modelo_log))
##
## Shapiro-Wilk normality test
##
## data: rstudent(modelo_log)
## W = 0.957, p-value = 0.25
if (hay("lmtest")) print(lmtest::bptest(modelo_log))
##
## studentized Breusch-Pagan test
##
## data: modelo_log
## BP = 2.73, df = 2, p-value = 0.26
c(cook_maximo = max(cooks.distance(modelo_log)),
obs = which.max(cooks.distance(modelo_log)),
leverage_maximo = max(hatvalues(modelo_log)))
## cook_maximo obs.18 leverage_maximo
## 0.22356 18.00000 0.24277
La curvatura desapareció. Los residuos vs. ajustados ahora son una nube sin forma. Normalidad (\(p = 0.278\)) y homocedasticidad (\(p = 0.256\)) sin problemas. Y la distancia de Cook máxima bajó de \(0.605\) (obs. 31) a \(0.224\) (obs. 18): el punto influyente dejó de serlo.
🎯 Esta es la lección más importante de la clase. La observación 31 no era un dato malo. Era el dato que más claramente delataba que el modelo estaba mal especificado. Al corregir la forma funcional, el “problema” se disolvió solo. Antes de sospechar del dato, sospeche del modelo.
En un modelo log-log los coeficientes son elasticidades:
\[\hat\beta_j = \frac{\%\,\text{de cambio en } Y}{\%\,\text{de cambio en } X_j}\]
# Comparación en la ESCALA ORIGINAL (única comparación justa entre los dos modelos)
pred_lineal <- fitted(modelo)
pred_log <- exp(fitted(modelo_log)) # se deshace la transformación
data.frame(
modelo = c("Lineal: Volume ~ Girth + Height", "Log-log: log(V) ~ log(G) + log(H)"),
RMSE_pies3 = round(c(sqrt(mean((trees$Volume - pred_lineal)^2)),
sqrt(mean((trees$Volume - pred_log)^2))), 4),
EAM_pies3 = round(c(mean(abs(trees$Volume - pred_lineal)),
mean(abs(trees$Volume - pred_log))), 4)
)
El error de predicción baja de 3.69 a 2.42 pies³: una mejora del 35 %.
⚠️ No compare los \(R^2\) de estos dos modelos. Uno explica la variabilidad de
Volumey el otro la delog(Volume): son variables respuesta distintas y los \(R^2\) no son comparables. La comparación válida es en la escala original, como arriba.
\[\textbf{MODELO} \rightarrow \textbf{DIAGNÓSTICO} \rightarrow \textbf{TRATAMIENTO} \rightarrow \textbf{NUEVO MODELO} \rightarrow \textbf{NUEVO DIAGNÓSTICO}\]
En nuestro caso:
Volume ~ Girth + Height. \(R^2 = 0.948\), todo significativo.log(Volume) ~ log(Girth) + log(Height). Coeficientes ≈ 2 y
≈ 1, coherentes con la geometría.Hasta ahora todas las predictoras eran numéricas. ¿Qué pasa si una es una categoría (sexo, región, tipo de suelo, presencia de piscina)?
Conjunto pequeño y didáctico (construido para esta
clase) sobre precios de vivienda: Precio en millones,
Area en m², y Piscina con tres niveles.
viviendas <- data.frame(
Precio = c(105, 112, 134, 137, 141, 164, 166, 176, 175, 191, 190, 200, 245, 227, 270),
Area = c( 60, 72, 85, 78, 95, 102, 88, 110, 125, 105, 130, 140, 148, 160, 170),
Piscina = c("Sin","Sin","Sin","Pequena","Sin","Pequena","Grande","Pequena",
"Sin","Grande","Pequena","Sin","Grande","Pequena","Grande")
)
viviendas$Piscina <- factor(viviendas$Piscina, levels = c("Sin", "Pequena", "Grande"))
str(viviendas)
## 'data.frame': 15 obs. of 3 variables:
## $ Precio : num 105 112 134 137 141 164 166 176 175 191 ...
## $ Area : num 60 72 85 78 95 102 88 110 125 105 ...
## $ Piscina: Factor w/ 3 levels "Sin","Pequena",..: 1 1 1 2 1 2 3 2 1 3 ...
table(viviendas$Piscina)
##
## Sin Pequena Grande
## 6 5 4
La tentación es reemplazar Sin → 1,
Pequena → 2, Grande → 3 y meterla como
numérica. Eso impone dos supuestos falsos:
viviendas$Cod <- as.numeric(viviendas$Piscina) # 1, 2, 3 -- NO HACER ESTO
m_malo <- lm(Precio ~ Area + Cod, data = viviendas)
m_bueno <- lm(Precio ~ Area + Piscina, data = viviendas)
data.frame(
enfoque = c("Codificación 1-2-3 (incorrecta)", "Variables dummy (correcta)"),
parametros = c(length(coef(m_malo)), length(coef(m_bueno))),
sigma = round(c(summary(m_malo)$sigma, summary(m_bueno)$sigma), 3),
R2_ajustado = round(c(summary(m_malo)$adj.r.squared, summary(m_bueno)$adj.r.squared), 4)
)
El modelo correcto tiene un error residual menor (\(4.01\) vs. \(5.38\)) pese a gastar un parámetro más: los saltos reales no son iguales, y forzarlos a serlo cuesta ajuste.
Con \(m\) niveles se crean \(m-1\) variables indicadoras y un nivel
queda como referencia (aquí Sin, por ser
el primer nivel del factor). R lo hace automáticamente con
lm():
head(model.matrix(m_bueno), 8)
## (Intercept) Area PiscinaPequena PiscinaGrande
## 1 1 60 0 0
## 2 1 72 0 0
## 3 1 85 0 0
## 4 1 78 1 0
## 5 1 95 0 0
## 6 1 102 1 0
## 7 1 88 0 1
## 8 1 110 1 0
Cada fila tiene un 1 en la columna de su categoría y 0 en las demás; las viviendas Sin piscina tienen 0 en ambas dummies —son el punto de partida.
summary(m_bueno)
##
## Call:
## lm(formula = Precio ~ Area + Piscina, data = viviendas)
##
## Residuals:
## Min 1Q Median 3Q Max
## -5.29 -3.55 1.69 3.03 4.27
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 31.2173 3.7755 8.27 0.0000047661128 ***
## Area 1.1780 0.0354 33.30 0.0000000000021 ***
## PiscinaPequena 10.9367 2.5292 4.32 0.0012 **
## PiscinaGrande 36.2954 2.8210 12.87 0.0000000566446 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 4.01 on 11 degrees of freedom
## Multiple R-squared: 0.994, Adjusted R-squared: 0.993
## F-statistic: 640 on 3 and 11 DF, p-value: 0.00000000000128
\[\widehat{Precio} = 31.22 + 1.178\,Area + 10.94\,\mathbb{1}[Pequena] + 36.30\,\mathbb{1}[Grande]\]
Area = 1.178: cada m² adicional agrega
1.18 millones a igual tipo de piscina.PiscinaPequena = 10.94: comparando dos
viviendas de la misma área, una con piscina pequeña
vale en promedio 10.94 millones más que una sin
piscina. El coeficiente es una diferencia respecto a la
referencia, no un valor absoluto.PiscinaGrande = 36.30: misma lectura.
Nótese que \(36.30 \neq 2\times10.94\):
los saltos no son iguales, exactamente lo que la
codificación 1-2-3 habría impuesto por la fuerza.Geométricamente: tres rectas paralelas (misma
pendiente en Area, distinto intercepto según la
piscina).
ggplot(viviendas, aes(Area, Precio, colour = Piscina)) +
geom_point(size = 3) +
geom_line(aes(y = fitted(m_bueno)), linewidth = 1) +
labs(title = "Variables dummy: tres rectas paralelas",
subtitle = "Mismo efecto de Area; distinto intercepto por categoría",
x = "Área (m²)", y = "Precio (millones)")
La elección de la referencia no cambia el modelo (mismos ajustados, mismo \(R^2\), misma \(F\)); solo cambia con quién se compara cada coeficiente.
viviendas$Piscina2 <- relevel(viviendas$Piscina, ref = "Grande")
m_ref <- lm(Precio ~ Area + Piscina2, data = viviendas)
round(coef(m_ref), 4)
## (Intercept) Area Piscina2Sin Piscina2Pequena
## 67.513 1.178 -36.295 -25.359
c(R2_original = summary(m_bueno)$r.squared, R2_nueva_ref = summary(m_ref)$r.squared)
## R2_original R2_nueva_ref
## 0.9943 0.9943
Para preguntar “¿importa la piscina?” no se miran las \(t\) individuales de las dummies —eso son comparaciones por pares. Se compara el modelo con y sin el factor completo:
anova(lm(Precio ~ Area, data = viviendas), m_bueno)
\(F = 85.2\) con 2 y 11 gl, \(p < 0.001\): el factor
Piscina en conjunto aporta.
Hasta ahora los efectos eran aditivos: el efecto de \(X_1\) era el mismo cualquiera fuese el valor de \(X_2\). A veces eso es falso.
\[Y = \beta_0 + \beta_1X_1 + \beta_2X_2 + \beta_3X_1X_2 + \epsilon\]
Reagrupando:
\[Y = \beta_0 + \underbrace{(\beta_1 + \beta_3X_2)}_{\text{efecto de } X_1}X_1 + \beta_2X_2 + \epsilon\]
El efecto de una variable depende del valor de la otra.
En trees esto tiene sentido físico: si \(V \propto d^2h\), el efecto de un pie más
de altura es mayor en un árbol grueso que en uno
delgado.
modelo_inter <- lm(Volume ~ Girth * Height, data = trees) # = Girth + Height + Girth:Height
summary(modelo_inter)
##
## Call:
## lm(formula = Volume ~ Girth * Height, data = trees)
##
## Residuals:
## Min 1Q Median 3Q Max
## -6.582 -1.067 0.303 1.564 4.665
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 69.3963 23.8358 2.91 0.00713 **
## Girth -5.8558 1.9213 -3.05 0.00511 **
## Height -1.2971 0.3098 -4.19 0.00027 ***
## Girth:Height 0.1347 0.0244 5.52 0.0000075 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 2.71 on 27 degrees of freedom
## Multiple R-squared: 0.976, Adjusted R-squared: 0.973
## F-statistic: 359 on 3 and 27 DF, p-value: <0.0000000000000002
El término Girth:Height es altamente significativo
(\(p < 0.001\)), confirmando que los
efectos no son aditivos.
b <- coef(modelo_inter)
data.frame(
Height = c(65, 76, 87),
efecto_de_Girth = round(b["Girth"] + b["Girth:Height"] * c(65, 76, 87), 3)
)
En un árbol de 65 pies, una pulgada más de diámetro vale \(\approx 2.9\) pies³; en uno de 87 pies, vale \(\approx 5.9\) pies³: más del doble.
malla_i <- expand.grid(Girth = seq(8, 21, length.out = 50), Height = c(65, 76, 87))
malla_i$pred <- predict(modelo_inter, malla_i)
ggplot(malla_i, aes(Girth, pred, colour = factor(Height))) +
geom_line(linewidth = 1) +
labs(title = "Interacción: las pendientes ya no son paralelas",
colour = "Height", x = "Girth", y = "Volume estimado")
Advertencias para la clase siguiente:
X1:X2, se deben mantener X1
y X2 (principio de jerarquía), aunque sus \(p\) individuales sean grandes.Girth ya
no es “el efecto de Girth”: es el efecto cuando
Height = 0, que aquí no significa nada.
Centrar las predictoras devuelve la
interpretabilidad.M0 <- lm(Volume ~ 1, data = trees)
M1 <- lm(Volume ~ Girth, data = trees)
M2 <- lm(Volume ~ Height, data = trees)
M3 <- lm(Volume ~ Girth + Height, data = trees)
M4 <- lm(log(Volume) ~ log(Girth) + log(Height), data = trees)
rmse_original <- function(m, log_escala = FALSE) {
pred <- if (log_escala) exp(fitted(m)) else fitted(m)
sqrt(mean((trees$Volume - pred)^2))
}
data.frame(
modelo = c("M0: Volume ~ 1", "M1: Volume ~ Girth", "M2: Volume ~ Height",
"M3: Volume ~ Girth + Height", "M4: log(V) ~ log(G) + log(H)"),
k_predictoras = c(0, 1, 1, 2, 2),
R2 = round(c(summary(M0)$r.squared, summary(M1)$r.squared, summary(M2)$r.squared,
summary(M3)$r.squared, summary(M4)$r.squared), 4),
R2_aj = round(c(summary(M0)$adj.r.squared, summary(M1)$adj.r.squared, summary(M2)$adj.r.squared,
summary(M3)$adj.r.squared, summary(M4)$adj.r.squared), 4),
sigma = round(c(summary(M0)$sigma, summary(M1)$sigma, summary(M2)$sigma,
summary(M3)$sigma, summary(M4)$sigma), 4),
AIC = round(c(AIC(M0), AIC(M1), AIC(M2), AIC(M3), AIC(M4)), 2),
RMSE_pies3 = round(c(rmse_original(M0), rmse_original(M1), rmse_original(M2),
rmse_original(M3), rmse_original(M4, log_escala = TRUE)), 4)
)
anova(M0, M1, M3) # modelos anidados: cada paso añade una variable
Volume.Girth) explica el 93.5 %: el
diámetro solo ya hace casi todo el trabajo. La prueba \(F\) contra M0 es abrumadora.Height) explica el 35.8 %:
mucho peor. La altura sola es un predictor pobre.Height a M1 y gana \(1.3\) puntos de \(R^2\) (\(p =
0.0145\)). Ganancia real pero modesta.No elija un modelo solo porque tenga el \(R^2\) más alto. Criterios que deben pesar tanto o más:
Decisión para trees: nos quedamos con
M4, el modelo log-log. Ajusta mejor, cumple los
supuestos, no tiene puntos influyentes y sus coeficientes recuperan la
geometría del problema.
Los materiales de esta clase (.Rmd, .R,
.ipynb) siguen la misma secuencia y el mismo archivo
trees.csv. Los resultados numéricos
coinciden. Esta tabla es la referencia de
equivalencias.
| Tarea | R | Python |
|---|---|---|
| Leer datos | read.csv("trees.csv") |
pd.read_csv("trees.csv") |
| Estructura | str(df) |
df.info() |
| Descriptivos | summary(df) |
df.describe() |
| Correlaciones | cor(df) |
df.corr() |
| Matriz de dispersión | GGally::ggpairs(df) / pairs(df) |
sns.pairplot(df) |
| Ajustar el modelo | lm(y ~ x1 + x2, data = d) |
smf.ols("y ~ x1 + x2", data=d).fit() |
| Resumen | summary(m) |
m.summary() |
| Coeficientes | coef(m) |
m.params |
| Errores estándar | summary(m)$coefficients[,2] |
m.bse |
| Estadísticos \(t\) / \(p\) | summary(m)$coefficients[,3:4] |
m.tvalues / m.pvalues |
| Intervalos de confianza | confint(m) |
m.conf_int() |
| \(R^2\) / \(R^2\) ajustado | summary(m)$r.squared / $adj.r.squared |
m.rsquared / m.rsquared_adj |
| \(\hat\sigma\) | summary(m)$sigma |
np.sqrt(m.mse_resid) |
| Estadístico \(F\) global | summary(m)$fstatistic |
m.fvalue, m.f_pvalue |
| Tabla ANOVA | anova(m) |
sm.stats.anova_lm(m, typ=1) |
| Comparar modelos anidados | anova(m1, m2) |
sm.stats.anova_lm(m1, m2) |
| AIC | AIC(m) |
m.aic |
| Ajustados | fitted(m) |
m.fittedvalues |
| Residuos ordinarios | residuals(m) |
m.resid |
| Residuos estandarizados | rstandard(m) |
infl.resid_studentized_internal |
| Residuos studentizados | rstudent(m) |
infl.resid_studentized_external |
| Leverage | hatvalues(m) |
infl.hat_matrix_diag |
| Distancia de Cook | cooks.distance(m) |
infl.cooks_distance[0] |
| Objeto de influencia | (integrado) | infl = m.get_influence() |
| Q-Q plot | qqnorm() + qqline() |
sm.qqplot(resid, line="q") |
| Shapiro-Wilk | shapiro.test(r) |
scipy.stats.shapiro(r) |
| Breusch-Pagan | lmtest::bptest(m) |
het_breuschpagan(m.resid, m.model.exog) |
| Durbin-Watson | lmtest::dwtest(m) |
durbin_watson(m.resid) (sin p-valor) |
| VIF | car::vif(m) |
variance_inflation_factor(X.values, i) |
| Predicción puntual | predict(m, newdata) |
m.predict(newdata) |
| IC + IP | predict(m, nd, interval="confidence"/"prediction") |
m.get_prediction(nd).summary_frame() |
| Variable categórica | factor automático en lm() |
C(var) o
C(var, Treatment(reference="...")) |
| Matriz de diseño | model.matrix(m) |
m.model.exog / patsy.dmatrix |
| Interacción | y ~ x1 * x2 |
"y ~ x1 * x2" |
pandas. Todas las tablas de
diagnóstico de esta clase usan una columna obs de 1 a 31
para que ambos materiales hablen el mismo idioma.lmtest::bptest() usa
por defecto la versión studentizada (Koenker), que es
exactamente la que devuelve het_breuschpagan() de
statsmodels. Coinciden (\(BP = 2.4681\), \(p = 0.2911\)). Si usa
bptest(m, studentize = FALSE) obtendrá \(1.7375\), que es la versión original —no
compare peras con manzanas.statsmodels.stats.stattools.durbin_watson() devuelve
solo el estadístico; lmtest::dwtest()
devuelve además el p-valor.anova(). En R es secuencial (tipo I)
por defecto; en statsmodels,
anova_lm(m, typ=1) replica ese comportamiento. Con
typ=2 o typ=3 los números cambian.summary() vs .summary().
El de statsmodels incluye de entrada Durbin-Watson,
Jarque-Bera, asimetría, curtosis y número de condición, que en R hay que
pedir aparte.trees.csv
trae una primera columna sin nombre con los números de fila. En R
aparece como X; en Python como Unnamed: 0.
Hay que eliminarla en ambos lados o entrará como
predictora.character
a factor automáticamente dentro de lm().
statsmodels también con fórmulas, pero es mejor ser
explícito con C(var); con sklearn hay que
codificar a mano.sklearn vs statsmodels.
sklearn.linear_model.LinearRegression ajusta el mismo
modelo pero no da errores estándar, ni \(t\), ni \(p\), ni intervalos: está pensado
para predicción, no para inferencia. Para esta clase, use
statsmodels.Si sus números difieren de estos, algo está mal en la lectura de datos:
| Cantidad | Valor |
|---|---|
| \(\hat\beta_0\) | \(-57.9877\) |
| \(\hat\beta_{Girth}\) | \(4.7082\) |
| \(\hat\beta_{Height}\) | \(0.3393\) |
| \(R^2\) | \(0.9480\) |
| \(R^2\) ajustado | \(0.9442\) |
| \(\hat\sigma\) | \(3.8818\) |
| \(F(2,28)\) | \(254.97\) |
| \(SCE\) | \(421.9214\) |
| Shapiro-Wilk (\(r^{*}\)) | \(W=0.9732\), \(p=0.6122\) |
| Breusch-Pagan | \(2.4681\), \(p=0.2911\) |
| VIF | \(1.3692\) |
| Cook máx. (obs. 31) | \(0.6052\) |
E1 (nivelación). Ajuste
Volume ~ Height. Interprete la pendiente. Luego ajuste
Volume ~ Girth + Height y compare el coeficiente de
Height en ambos. ¿Aumentó o disminuyó? Explique por qué
usando el argumento de la sección 3.
E2 (efecto parcial). Repita el procedimiento de
Frisch-Waugh-Lovell de la sección 3 pero para Height
(regrese Height contra Girth, y
Volume contra Girth, y cruce los residuos).
Verifique que obtiene \(0.3393\).
E3 (ANOVA). Construya la tabla ANOVA a mano para el
modelo Volume ~ Girth. Verifique que \(F = t^2\) donde \(t\) es el estadístico de la pendiente.
E4 (predicción). Un ingeniero forestal mide un árbol de \(Girth = 19\) pulgadas y \(Height = 85\) pies. Dé la predicción con el modelo lineal y con el log-log. ¿Cuál reportaría y con qué intervalo? Justifique.
E5 (extrapolación). Prediga el volumen de un árbol con \(Girth = 25\), \(Height = 90\). ¿Confía en el resultado? ¿Por qué?
E6 (diagnóstico). Ajuste Volume ~ Girth
(solo diámetro) y haga el diagnóstico completo. ¿El problema de
curvatura es peor o mejor que en el modelo con dos predictoras? ¿Qué le
dice eso?
E7 (influencia). Identifique la observación más influyente del modelo log-log y haga el análisis de sensibilidad correspondiente. ¿Cambia alguna conclusión?
E8 (transformación alternativa). Ajuste
Volume ~ I(Girth^2 * Height) —una sola predictora
construida a partir de la teoría del cono. Compare su RMSE con los de M3
y M4. Discuta.
E9 (categóricas). En el conjunto
viviendas, cambie la referencia a Pequena y
reinterprete los tres coeficientes. Verifique que los valores ajustados
son idénticos.
E10 (interacción). Ajuste
Precio ~ Area * Piscina en viviendas. ¿Qué
significaría que la interacción fuera significativa? Dibuje las
rectas.
E11 (aplicado). Tome un conjunto de datos de su
interés profesional con una respuesta continua y al menos dos
predictoras. Recorra el ciclo completo: pregunta → datos → modelo →
resultados → diagnóstico → decisión. Entregue un .Rmd o
.ipynb reproducible con una conclusión de máximo un párrafo
dirigida a alguien que no sabe estadística.
| Término | Definición operativa |
|---|---|
| Coeficiente parcial | Cambio promedio en \(Y\) por unidad de \(X_j\) con las demás predictoras del modelo fijas. |
| \(R^2\) | \(1 - SCE/SCT\). Proporción de variabilidad de \(Y\) explicada. Nunca baja al agregar predictoras. |
| \(R^2\) ajustado | \(R^2\) penalizado por el número de parámetros. Puede bajar. Úselo para comparar modelos con distinto \(k\). |
| \(\hat\sigma\) (RSE) | Desviación típica de los residuos, en unidades de \(Y\). Interpretable. |
| Leverage \(h_{ii}\) | Cuánto pesa \(y_i\) en su propio ajustado. Depende solo de las \(X\). Suma \(= p\); promedio \(= p/n\). |
| Residuo studentizado | Residuo en escala comparable, con \(\sigma\) estimado sin esa observación. Sigue una \(t_{n-p-1}\). |
| Distancia de Cook | Cuánto se mueven todos los ajustados si se quita la observación \(i\). Combina residuo y leverage. |
| VIF | \(1/(1-R_j^2)\). Factor por el que se infla la varianza del coeficiente \(j\) por colinealidad. \(\sqrt{VIF}\) infla el error estándar. |
| Intervalo de confianza | Para la media de \(Y\) en \(x_0\). Se angosta con \(n\). |
| Intervalo de predicción | Para una observación en \(x_0\). Siempre más ancho; tiene piso \(\approx\pm1.96\hat\sigma\). |
| Homocedasticidad | Varianza del error constante a lo largo de los ajustados. |
| Elasticidad | En modelos log-log, el coeficiente es el % de cambio en \(Y\) por 1 % de cambio en \(X\). |
car]trees]sessionInfo()
## R version 4.5.2 (2025-10-31 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26200)
##
## Matrix products: default
## LAPACK version 3.12.1
##
## locale:
## [1] LC_COLLATE=Spanish_Colombia.utf8 LC_CTYPE=Spanish_Colombia.utf8
## [3] LC_MONETARY=Spanish_Colombia.utf8 LC_NUMERIC=C
## [5] LC_TIME=Spanish_Colombia.utf8
##
## time zone: America/Bogota
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] broom_1.0.11 dplyr_1.1.4 ggplot2_4.0.1
##
## loaded via a namespace (and not attached):
## [1] sass_0.4.10 generics_0.1.4 tidyr_1.3.2 stringi_1.8.7
## [5] lattice_0.22-7 digest_0.6.38 magrittr_2.0.4 evaluate_1.0.5
## [9] grid_4.5.2 RColorBrewer_1.1-3 fastmap_1.2.0 Matrix_1.7-4
## [13] jsonlite_2.0.0 backports_1.5.0 Formula_1.2-5 gridExtra_2.3
## [17] mgcv_1.9-3 GGally_2.4.0 purrr_1.2.0 scales_1.4.0
## [21] ggfortify_0.4.19 jquerylib_0.1.4 abind_1.4-8 cli_3.6.5
## [25] rlang_1.1.6 splines_4.5.2 withr_3.0.2 cachem_1.1.0
## [29] yaml_2.3.10 tools_4.5.2 ggstats_0.14.0 vctrs_0.6.5
## [33] R6_2.6.1 zoo_1.8-15 lifecycle_1.0.4 stringr_1.6.0
## [37] car_3.1-5 pkgconfig_2.0.3 pillar_1.11.1 bslib_0.9.0
## [41] gtable_0.3.6 glue_1.8.0 xfun_0.55 tibble_3.3.0
## [45] lmtest_0.9-40 tidyselect_1.2.1 rstudioapi_0.18.0 knitr_1.50
## [49] farver_2.1.2 htmltools_0.5.8.1 nlme_3.1-168 labeling_0.4.3
## [53] rmarkdown_2.30 carData_3.0-6 compiler_4.5.2 S7_0.2.0