biomasa ~ K?Los resultados obtenidos en R y en Python coinciden porque ambos
lenguajes ajustan el modelo de regresión lineal múltiple con el mismo
método, el de mínimos cuadrados ordinarios, cuyo estimador β̂ =
(XᵀX)⁻¹Xᵀy es único cuando las variables no son perfectamente
colineales, y lo aplican sobre exactamente los mismos datos. Como la
estimación de los coeficientes, la varianza residual σ̂², los errores
estándar, el estadístico F global, el R² y los intervalos de confianza y
de predicción (que se construyen con la misma distribución t de Student
y los mismos grados de libertad) dependen únicamente de esos datos y de
esas fórmulas, ambos programas deben entregar los mismos valores. Las
únicas diferencias observables son de redondeo numérico, del orden de
10⁻¹² o menores, debidas a los algoritmos de álgebra lineal internos de
cada implementación, y de formato de presentación, como la forma de
mostrar p-valores muy pequeños (<2e-16 en R frente a
0.000 en Python). Por tanto, la coincidencia confirma que
la implementación es correcta y que el análisis es reproducible
independientemente del software utilizado.
# --- Lectura y estructura ---
ventas <- read.table("ventas.txt", header = TRUE)
cat("Dimensiones:", dim(ventas), "\n")
## Dimensiones: 180 4
kable(head(ventas), caption = "Primeras filas del dataset (ventas)") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| tv | radio | periodico | ventas |
|---|---|---|---|
| 230.1 | 37.8 | 69.2 | 22.1 |
| 44.5 | 39.3 | 45.1 | 10.4 |
| 17.2 | 45.9 | 69.3 | 9.3 |
| 151.5 | 41.3 | 58.5 | 18.5 |
| 180.8 | 10.8 | 58.4 | 12.9 |
| 8.7 | 48.9 | 75.0 | 7.2 |
# --- Datos faltantes ---
kable(t(colSums(is.na(ventas))), caption = "Valores faltantes por variable") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| tv | radio | periodico | ventas |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
# --- Descriptivos ---
desc1 <- data.frame(
media = sapply(ventas, mean),
sd = sapply(ventas, sd),
cv = sapply(ventas, function(x) sd(x) / mean(x)),
min = sapply(ventas, min),
max = sapply(ventas, max)
)
kable(round(desc1, 4), caption = "Estadísticos descriptivos: Publicidad y Ventas") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| media | sd | cv | min | max | |
|---|---|---|---|---|---|
| tv | 145.2989 | 87.2465 | 0.6005 | 0.7 | 296.4 |
| radio | 24.0583 | 15.0130 | 0.6240 | 0.0 | 49.6 |
| periodico | 31.3228 | 22.0205 | 0.7030 | 0.3 | 114.0 |
| ventas | 14.0789 | 5.3873 | 0.3826 | 1.6 | 27.0 |
library(ggcorrplot)
# --- Matriz de correlaciones (Gráfico) ---
ggcorrplot(cor(ventas),
hc.order = FALSE,
type = "lower",
lab = TRUE,
lab_size = 4.5,
method = "square",
colors = c("#2166ac", "#f7f7f7", "#b2182b"),
title = "Matriz de Correlación: Publicidad y Ventas",
ggtheme = theme_minimal())
# --- Dispersión ventas vs. cada predictor ---
par(mfrow = c(1, 3))
for (v in c("tv", "radio", "periodico")) {
plot(ventas[[v]], ventas$ventas,
xlab = paste(v, "(millones)"), ylab = "ventas (millones)",
main = paste("ventas vs.", v), pch = 19, col = "steelblue")
abline(lm(ventas$ventas ~ ventas[[v]]), col = "red", lwd = 2)
}
par(mfrow = c(1, 1))
# --- Atípicos: boxplots y conteo por regla 1.5*IQR ---
par(mfrow = c(1, 4))
for (v in names(ventas)) boxplot(ventas[[v]], main = v, col = "lightblue")
par(mfrow = c(1, 1))
atipicos1 <- t(as.data.frame(sapply(ventas, function(x) length(boxplot.stats(x)$out))))
rownames(atipicos1) <- "Nº atípicos"
kable(atipicos1, caption = "Número de valores atípicos por variable (regla 1.5·IQR)") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| tv | radio | periodico | ventas | |
|---|---|---|---|---|
| Nº atípicos | 0 | 0 | 2 | 0 |
Comentarios:
Vemos que el dataset tiene 180 observaciones donde la variable respuesta es ventas (expresado en millones) y las variables explicativas son el presupuesto destinado a la publicidad en la radio, periodico y ventas. Vemos que no hay ningun dato faltante y que la mayor media (por una diferencia mayor de 100 millones) la tiene el presupuesto destinado a la televisión con un poco mas de 145 millones. El rango de publicidad televisiva es el mayor también. Esto es lógico teniendo en cuenta que la televisión es una herramienta donde la publicidad está mas valorada que en las demás herramientas.
La matriz de correlaicones nos indica que hay correlación positiva entre todas las variables, con un grado mayor entre ventas y las variables explicativas comparando con la correlación entre las variables explicativas ensi. Lo principal que vemos en las graficas de dispersion es que todas las variables tienen una asociación lineal positiva. Esto es importante, no tendría sentido que invertir en publicidad se asocie con una disminución en ventas. La mayor pendiente lo tiene la televisión. Aparte de eso, vemos que en la radio y más en el periódico, la nube de puntos es muscho más dispersa y que con el mismo dinero invertido se obtienen unos resultados distintos, luego hay una menor correlación con ventas.
Cabe destacar que esto es analisis NO es la de la regresión múltiple y que solo coincidiría con el si los predictores no estuvieran correlaiconados entre si. Que como hemos visto no ocurre así.
En cuanto a valores atípicos, hay únicamente 2 y son las dos en la variable periódico.
El modelo ajustado es
\[\text{ventas}_i = \beta_0 + \beta_1\,\text{tv}_i + \beta_2\,\text{radio}_i + \beta_3\,\text{periodico}_i + \varepsilon_i, \qquad \varepsilon_i \overset{iid}{\sim} N(0,\sigma^2).\]
mod1 <- lm(ventas ~ tv + radio + periodico, data = ventas)
# Tabla de coeficientes del modelo
coef_mod1 <- as.data.frame(summary(mod1)$coefficients)
names(coef_mod1) <- c("Estimación", "Error Estándar", "t", "p-valor")
kable(round(coef_mod1, 5), caption = "Coeficientes del modelo: ventas ~ tv + radio + periodico") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE) |>
column_spec(5, bold = TRUE, color = ifelse(coef_mod1$`p-valor` < 0.05, "green", "red"))
| Estimación | Error Estándar | t | p-valor | |
|---|---|---|---|---|
| (Intercept) | 2.76972 | 0.32929 | 8.41130 | 0.00000 |
| tv | 0.04716 | 0.00145 | 32.48482 | 0.00000 |
| radio | 0.18791 | 0.00894 | 21.02306 | 0.00000 |
| periodico | -0.00205 | 0.00609 | -0.33572 | 0.73748 |
# Estadísticos globales
gl_mod1 <- data.frame(
`R²` = summary(mod1)$r.squared,
`R² ajustado` = summary(mod1)$adj.r.squared,
`Sigma (RSE)` = summary(mod1)$sigma,
`F global` = summary(mod1)$fstatistic[1],
check.names = FALSE
)
kable(round(gl_mod1, 6), caption = "Bondad de ajuste global") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| R² | R² ajustado | Sigma (RSE) | F global | |
|---|---|---|---|---|
| value | 0.90324 | 0.901591 | 1.690005 | 547.644 |
Interpretación:
Intercepto (β̂₀): Las ventas esperadas cuando tv, radio y periodico son 0 son 2.77. Esto indica que sin ninguna publicidad las ventas serían positivas (resultado intepretable por lógica).
β̂₁ (tv): manteniendo constantes radio y periodico, un aumento de 1 millón en la inversión en TV se asocia con un aumento de 47.160 € en las ventas esperadas.
β̂₂ (radio): manteniendo constantes televisión y periodico, un aumento de 1 millón en la inversión en TV se asocia con un aumento de 187.910 € en las ventas esperadas.
β̂₃ (periodico): manteniendo constantes televisión y radio, un aumento de 1 millón en la inversión en TV se asocia con una disminución de 2.050 € en las ventas esperadas.
NOTA IMPORTANTE: Este resultado no es estadísticamente significativo (a diferencia de las dos anteriores) ya que su p-valor es 0.737. Luego no deberíamos tomar en cuenta esta interpretación.
Contraste t individual (cada coeficiente \(\beta_j\)):
\[H_0:\beta_j = 0 \quad \text{frente a} \quad H_1:\beta_j \neq 0, \qquad j = 1, 2, 3.\]
Los p-valores de \(\hat\beta_1\) (tv) y \(\hat\beta_2\) (radio) son \(\approx 0\), por lo que se rechaza \(H_0\) para ambos. El de \(\hat\beta_3\) (periódico) es \(0.737 > 0.05\), no se rechaza \(H_0\): periódico no tiene efecto significativo.
Contraste F global:
\[H_0:\beta_1 = \beta_2 = \beta_3 = 0 \quad \text{frente a} \quad H_1:\exists\, j \text{ tal que } \beta_j \neq 0.\]
Con \(F = 547.6\) y p-valor \(\approx 0\), se rechaza \(H_0\): al menos una variable explica la variación de las ventas.
R² ajustado: el modelo explica aproximadamente 90.16% de la variabilidad de las ventas. El no ajustado tiene un valor de 90.32
Las sumas de cuadrados se definen como:
\[SST = \sum_{i=1}^{n}(y_i - \bar{y})^2, \qquad SSR = \sum_{i=1}^{n}(\hat{y}_i - \bar{y})^2, \qquad SSE = \sum_{i=1}^{n}(y_i - \hat{y}_i)^2\]
y el coeficiente de determinación se obtiene como:
\[R^2 = \frac{SSR}{SST} = 1 - \frac{SSE}{SST}\]
Con los valores de este modelo (\(n = 180\), \(k = 3\) predictores, \(p = k+1 = 4\) parámetros):
\[R^2 = \frac{4{,}692.40}{5{,}195.08} = 1 - \frac{502.68}{5{,}195.08} = 0.9032\]
El \(R^2\) ajustado penaliza por el número de parámetros para evitar sobreajuste:
\[R^2_{\text{adj}} = 1 - \frac{SSE/(n-p)}{SST/(n-1)}\]
Con los valores concretos:
\[R^2_{\text{adj}} = 1 - \frac{502.68\,/\,(180-4)}{5{,}195.08\,/\,(180-1)} = 1 - \frac{502.68/176}{5{,}195.08/179} = 1 - \frac{2.8561}{29.0228} = 0.9016\]
El estadístico F global contrasta si algún predictor es útil:
\[F = \frac{SSR/k}{SSE/(n-p)}\]
Con los valores concretos (\(k=3\) predictores):
\[F = \frac{SSR/k}{SSE/(n-p)} = \frac{4{,}692.40\,/\,3}{502.68\,/\,(180-4)} = \frac{1{,}564.13}{2.856} = 547.64\]
Este valor de \(F\) tiene una distribución \(F_{k,\,n-p} = F_{3,\,176}\) bajo \(H_0\), dando un p-valor \(\approx 0\).
Se busca un intervalo de confianza para
\[E(\text{ventas} \mid \text{tv}=147.05,\ \text{radio}=23.3,\ \text{periodico}=31) = \beta_0 + 147.05\,\beta_1 + 23.3\,\beta_2 + 31\,\beta_3 .\]
x0 <- data.frame(tv = 147.05, radio = 23.3, periodico = 31)
ic_r <- predict(mod1, newdata = x0, interval = "confidence", level = 0.95)
ic_df <- data.frame(Estimación = ic_r[,"fit"], `Límite inf. (95%)` = ic_r[,"lwr"],
`Límite sup. (95%)` = ic_r[,"upr"], check.names = FALSE)
kable(round(ic_df, 5), caption = "Intervalo de confianza al 95 % para la respuesta media (tv=147.05, radio=23.3, periodico=31)") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| Estimación | Límite inf. (95%) | Límite sup. (95%) |
|---|---|---|
| 14.01963 | 13.77065 | 14.26861 |
# --- Cálculo manual (verificación) ---
x0v <- c(1, 147.05, 23.3, 31)
X <- model.matrix(mod1)
s2 <- summary(mod1)$sigma^2
gl <- df.residual(mod1)
est <- sum(x0v * coef(mod1))
ee <- sqrt(s2 * as.numeric(t(x0v) %*% solve(t(X) %*% X) %*% x0v))
tcrit <- qt(0.975, gl)
manual_ic <- data.frame(
Estimación = est,
`Error Estándar` = ee,
`t crítico` = tcrit,
`Límite inf.` = est - tcrit * ee,
`Límite sup.` = est + tcrit * ee,
check.names = FALSE
)
kable(round(manual_ic, 5), caption = "Verificación manual del IC al 95 %") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| Estimación | Error Estándar | t crítico | Límite inf. | Límite sup. |
|---|---|---|---|---|
| 14.01963 | 0.12616 | 1.97353 | 13.77065 | 14.26861 |
Interpretación: con un nivel de confianza del 95 %, el ingreso medio por ventas de las regiones que invierten 147.05 millones en TV, 23.3 en radio y 31 en periódicos se estima entre 13.77 y 14.27 millones, con una estimación puntual de 14.02 millones. Es importante destcar que es un intervalo para la media de ventas. Queda además verificado manualmente. Además vemos que en Python el resultado es el mismo quitando las diferencias de redondeo.
Para una nueva sucursal con \(\text{tv}=160\), \(\text{radio}=26.7\) y \(\text{periodico}=35.1\) se busca la predicción puntual \(\widehat{\text{ventas}}\) y un intervalo de predicción del 95 % para el valor de sus ventas.
x1 <- data.frame(tv = 160, radio = 26.7, periodico = 35.1)
# Intervalo de predicción (lo que pide el apartado)
ip_r <- predict(mod1, newdata = x1, interval = "prediction", level = 0.95)
ip_df <- data.frame(Predicción = ip_r[,"fit"], `Límite inf. (95%)` = ip_r[,"lwr"],
`Límite sup. (95%)` = ip_r[,"upr"], check.names = FALSE)
kable(round(ip_df, 5), caption = "Intervalo de predicción al 95 % (tv=160, radio=26.7, periodico=35.1)") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| Predicción | Límite inf. (95%) | Límite sup. (95%) |
|---|---|---|
| 15.26088 | 11.91571 | 18.60605 |
# IC de la media en el mismo punto (para el apartado e)
ic_x1_r <- predict(mod1, newdata = x1, interval = "confidence", level = 0.95)
ancho_IP <- unname(ip_r[, "upr"] - ip_r[, "lwr"])
ancho_IC <- unname(ic_x1_r[, "upr"] - ic_x1_r[, "lwr"])
comp_df <- data.frame(
Intervalo = c("IC (confianza)", "IP (predicción)"),
Estimación = c(ic_x1_r[,"fit"], ip_r[,"fit"]),
`Lím. inf.` = c(ic_x1_r[,"lwr"], ip_r[,"lwr"]),
`Lím. sup.` = c(ic_x1_r[,"upr"], ip_r[,"upr"]),
Anchura = c(ancho_IC, ancho_IP),
check.names = FALSE
)
comp_df[, sapply(comp_df, is.numeric)] <- round(comp_df[, sapply(comp_df, is.numeric)], 5)
kable(comp_df, caption = "Comparación IC vs. IP en el mismo punto (tv=160, radio=26.7, periodico=35.1)") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| Intervalo | Estimación | Lím. inf. | Lím. sup. | Anchura |
|---|---|---|---|---|
| IC (confianza) | 15.26088 | 15.00383 | 15.51792 | 0.51409 |
| IP (predicción) | 15.26088 | 11.91571 | 18.60605 | 6.69034 |
# --- Cálculo manual (verificación) ---
x1v <- c(1, 160, 26.7, 35.1)
X <- model.matrix(mod1)
s2 <- summary(mod1)$sigma^2
gl <- df.residual(mod1)
h <- as.numeric(t(x1v) %*% solve(t(X) %*% X) %*% x1v)
est <- sum(x1v * coef(mod1))
ee_p <- sqrt(s2 * (1 + h))
tcrit <- qt(0.975, gl)
manual_ip <- data.frame(
Predicción = est,
`Error Estándar` = ee_p,
`t crítico` = tcrit,
`Límite inf.` = est - tcrit * ee_p,
`Límite sup.` = est + tcrit * ee_p,
check.names = FALSE
)
kable(round(manual_ip, 5), caption = "Verificación manual del IP al 95 %") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| Predicción | Error Estándar | t crítico | Límite inf. | Límite sup. |
|---|---|---|---|---|
| 15.26088 | 1.69502 | 1.97353 | 11.91571 | 18.60605 |
Interpretación: La predicción puntual de ventas para la nueva sucursal es de 15.261 millones. Con un 95 % de confianza, se espera que las ventas de esa sucursal concreta se encuentren entre 11.92 y 18.61 millones. Este intervalo se refiere al valor de una observación nueva, no a la media de ventas de un grupo de regiones, por lo que es más ancho que el intervalo de confianza del apartado c). Vemos que la diferencia entre anchuras es más de 6.1 millones de euros.
NOTA: Vemos que la función predict es la misma para los dos pero interval en el de confianza toma el valor “confidence” y en la de la predicción “prediction”.
Los intervalos de los apartados c) y d) responden a preguntas distintas.
Intervalo de confianza (apartado c).
Estima \(E(Y \mid X = x_0) = x_0^\top \beta\), la media de ventas de todas las regiones con esa inversión. Es un parámetro fijo, aunque desconocido; el intervalo cuantifica solo la incertidumbre por haber estimado \(\beta\) con una muestra finita. Su varianza es \[\operatorname{Var}(\hat{Y}_0) = \sigma^2\, x_0^\top (X^\top X)^{-1} x_0 .\]
Intervalo de predicción (apartado d). Cubre \(Y_{\text{nuevo}} \mid X = x_0\), el valor de una concreta observación futura, que es una variable aleatoria: \(Y_{\text{nuevo}} = x_0^\top \beta + \varepsilon_{\text{nuevo}}\). El error de predicción \(Y_{\text{nuevo}} - \hat{Y}_0\) tiene dos fuentes independientes de variabilidad: la incertidumbre al estimar la recta y el error aleatorio \(\varepsilon_{\text{nuevo}}\) de la nueva observación. Por eso \[\operatorname{Var}(Y_{\text{nuevo}} - \hat{Y}_0) = \sigma^2 + \sigma^2\, x_0^\top (X^\top X)^{-1} x_0 = \sigma^2\left[1 + x_0^\top (X^\top X)^{-1} x_0\right].\]
Ambos intervalos comparten el término \(\sigma^2 x_0^\top (X^\top X)^{-1} x_0\), pero el de predicción suma además \(\sigma^2\), la variabilidad inevitable de una observación individual alrededor de la media, ya que es una variable aleatoria. Como esa variabilidad no desaparece aunque se conozca perfectamente la recta, el IP es siempre más ancho que el IC en el mismo punto. Además, al aumentar el tamaño muestral el término \(x_0^\top (X^\top X)^{-1} x_0\) tiende a 0, de modo que el IC se contrae hacia un punto, mientras que el IP se estabiliza en torno a \(\pm t\,\sigma\).
suelo <- read.table("datos.txt", header = TRUE)
cat("Dimensiones:", dim(suelo), "\n")
## Dimensiones: 40 6
kable(head(suelo), caption = "Primeras filas del dataset (suelo)") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| biomasa | salinidad | pH | K | Na | Zn |
|---|---|---|---|---|---|
| 676 | 33 | 5.00 | 1441.67 | 35184.5 | 16.4524 |
| 516 | 35 | 4.75 | 1299.19 | 28170.4 | 13.9852 |
| 1052 | 32 | 4.20 | 1154.27 | 26455.0 | 15.3276 |
| 868 | 30 | 4.40 | 1045.15 | 25072.9 | 17.3128 |
| 436 | 33 | 5.05 | 1273.02 | 25491.7 | 12.2778 |
| 544 | 36 | 4.25 | 1346.35 | 20877.3 | 17.8225 |
Vemos como tenemos un dataset de 40 observaciones con 5 variables explicativas: salinidad(%), pH, potasio(ppm), Sodio(ppm) y Zinc(ppm) que se utilizan para identificar características del suelo que pueden influir sobre la biomasa Y de una especie de pasto. Cabe destacar como y ahe indicado en paentesis que las variables están medidos en unidades distintos y no comparables sin una normalización previa.
# --- Datos faltantes ---
kable(t(colSums(is.na(suelo))), caption = "Valores faltantes por variable") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| biomasa | salinidad | pH | K | Na | Zn |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 |
# --- Descriptivos ---
desc <- data.frame(
media = sapply(suelo, mean),
sd = sapply(suelo, sd),
cv = sapply(suelo, function(x) sd(x) / mean(x)),
min = sapply(suelo, min),
max = sapply(suelo, max)
)
kable(round(desc, 3), caption = "Estadísticos descriptivos: Características del Suelo") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| media | sd | cv | min | max | |
|---|---|---|---|---|---|
| biomasa | 996.900 | 681.755 | 0.684 | 236.00 | 2436.000 |
| salinidad | 30.175 | 3.679 | 0.122 | 24.00 | 38.000 |
| pH | 4.541 | 1.219 | 0.268 | 3.20 | 7.450 |
| K | 804.364 | 293.826 | 0.365 | 350.73 | 1441.670 |
| Na | 16213.448 | 6606.895 | 0.407 | 7886.50 | 35184.500 |
| Zn | 18.176 | 8.220 | 0.452 | 0.21 | 31.286 |
Vemos como no hay ningun valor faltante (na). Además de eso, lo mas significativo es que el mayor rango lo tiene el sodio (Na).
library(ggcorrplot)
# --- Matriz de correlaciones (Gráfico) ---
ggcorrplot(cor(suelo),
hc.order = FALSE,
type = "lower",
lab = TRUE,
lab_size = 4,
method = "square",
colors = c("#2166ac", "#f7f7f7", "#b2182b"),
title = "Matriz de Correlación: Características del Suelo y Biomasa",
ggtheme = theme_minimal())
En cuanto a las correlaciones, en este dataset vemos que varían mucho más que en la anterior. Los más signficativos (con una relación mas fuerte) son el sodio y el potasio, con una correlación del 0.9, la biomasa y el pH, con 0.82, lo cual significa que ap riori será un estimado rmuy bueno por si solo, y por último, vemos que el Zinc tuene una relación negativa bastante alta tanto con la biomasa, la salinidad y el pH. Viendo estas correlaciones, se cofirma lo que he dicho anteriormente.
library(ggplot2)
library(patchwork)
g1 <- ggplot(suelo, aes(pH, biomasa)) +
geom_point(color = "steelblue", alpha = 0.7) +
geom_smooth(method = "lm", color = "red") +
labs(title = "biomasa vs. pH", x = "pH", y = "biomasa")
g2 <- ggplot(suelo, aes(K, biomasa)) +
geom_point(color = "steelblue", alpha = 0.7) +
geom_smooth(method = "lm", color = "red") +
labs(title = "biomasa vs. K (potasio)", x = "K (ppm)", y = "biomasa")
g3 <- ggplot(suelo, aes(pH, K)) +
geom_point(color = "steelblue", alpha = 0.7) +
geom_smooth(method = "lm", color = "red") +
labs(title = "K vs. pH", x = "pH", y = "K (ppm)")
g1 + g2 + g3
# Matriz de dispersión de las tres variables de interés
pairs(suelo[, c("biomasa", "pH", "K")], pch = 19, col = "steelblue")
# Correlaciones de interés para la interpretación
cor_clave <- data.frame(
Par = c("biomasa vs. pH", "biomasa vs. K", "pH vs. K"),
`Correlación (r)` = c(cor(suelo$biomasa, suelo$pH),
cor(suelo$biomasa, suelo$K),
cor(suelo$pH, suelo$K)),
check.names = FALSE
)
cor_clave[, sapply(cor_clave, is.numeric)] <- round(cor_clave[, sapply(cor_clave, is.numeric)], 5)
kable(cor_clave, caption = "Correlaciones clave entre variables") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| Par | Correlación (r) |
|---|---|
| biomasa vs. pH | 0.81871 |
| biomasa vs. K | -0.14870 |
| pH vs. K | 0.09228 |
Interpretación:
Se ajusta el modelo
\[\text{biomasa}_i = \beta_0 + \beta_2\,\text{pH}_i + \varepsilon_i, \qquad \varepsilon_i \overset{iid}{\sim} N(0,\sigma^2).\]
m_simple <- lm(biomasa ~ pH, data = suelo)
# Tabla de coeficientes
coef_s <- as.data.frame(summary(m_simple)$coefficients)
names(coef_s) <- c("Estimación", "Error Estándar", "t", "p-valor")
kable(round(coef_s, 6), caption = "Coeficientes del modelo simple: biomasa ~ pH") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE) |>
column_spec(5, bold = TRUE, color = ifelse(coef_s$`p-valor` < 0.05, "green", "red"))
| Estimación | Error Estándar | t | p-valor | |
|---|---|---|---|---|
| (Intercept) | -1083.2272 | 244.83514 | -4.424313 | 7.9e-05 |
| pH | 458.0517 | 52.11537 | 8.789186 | 0.0e+00 |
# --- Coeficientes con IC 95 % ---
tabla_coef <- cbind(
Estimación = coef(m_simple),
confint(m_simple),
`p-valor` = summary(m_simple)$coefficients[, 4]
)
kable(round(tabla_coef, 6), caption = "Coeficientes e IC 95 %: biomasa ~ pH") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| Estimación | 2.5 % | 97.5 % | p-valor | |
|---|---|---|---|---|
| (Intercept) | -1083.2272 | -1578.8700 | -587.5844 | 7.9e-05 |
| pH | 458.0517 | 352.5496 | 563.5537 | 0.0e+00 |
# --- Ajuste global ---
f <- summary(m_simple)$fstatistic
gl_s <- data.frame(
`R²` = summary(m_simple)$r.squared,
`R² ajustado` = summary(m_simple)$adj.r.squared,
`Sigma (RSE)` = summary(m_simple)$sigma,
`F global` = unname(f[1]),
`p-valor F` = pf(f[1], f[2], f[3], lower.tail = FALSE),
check.names = FALSE
)
kable(round(gl_s, 6), caption = "Bondad de ajuste global: biomasa ~ pH") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| R² | R² ajustado | Sigma (RSE) | F global | p-valor F | |
|---|---|---|---|---|---|
| value | 0.670281 | 0.661605 | 396.5888 | 77.24979 | 0 |
library(ggplot2)
ggplot(suelo, aes(pH, biomasa)) +
geom_point(color = "steelblue", alpha = 0.7) +
geom_smooth(method = "lm", color = "red") +
labs(title = "Recta ajustada: biomasa ~ pH", x = "pH", y = "biomasa")
Interpretación:
Pendiente (β̂₂): por cada unidad adicional de pH, con un signo positiv, la biomasa esperada cambia en 458.03 unidades. Esto indica que suelos con mayor pH se asocian con mayor biomasa.
Significancia de la pendiente:
\[H_0:\beta_2 = 0 \quad \text{frente a} \quad H_1:\beta_2 \neq 0.\]
El p-valor es \(1.09 \times 10^{-10} \ll 0.05\), por lo que se rechaza \(H_0\): el pH tiene un efecto significativo sobre la biomasa.
Intercepto (β̂₀): biomasa esperada con pH = 0 que da -1083.23, un valor fuera del rango observado, por lo que no tiene interpretación práctica; solo ancla la recta.
Ajuste: R² = 0.67 (el ajustado 0.66), es decir, el pH explica aproximadamente el 67% de la variabilidad de la biomasa.
Aplicando la fórmula general con los valores de este modelo (\(n = 40\), \(k = 1\), \(p = 2\)):
\[R^2 = \frac{12{,}150{,}057}{18{,}126{,}800} = 1 - \frac{5{,}976{,}743}{18{,}126{,}800} = 0.6703\]
El \(R^2\) ajustado:
\[R^2_{\text{adj}} = 1 - \frac{SSE/(n-p)}{SST/(n-1)} = 1 - \frac{5{,}976{,}743\,/\,(40-2)}{18{,}126{,}800\,/\,(40-1)} = 1 - \frac{157{,}283.8}{464{,}789.7} = 0.6616\]
El estadístico F global (en regresión simple coincide con \(t^2\) de la pendiente):
\[F = \frac{SSR/k}{SSE/(n-p)} = \frac{12{,}150{,}057\,/\,1}{5{,}976{,}743\,/\,(40-2)} = \frac{12{,}150{,}057}{157{,}283.8} = 77.25\]
Bajo \(H_0\) este estadístico sigue una \(F_{1,\,38}\), con p-valor \(\approx 1.09 \times 10^{-10}\).
Aviso importante: este coeficiente es un efecto marginal: no controla por K ni por las demás variables del suelo. Puede cambiar al incorporar otras variables como se hace con la k en el apartado e de este mismo ejercicio.
En el apartado b) hemos ajustado biomasa ~ pH y obtenido
un coeficiente marginal: ese \(\hat\beta_2 =
458\) recoge no solo el efecto directo del pH sino todo lo que K
pueda tener en común con el pH. El objetivo de este apartado es
aislar el efecto de K sobre la biomasa descontando el
pH, y hacerlo de forma visual y conceptual antes de llegar al
modelo múltiple formal.
La idea clave es la siguiente: tanto la biomasa como K están parcialmente “contaminadas” por el pH. Si queremos ver la relación pura entre biomasa y K, debemos quitarle a ambas variables lo que el pH ya explica de ellas. Para eso se construyen dos regresiones auxiliares y se toman sus residuos:
biomasa ~ pH \(\Rightarrow\) residuos \(e_Y\) = parte de la biomasa no
explicada por el pH.K ~ pH
\(\Rightarrow\) residuos \(e_{X_3}\) = parte de K no
explicada por el pH.Una vez que ambas variables están “depuradas” del pH, la relación entre ellas refleja únicamente la información que K aporta por encima de lo que ya recoge el pH.
# (i) Regresión biomasa ~ pH -> residuos e_Y
mod_Y <- lm(biomasa ~ pH, data = suelo)
eY <- resid(mod_Y)
# (ii) Regresión K ~ pH -> residuos e_X3
mod_X3 <- lm(K ~ pH, data = suelo)
eX3 <- resid(mod_X3)
# Coeficientes de ambas regresiones auxiliares
coef_Y <- as.data.frame(summary(mod_Y)$coefficients)
coef_X3 <- as.data.frame(summary(mod_X3)$coefficients)
names(coef_Y) <- names(coef_X3) <- c("Estimación", "Error Estándar", "t", "p-valor")
kable(round(coef_Y, 6), caption = "Coeficientes: biomasa ~ pH") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| Estimación | Error Estándar | t | p-valor | |
|---|---|---|---|---|
| (Intercept) | -1083.2272 | 244.83514 | -4.424313 | 7.9e-05 |
| pH | 458.0517 | 52.11537 | 8.789186 | 0.0e+00 |
kable(round(coef_X3, 6), caption = "Coeficientes: K ~ pH") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| Estimación | Error Estándar | t | p-valor | |
|---|---|---|---|---|
| (Intercept) | 703.31236 | 182.98112 | 3.843634 | 0.000448 |
| pH | 22.25183 | 38.94918 | 0.571304 | 0.571157 |
# Comprobaciones: residuos con media 0 y ortogonales a pH
check_df <- data.frame(
`Media eY` = mean(eY),
`Media eX3` = mean(eX3),
`cor(eY,pH)` = cor(eY, suelo$pH),
`cor(eX3,pH)` = cor(eX3, suelo$pH),
check.names = FALSE
)
kable(round(check_df, 10), caption = "Verificación: residuos ortogonales a pH y con media 0") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| Media eY | Media eX3 | cor(eY,pH) | cor(eX3,pH) |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
kable(round(head(data.frame(eY, eX3)), 5), caption = "Primeras filas de los residuos") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| eY | eX3 |
|---|---|
| -531.0312 | 627.0985 |
| -576.5183 | 490.1814 |
| 211.4101 | 357.4999 |
| -64.2002 | 243.9296 |
| -793.9338 | 457.3359 |
| -319.4925 | 548.4674 |
# (iii) Gráfico e_Y vs e_X3
library(ggplot2)
df_res <- data.frame(eX3 = eX3, eY = eY)
ggplot(df_res, aes(eX3, eY)) +
geom_point(color = "steelblue", alpha = 0.7) +
geom_smooth(method = "lm", color = "red", se = FALSE) +
geom_hline(yintercept = 0, linetype = "dashed") +
geom_vline(xintercept = 0, linetype = "dashed") +
labs(title = "Residuos de biomasa ~ pH vs. residuos de K ~ pH",
x = "e_X3 (K libre de pH)", y = "e_Y (biomasa libre de pH)")
# Correlación entre residuos (= correlación parcial entre biomasa y K dado pH)
cor_parcial <- data.frame(
`Correlación parcial biomasa-K dado pH` = cor(eY, eX3),
check.names = FALSE
)
kable(round(cor_parcial, 6), caption = "Correlación parcial entre biomasa y K controlando por pH") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| Correlación parcial biomasa-K dado pH |
|---|
| -0.392211 |
(iv) Qué representa cada conjunto de residuos e interpretación completa:
\(e_Y\) (residuos de
biomasa ~ pH): representan la biomasa que
no está explicada por el pH. Formalmente, para cada
parcela \(i\), \(e_{Y,i} = \text{biomasa}_i -
\hat{\text{biomasa}}_i(\text{pH})\). Un valor positivo indica que
esa parcela tiene más biomasa de lo que su pH predeciría; uno negativo,
menos. Esta variable ya no lleva información del pH.
\(e_{X_3}\) (residuos de
K ~ pH): representan la parte del potasio que
no está correlacionada con el pH. Es la variación en K
que es independiente del pH, la única parte que puede aportar
nueva información al modelo una vez que el pH ya está incluido.
Verificación de ortogonalidad: los residuos MCO tienen siempre media exactamente cero y son ortogonales (no correlacionados) a los predictores del modelo. Confirmar que \(\bar{e}_Y \approx 0\), \(\bar{e}_{X_3} \approx 0\) y que \(\text{cor}(e_Y, \text{pH}) \approx 0\), \(\text{cor}(e_{X_3}, \text{pH}) \approx 0\) es sólo una verificación numérica de que la construcción es correcta.
Gráfico \(e_Y\) vs. \(e_{X_3}\): este gráfico es el central del apartado. La pendiente de la recta ajustada sobre esta nube de puntos mide el efecto de K sobre la biomasa una vez eliminada la influencia compartida con el pH, es decir, es el efecto parcial de K. La correlación entre ambos residuos es la correlación parcial de biomasa y K dado pH. Veremos en el apartado e) que esa pendiente es exactamente \(\hat\beta_3\) del modelo múltiple (Teorema de Frisch-Waugh-Lovell).
Dirección de la nube: la nube tiene pendiente negativa, lo que indica que los suelos con más potasio del que les correspondería por su pH tienden a tener menos biomasa de la que les correspondería por su pH. Dicho de otro modo, el efecto parcial de K sobre la biomasa es negativo: dado el mismo pH, un mayor contenido en K se asocia con menor biomasa.
Propósito global del apartado: este procedimiento de residualización sirve para aislar visualmente el efecto parcial de una variable, y es la demostración intuitiva de por qué el coeficiente de K en el modelo múltiple puede ser muy distinto de su correlación simple con la biomasa. No se trata simplemente de calcular residuos: se trata de descomponer la variación de cada variable en una parte explicada por el pH y una parte nueva, y luego relacionar esas partes nuevas entre sí.
(v) Visualización 3D interactiva de los residuos:
Puedes escanear el siguiente código QR con tu móvil para acceder a una visualización interactiva en 3D que hemos preparado. En ella se aprecia el plano de regresión múltiple y las líneas verticales negras que caen desde cada punto real hasta el plano (los residuos ortogonales).
# Si no tienes instalado el paquete qrcode, ejecuta en la consola: install.packages("qrcode")
library(qrcode)
# NOTA: Debes subir el archivo Vis3D.html (que acabo de crear en tu carpeta) a alguna web
# gratuita como RPubs o GitHub Pages. Cuando tengas el enlace, ponlo aquí abajo:
url_visualizacion <- "https://rpubs.com/tu_usuario/visualizacion_3D"
qr <- qr_code(url_visualizacion)
plot(qr)
Se ajusta el modelo
\[e_{Y,i} = \alpha + \gamma\, e_{X_3,i} + u_i ,\]
donde \(e_Y\) son los residuos de
biomasa ~ pH y \(e_{X_3}\)
los de K ~ pH.
mod_res <- lm(eY ~ eX3)
# Tabla de coeficientes del modelo de residuos
coef_res <- as.data.frame(summary(mod_res)$coefficients)
names(coef_res) <- c("Estimación", "Error Estándar", "t", "p-valor")
kable(round(coef_res, 8), caption = "Coeficientes del modelo de residuos: eY ~ eX3") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE) |>
column_spec(5, bold = TRUE, color = ifelse(coef_res$`p-valor` < 0.05, "green", "red"))
| Estimación | Error Estándar | t | p-valor | |
|---|---|---|---|---|
| (Intercept) | 0.0000000 | 57.6818902 | 0.000000 | 1.0000000 |
| eX3 | -0.5247918 | 0.1996662 | -2.628345 | 0.0123111 |
gamma_hat <- unname(coef(mod_res)["eX3"])
# Tabla de coeficientes con IC 95%
tabla_res <- cbind(
Estimación = coef(mod_res),
confint(mod_res),
`p-valor` = summary(mod_res)$coefficients[, 4]
)
kable(round(tabla_res, 8), caption = "Coeficientes e IC 95 %: regresión de residuos") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| Estimación | 2.5 % | 97.5 % | p-valor | |
|---|---|---|---|---|
| (Intercept) | 0.0000000 | -116.770882 | 116.7708818 | 1.0000000 |
| eX3 | -0.5247918 | -0.928995 | -0.1205886 | 0.0123111 |
Interpretación:
γ̂ = -0.52: pendiente de la nube de puntos \(e_Y\) vs. \(e_{X_3}\). El cambio esperado en la biomasa por cada unidad adicional de K una vez descontado el efecto del pH en ambas variables es de -0.52 unidades.
Intercepto α̂ ≈ 0: es cero lógicamente (salvo error numérico) porque los dos conjuntos de residuos tienen media exactamente cero.
Significancia de \(\hat\gamma\):
\[H_0:\gamma = 0 \quad \text{frente a} \quad H_1:\gamma \neq 0.\]
El p-valor de \(\hat\gamma\) es 0.0123. Fijando \(\alpha = 0.05\), hay evidencia de que K aporta información sobre la biomasa más allá del pH.
Nota importante (inferencia): el error estándar y el p-valor de γ̂ que reporta esta regresión no son los del modelo múltiple.
Se ajusta el modelo
\[\text{biomasa}_i = \beta_0 + \beta_2\,\text{pH}_i + \beta_3\,K_i + \varepsilon_i .\]
m_mult <- lm(biomasa ~ pH + K, data = suelo)
# Tabla de coeficientes
coef_m <- as.data.frame(summary(m_mult)$coefficients)
names(coef_m) <- c("Estimación", "Error Estándar", "t", "p-valor")
kable(round(coef_m, 6), caption = "Coeficientes del modelo múltiple: biomasa ~ pH + K") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE) |>
column_spec(5, bold = TRUE, color = ifelse(coef_m$`p-valor` < 0.05, "green", "red"))
| Estimación | Error Estándar | t | p-valor | |
|---|---|---|---|---|
| (Intercept) | -714.134638 | 268.973751 | -2.655035 | 0.011626 |
| pH | 469.729258 | 48.791357 | 9.627305 | 0.000000 |
| K | -0.524792 | 0.202346 | -2.593531 | 0.013530 |
# Bondad de ajuste
gl_m <- data.frame(
`R²` = summary(m_mult)$r.squared,
`R² ajustado` = summary(m_mult)$adj.r.squared,
`Sigma (RSE)` = summary(m_mult)$sigma,
check.names = FALSE
)
kable(round(gl_m, 6), caption = "Bondad de ajuste: biomasa ~ pH + K") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| R² | R² ajustado | Sigma (RSE) |
|---|---|---|
| 0.721002 | 0.705921 | 369.7093 |
beta3_hat <- unname(coef(m_mult)["K"])
gamma_hat <- unname(coef(mod_res)["eX3"])
# Comparación con todos los decimales
print(beta3_hat, digits = 15)
## [1] -0.524791779453377
print(gamma_hat, digits = 15)
## [1] -0.524791779453377
c(beta3 = beta3_hat,
gamma = gamma_hat,
diferencia = beta3_hat - gamma_hat)
## beta3 gamma diferencia
## -5.247918e-01 -5.247918e-01 2.220446e-16
# Verificación numérica formal
all.equal(beta3_hat, gamma_hat)
## [1] TRUE
isTRUE(all.equal(beta3_hat, gamma_hat, tolerance = 1e-10))
## [1] TRUE
# Tabla comparativa
tabla_e <- data.frame(
procedimiento = c("Modelo múltiple: beta3 (K)",
"Residuos sobre residuos: gamma"),
estimacion = c(beta3_hat, gamma_hat),
error_est = c(summary(m_mult)$coefficients["K", 2],
summary(mod_res)$coefficients["eX3", 2]),
gl_residuales = c(df.residual(m_mult), df.residual(mod_res))
)
knitr::kable(tabla_e, digits = 8,
caption = "Coincide la estimación; difieren error estándar y grados de libertad")
| procedimiento | estimacion | error_est | gl_residuales |
|---|---|---|---|
| Modelo múltiple: beta3 (K) | -0.5247918 | 0.2023465 | 37 |
| Residuos sobre residuos: gamma | -0.5247918 | 0.1996662 | 38 |
Este es el resultado más significativo que podemos obtener.
Aplicando la fórmula con los valores del modelo múltiple (\(n = 40\), \(k = 2\), \(p = 3\)):
\[R^2 = \frac{SSR}{SST} = 1 - \frac{SSE}{SST} = \frac{13{,}069{,}455}{18{,}126{,}800} = 1 - \frac{5{,}057{,}345}{18{,}126{,}800} = 0.7210\]
El \(R^2\) ajustado (penaliza por usar \(k=2\) predictores en vez de 1):
\[R^2_{\text{adj}} = 1 - \frac{SSE/(n-p)}{SST/(n-1)} = 1 - \frac{5{,}057{,}345\,/\,(40-3)}{18{,}126{,}800\,/\,(40-1)} = 1 - \frac{136{,}685.0}{464{,}789.7} = 0.7059\]
El estadístico F global para el contraste \(H_0:\beta_2 = \beta_3 = 0\) frente a \(H_1:\exists\,j,\,\beta_j\neq 0\):
\[F = \frac{SSR/k}{SSE/(n-p)} = \frac{13{,}069{,}455\,/\,2}{5{,}057{,}345\,/\,(40-3)} = \frac{6{,}534{,}727.5}{136{,}685.0} = 47.81\]
Bajo \(H_0\) este estadístico sigue una \(F_{2,\,37}\), con p-valor \(\approx 0\).
Nótese que el \(SST =
18{,}126{,}800\) es el mismo que en el modelo
simple del apartado b) porque la variable respuesta
(biomasa) no cambia. El \(SSR\) aumenta de \(12{,}150{,}057\) a \(13{,}069{,}455\) al incorporar \(K\), confirmando que esta variable explica
variabilidad adicional que el pH solo no recogía.
# Comparación con la regresión simple biomasa ~ K (apoya el apartado f)
m_simple_K <- lm(biomasa ~ K, data = suelo)
fwl_comp <- data.frame(
Modelo = c("Múltiple (biomasa ~ pH + K)", "Simple (biomasa ~ K)"),
`Coef. K estimado` = c(beta3_hat, unname(coef(m_simple_K)["K"])),
check.names = FALSE
)
fwl_comp[, sapply(fwl_comp, is.numeric)] <- round(fwl_comp[, sapply(fwl_comp, is.numeric)], 8)
kable(fwl_comp, caption = "Comparación coeficiente K: múltiple vs simple") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| Modelo | Coef. K estimado |
|---|---|
| Múltiple (biomasa ~ pH + K) | -0.5247918 |
| Simple (biomasa ~ K) | -0.3450211 |
Interpretación:
biomasa ~ pH sobre
los residuos de K ~ pH.biomasa ~ K?m_simple_K <- lm(biomasa ~ K, data = suelo)
m_mult <- lm(biomasa ~ pH + K, data = suelo)
m_aux <- lm(K ~ pH, data = suelo) # regresión auxiliar de K sobre pH
b_simple <- unname(coef(m_simple_K)["K"]) # efecto marginal de K
b_multiple <- unname(coef(m_mult)["K"]) # efecto parcial de K (controlando pH)
b_pH <- unname(coef(m_mult)["pH"]) # efecto parcial de pH
d_aux <- unname(coef(m_aux)["pH"]) # pendiente de K sobre pH
tabla_f <- data.frame(
coeficiente = c("K en biomasa ~ K (simple)",
"K en biomasa ~ pH + K (múltiple)",
"pH en biomasa ~ pH + K (múltiple)",
"pH en K ~ pH (auxiliar)"),
valor = c(b_simple, b_multiple, b_pH, d_aux)
)
knitr::kable(tabla_f, digits = 6, caption = "Ingredientes de la comparación")
| coeficiente | valor |
|---|---|
| K en biomasa ~ K (simple) | -0.345021 |
| K en biomasa ~ pH + K (múltiple) | -0.524792 |
| pH en biomasa ~ pH + K (múltiple) | 469.729258 |
| pH en K ~ pH (auxiliar) | 22.251835 |
# Identidad del sesgo por variable omitida (exacta en MCO):
# beta_simple(K) = beta3 + beta2 * (pendiente de pH sobre K)
m_aux2 <- lm(pH ~ K, data = suelo)
d2 <- unname(coef(m_aux2)["K"])
sesgo_df <- data.frame(
Cantidad = c("β̂K (simple)", "β̂3 + sesgo", "Sesgo (β̂2 · δ̂)", "Diferencia"),
Valor = c(b_simple,
b_multiple + b_pH * d2,
b_pH * d2,
b_simple - (b_multiple + b_pH * d2))
)
sesgo_df[, sapply(sesgo_df, is.numeric)] <- round(sesgo_df[, sapply(sesgo_df, is.numeric)], 8)
kable(sesgo_df, caption = "Verificación numérica de la identidad del sesgo por variable omitida") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| Cantidad | Valor |
|---|---|
| β̂K (simple) |
-0.345021
|
# Cuánto se relacionan los predictores (mecanismo del sesgo)
cor_ph_k <- data.frame(`cor(pH, K)` = cor(suelo$pH, suelo$K), check.names = FALSE)
kable(round(cor_ph_k, 6), caption = "Correlación entre pH y K") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| cor(pH, K) |
|---|
| 0.092282 |
\(\beta_3\) es el efecto
parcial de \(K\) sobre la
biomasa: el cambio esperado en la biomasa por cada unidad adicional de
potasio manteniendo constante el pH. Por otro lado el
coeficiente de la regresión simple biomasa ~ K es un efecto
marginal: compara suelos con distinto potasio
sin fijar el pH, de modo que también recoge parte del
efecto del pH siempre que ambas variables estén relacionadas.
La relacion entre ambos coeficientes es
\[\hat\beta_K^{\,simple} \;=\; \hat\beta_3 \;+\; \hat\beta_2\,\hat\delta ,\]
donde \(\hat\delta\) es la pendiente
de la regresión de \(\text{pH}\) sobre
\(K\). El segundo término es el
sesgo por variable omitida: mide cuánto del efecto del
pH se añade adicionalmente en el coeficiente de \(K\) cuando el pH se deja fuera del modelo.
Por tanto, los dos coeficientes solo coinciden si \(\hat\beta_2 = 0\), es decir, si el pH no
afecta a la biomasa, o si \(\hat\delta =
0\), que significa que pH y \(K\) no están correlacionados (son
ortogonales). En estos datos, cor(pH, K) = 0.092 y \(\hat\beta_2\) = 469.729, así que el sesgo
vale 0.180 y el coeficiente simple (\(\hat\beta_K^{simple}\) = -0.345) difiere
del parcial (\(\hat\beta_3\) = -0.525).
La identidad se verifica numéricamente: \(-0.525 + 0.180 = -0.345\), con una
diferencia de 0 entre ambos lados.
Es verdad que la correlacion entre pH y \(K\) es baja (0.092) pero aun asi el sesgo no es despreciable porque el pH tiene un efecto parcial muy grande sobre la biomasa (\(\hat\beta_2\) = 469.729): el sesgo depende del producto \(\hat\beta_2 \cdot \hat\delta\), no solo de la correlación. Adicionalmente a esto, como el sesgo es positivo, la regresión simple subestima en valor absoluto el efecto negativo de \(K\): pasa de -0.525 (efecto parcial, controlando por pH) a -0.345 (efecto marginal). El signo se mantiene, pero la magnitud del efecto de \(K\) es un 34 % menor en la regresión simple.
Esto se conecta con lo visto en el apartado c) ya que ahi vemos que \(\hat\beta_3\) es la pendiente entre las partes de la biomasa y de \(K\) no explicadas por el pH (teorema de Frisch-Waugh-Lovell), mientras que la regresión simple usa las variables completas.
esp <- read.table("esperanza.txt", header = TRUE)
cat("Dimensiones:", dim(esp), "\n")
## Dimensiones: 45 8
kable(head(esp), caption = "Primeras filas del dataset (esperanza de vida)") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| habitantes | ingresos | analfabetismo | esp_vida | asesinatos | universitarios | heladas | area |
|---|---|---|---|---|---|---|---|
| 25715 | 3804 | 2.1 | 69.05 | 15.1 | 41.3 | 66 | 50708 |
| 22466 | 6495 | 1.5 | 69.31 | 11.3 | 66.7 | 199 | 566432 |
| 24213 | 3558 | 1.9 | 70.66 | 10.1 | 39.9 | 114 | 51945 |
| 43302 | 5294 | 1.1 | 71.71 | 10.3 | 62.6 | 70 | 156361 |
| 24646 | 5064 | 0.7 | 72.06 | 6.8 | 63.9 | 217 | 103766 |
| 22686 | 4989 | 0.9 | 70.06 | 6.2 | 54.6 | 156 | 1982 |
# Nombre de la variable respuesta: la columna que NO es un predictor del enunciado
predictores <- c("habitantes", "analfabetismo", "ingresos", "asesinatos",
"universitarios", "heladas", "area")
resp <- setdiff(names(esp), predictores)
resp # debe ser un único nombre (la esperanza de vida)
## [1] "esp_vida"
# --- Datos faltantes ---
kable(t(colSums(is.na(esp))), caption = "Valores faltantes por variable") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| habitantes | ingresos | analfabetismo | esp_vida | asesinatos | universitarios | heladas | area |
|---|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
# --- Descriptivos ---
desc <- data.frame(
media = sapply(esp, mean),
sd = sapply(esp, sd),
cv = sapply(esp, function(x) sd(x) / mean(x)),
min = sapply(esp, min),
max = sapply(esp, max)
)
kable(round(desc, 3), caption = "Estadísticos descriptivos: Esperanza de Vida") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| media | sd | cv | min | max | |
|---|---|---|---|---|---|
| habitantes | 26431.600 | 4632.280 | 0.175 | 22466.00 | 43302.0 |
| ingresos | 4596.178 | 619.595 | 0.135 | 3278.00 | 6495.0 |
| analfabetismo | 1.178 | 0.626 | 0.531 | 0.50 | 2.8 |
| esp_vida | 70.820 | 1.356 | 0.019 | 67.96 | 73.6 |
| asesinatos | 7.607 | 3.656 | 0.481 | 1.40 | 15.1 |
| universitarios | 52.809 | 8.443 | 0.160 | 37.80 | 67.3 |
| heladas | 177.244 | 54.265 | 0.306 | 56.00 | 268.0 |
| area | 72315.911 | 89003.029 | 1.231 | 1049.00 | 566432.0 |
library(ggcorrplot)
# --- Matriz de correlaciones (Gráfico) ---
ggcorrplot(cor(esp),
hc.order = FALSE,
type = "lower",
lab = TRUE,
lab_size = 3.5,
method = "square",
colors = c("#2166ac", "#f7f7f7", "#b2182b"),
title = "Matriz de Correlación Completa: Esperanza de Vida y Predictores",
ggtheme = theme_minimal())
# Correlación de cada predictor con la respuesta (graficado)
cor_resp <- sort(cor(esp[, predictores], esp[[resp]])[, 1], decreasing = TRUE)
df_cor_resp <- data.frame(
predictor = factor(names(cor_resp), levels = names(cor_resp)),
correlacion = as.numeric(cor_resp)
)
ggplot(df_cor_resp, aes(x = reorder(predictor, correlacion), y = correlacion, fill = correlacion)) +
geom_col(width = 0.6) +
geom_text(aes(label = round(correlacion, 3)),
hjust = ifelse(df_cor_resp$correlacion >= 0, -0.25, 1.25), size = 3.8, fontface = "bold") +
coord_flip() +
scale_fill_gradient2(low = "#2166ac", mid = "#f7f7f7", high = "#b2182b", midpoint = 0, limits = c(-1, 1)) +
scale_y_continuous(limits = c(-1, 1)) +
labs(title = "Correlación de cada Predictor con Esperanza de Vida",
x = "Predictor", y = "Coeficiente de Correlación (r)") +
theme_minimal() +
theme(legend.position = "none")
library(ggplot2)
library(patchwork)
graf <- lapply(predictores, function(v) {
ggplot(esp, aes(.data[[v]], .data[[resp]])) +
geom_point(color = "steelblue", alpha = 0.7) +
geom_smooth(method = "lm", color = "red") +
labs(title = paste(resp, "vs.", v), x = v, y = resp)
})
wrap_plots(graf, ncol = 3)
# Relaciones entre los propios predictores
pairs(esp[, predictores], pch = 19, col = "steelblue", cex = 0.6)
# Correlaciones entre predictores (busca las de mayor valor absoluto)
cp <- cor(esp[, predictores])
cp[upper.tri(cp, diag = TRUE)] <- NA
pares_cor <- as.data.frame(as.table(cp))
pares_cor <- na.omit(pares_cor)
names(pares_cor) <- c("Variable 1", "Variable 2", "Correlación")
top5 <- head(pares_cor[order(-abs(pares_cor$Correlación)), ], 5)
top5$Correlación <- round(top5$Correlación, 5)
kable(top5,
caption = "Top 5 pares de predictores con mayor correlación (en valor absoluto)") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| Variable 1 | Variable 2 | Correlación | |
|---|---|---|---|
| 11 | asesinatos | analfabetismo | 0.70845 |
| 12 | universitarios | analfabetismo | -0.67952 |
| 13 | heladas | analfabetismo | -0.65479 |
| 19 | universitarios | ingresos | 0.64165 |
| 27 | heladas | asesinatos | -0.54625 |
Interpretación :
add1()Se parte del modelo nulo y, en cada etapa, se calcula con
add1() el test F parcial de cada variable candidata.
Ingresa la de menor p-valor, siempre que sea
< 0.01; el proceso se detiene cuando ninguna de
ellas obtiene un p–valor menor que 0.01.
alpha <- 0.01
# --- Etapa 0: modelo nulo y primer add1() ---
mod_actual <- lm(reformulate("1", resp), data = esp)
scope_f <- reformulate(predictores) # ~ todos los predictores
add1(mod_actual, scope = scope_f, test = "F")
## Single term additions
##
## Model:
## esp_vida ~ 1
## Df Sum of Sq RSS AIC F value Pr(>F)
## <none> 80.852 28.368
## habitantes 1 0.365 80.487 30.164 0.1952 0.66082
## analfabetismo 1 28.769 52.083 10.578 23.7521 1.530e-05 ***
## ingresos 1 7.820 73.032 25.791 4.6044 0.03758 *
## asesinatos 1 50.882 29.970 -14.290 73.0022 8.152e-11 ***
## universitarios 1 28.097 52.755 11.155 22.9018 2.036e-05 ***
## heladas 1 4.823 76.029 27.601 2.7277 0.10591
## area 1 0.775 80.077 29.935 0.4160 0.52239
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Para no repetir el proceso a mano, el siguiente chunk lo automatiza y
muestra el add1() de cada etapa:
mod_actual <- lm(reformulate("1", resp), data = esp)
incluidas <- character(0)
historial <- data.frame(paso = integer(0), variable = character(0),
F = numeric(0), p_valor = numeric(0))
repeat {
candidatas <- setdiff(predictores, incluidas)
if (length(candidatas) == 0) break
tab <- add1(mod_actual, scope = reformulate(predictores), test = "F")
tab <- tab[candidatas, , drop = FALSE] # solo las aún no incluidas
cat("\n--- Paso", length(incluidas) + 1, ": modelo actual =",
deparse(formula(mod_actual)), "---\n")
print(tab[, c("Df", "Sum of Sq", "RSS", "AIC", "F value", "Pr(>F)")])
mejor <- rownames(tab)[which.min(tab[["Pr(>F)"]])]
p_min <- min(tab[["Pr(>F)"]])
f_min <- tab[mejor, "F value"]
if (p_min < alpha) {
incluidas <- c(incluidas, mejor)
historial <- rbind(historial,
data.frame(paso = length(incluidas), variable = mejor,
F = f_min, p_valor = p_min))
mod_actual <- update(mod_actual, reformulate(incluidas, resp))
cat("=> Ingresa:", mejor, " (p =", format(p_min, digits = 4), ")\n")
} else {
cat("=> Ninguna variable cumple p <", alpha, ". Fin de la selección.\n")
break
}
}
##
## --- Paso 1 : modelo actual = esp_vida ~ 1 ---
## Df Sum of Sq RSS AIC F value Pr(>F)
## habitantes 1 0.365 80.487 30.164 0.1952 0.66082
## analfabetismo 1 28.769 52.083 10.578 23.7521 1.530e-05 ***
## ingresos 1 7.820 73.032 25.791 4.6044 0.03758 *
## asesinatos 1 50.882 29.970 -14.290 73.0022 8.152e-11 ***
## universitarios 1 28.097 52.755 11.155 22.9018 2.036e-05 ***
## heladas 1 4.823 76.029 27.601 2.7277 0.10591
## area 1 0.775 80.077 29.935 0.4160 0.52239
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## => Ingresa: asesinatos (p = 8.152e-11 )
##
## --- Paso 2 : modelo actual = esp_vida ~ asesinatos ---
## Df Sum of Sq RSS AIC F value Pr(>F)
## habitantes 1 3.2826 26.688 -17.510 5.1659 0.02821 *
## analfabetismo 1 0.1933 29.777 -12.581 0.2726 0.60435
## ingresos 1 1.0796 28.891 -13.941 1.5694 0.21722
## universitarios 1 4.2458 25.725 -19.165 6.9320 0.01180 *
## heladas 1 4.1207 25.850 -18.946 6.6953 0.01322 *
## area 1 0.4782 29.492 -13.014 0.6811 0.41388
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## => Ninguna variable cumple p < 0.01 . Fin de la selección.
mod_forward <- mod_actual
# --- Orden de ingreso y p-valores ---
kable(historial, digits = 6, row.names = FALSE,
caption = "Selección progresiva (forward): orden de ingreso (alfa = 0.01)") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| paso | variable | F | p_valor |
|---|---|---|---|
| 1 | asesinatos | 73.00217 | 0 |
# --- Modelo final ---
cat("Fórmula del modelo final (forward):", deparse(formula(mod_forward)), "\n")
## Fórmula del modelo final (forward): esp_vida ~ asesinatos
coef_fw <- as.data.frame(summary(mod_forward)$coefficients)
names(coef_fw) <- c("Estimación", "Error Estándar", "t", "p-valor")
kable(round(coef_fw, 6), caption = "Coeficientes del modelo final (forward)") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE) |>
column_spec(5, bold = TRUE, color = ifelse(coef_fw$`p-valor` < 0.05, "green", "red"))
| Estimación | Error Estándar | t | p-valor | |
|---|---|---|---|---|
| (Intercept) | 73.057001 | 0.289915 | 251.994794 | 0 |
| asesinatos | -0.294114 | 0.034423 | -8.544131 | 0 |
gl_fw <- data.frame(`R²` = summary(mod_forward)$r.squared,
`R² ajustado` = summary(mod_forward)$adj.r.squared,
`Sigma` = summary(mod_forward)$sigma, check.names = FALSE)
kable(round(gl_fw, 6), caption = "Bondad de ajuste: modelo forward") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| R² | R² ajustado | Sigma |
|---|---|---|
| 0.629317 | 0.620697 | 0.834858 |
Interpretación:
Orden de ingreso: la única variable que entra al modelo es asesinatos, con un p-valor de
8.152e-11
Es la variable de mayor asociación marginal con la esperanza de vida (coincide con la de mayor |r| del EDA).
Detención: el proceso para cuando ninguna variable restante tiene p < 0.01; las variables excluidas son todas las demás.
Modelo final (forward):
esp_vida~ asesinatos, con R² ajustado = 0.6207
Nota importante: el p-valor de cada variable depende de qué otras ya estén en el modelo; por eso un predictor correlacionado con otro ya incluido puede perder significancia. Es por eso que los p-valpores de las variables que al principio eran casi 0 suben a mas de 0.1
Nota importante 2: Si hubieramos sido mas plausibles con alpha y tomado un nivel de significancia mayor, con add1() añadiriamos al modelo la variable “universitarios”. Esto puede ser interesnate para considerar un modelo que quizá obtenga mejores resultados predictivos por ejemplo.
drop1()Se parte del modelo completo y, en cada etapa, drop1()
calcula el test F parcial de cada variable incluida (efecto de quitarla,
dado que las demás permanecen). Se elimina la de mayor
p-valor, siempre que sea > 0.01; el proceso
se detiene cuando todas son significativas.
# --- Etapa 0: modelo completo y primer drop1() ---
mod_completo <- lm(reformulate(predictores, resp), data = esp)
summary(mod_completo)
##
## Call:
## lm(formula = reformulate(predictores, resp), data = esp)
##
## Residuals:
## Min 1Q Median 3Q Max
## -1.13551 -0.45779 0.00357 0.54006 1.37940
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 7.145e+01 2.030e+00 35.188 < 2e-16 ***
## habitantes 5.222e-05 2.738e-05 1.907 0.0643 .
## analfabetismo -3.699e-02 3.613e-01 -0.102 0.9190
## ingresos -2.527e-04 2.537e-04 -0.996 0.3257
## asesinatos -3.198e-01 4.689e-02 -6.819 4.94e-08 ***
## universitarios 5.351e-02 2.249e-02 2.380 0.0226 *
## heladas -6.931e-03 2.907e-03 -2.384 0.0224 *
## area 4.635e-07 1.626e-06 0.285 0.7772
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.7055 on 37 degrees of freedom
## Multiple R-squared: 0.7722, Adjusted R-squared: 0.7292
## F-statistic: 17.92 on 7 and 37 DF, p-value: 3.915e-10
drop1(mod_completo, test = "F")
## Single term deletions
##
## Model:
## esp_vida ~ habitantes + analfabetismo + ingresos + asesinatos +
## universitarios + heladas + area
## Df Sum of Sq RSS AIC F value Pr(>F)
## <none> 18.414 -24.209
## habitantes 1 1.8099 20.224 -21.991 3.6366 0.06431 .
## analfabetismo 1 0.0052 18.419 -26.197 0.0105 0.91901
## ingresos 1 0.4937 18.908 -25.019 0.9919 0.32574
## asesinatos 1 23.1439 41.558 10.419 46.5037 4.94e-08 ***
## universitarios 1 2.8181 21.232 -19.802 5.6624 0.02260 *
## heladas 1 2.8283 21.242 -19.780 5.6831 0.02237 *
## area 1 0.0404 18.455 -26.111 0.0813 0.77719
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
El siguiente chunk automatiza el proceso y muestra el
drop1() de cada etapa:
mod_actual <- lm(reformulate(predictores, resp), data = esp)
incluidas <- predictores
historial <- data.frame(paso = integer(0), variable_eliminada = character(0),
F = numeric(0), p_valor = numeric(0))
repeat {
if (length(incluidas) == 0) break
tab <- drop1(mod_actual, test = "F")
tab <- tab[incluidas, , drop = FALSE] # quita la fila <none>
cat("\n--- Paso", nrow(historial) + 1, ": modelo actual =",
deparse(formula(mod_actual)), "---\n")
print(tab[, c("Df", "Sum of Sq", "RSS", "AIC", "F value", "Pr(>F)")])
peor <- rownames(tab)[which.max(tab[["Pr(>F)"]])]
p_max <- max(tab[["Pr(>F)"]])
f_max <- tab[peor, "F value"]
if (p_max > alpha) {
historial <- rbind(historial,
data.frame(paso = nrow(historial) + 1,
variable_eliminada = peor,
F = f_max, p_valor = p_max))
incluidas <- setdiff(incluidas, peor)
mod_actual <- if (length(incluidas) > 0) {
lm(reformulate(incluidas, resp), data = esp)
} else {
lm(reformulate("1", resp), data = esp)
}
cat("=> Se elimina:", peor, " (p =", format(p_max, digits = 4), ")\n")
} else {
cat("=> Todas las variables son significativas (p <=", alpha,
"). Fin de la selección.\n")
break
}
}
##
## --- Paso 1 : modelo actual = esp_vida ~ habitantes + analfabetismo + ingresos + asesinatos + universitarios + heladas + area ---
## Df Sum of Sq RSS AIC F value Pr(>F)
## habitantes 1 1.8099 20.224 -21.991 3.6366 0.06431 .
## analfabetismo 1 0.0052 18.419 -26.197 0.0105 0.91901
## ingresos 1 0.4937 18.908 -25.019 0.9919 0.32574
## asesinatos 1 23.1439 41.558 10.419 46.5037 4.94e-08 ***
## universitarios 1 2.8181 21.232 -19.802 5.6624 0.02260 *
## heladas 1 2.8283 21.242 -19.780 5.6831 0.02237 *
## area 1 0.0404 18.455 -26.111 0.0813 0.77719
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## => Se elimina: analfabetismo (p = 0.919 )
##
## --- Paso 2 : modelo actual = esp_vida ~ habitantes + ingresos + asesinatos + universitarios + heladas + area ---
## Df Sum of Sq RSS AIC F value Pr(>F)
## habitantes 1 1.9255 20.345 -23.723 3.9724 0.053469 .
## ingresos 1 0.4931 18.912 -27.008 1.0174 0.319518
## asesinatos 1 26.1458 44.565 11.563 53.9400 8.452e-09 ***
## universitarios 1 3.9038 22.323 -19.547 8.0538 0.007247 **
## heladas 1 3.8665 22.286 -19.622 7.9769 0.007505 **
## area 1 0.0352 18.455 -28.111 0.0727 0.788930
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## => Se elimina: area (p = 0.7889 )
##
## --- Paso 3 : modelo actual = esp_vida ~ habitantes + ingresos + asesinatos + universitarios + heladas ---
## Df Sum of Sq RSS AIC F value Pr(>F)
## habitantes 1 1.896 20.350 -25.711 4.0058 0.052335 .
## ingresos 1 0.458 18.913 -29.008 0.9679 0.331277
## asesinatos 1 33.535 51.990 16.497 70.8700 2.641e-10 ***
## universitarios 1 4.583 23.038 -20.128 9.6860 0.003468 **
## heladas 1 3.832 22.286 -21.621 8.0978 0.007028 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## => Se elimina: ingresos (p = 0.3313 )
##
## --- Paso 4 : modelo actual = esp_vida ~ habitantes + asesinatos + universitarios + heladas ---
## Df Sum of Sq RSS AIC F value Pr(>F)
## habitantes 1 1.489 20.402 -27.597 3.1495 0.083563 .
## asesinatos 1 33.746 52.658 15.072 71.3718 1.972e-10 ***
## universitarios 1 4.764 23.676 -20.898 10.0753 0.002889 **
## heladas 1 4.127 23.040 -22.125 8.7293 0.005226 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## => Se elimina: habitantes (p = 0.08356 )
##
## --- Paso 5 : modelo actual = esp_vida ~ asesinatos + universitarios + heladas ---
## Df Sum of Sq RSS AIC F value Pr(>F)
## asesinatos 1 32.259 52.660 13.074 64.828 5.635e-10 ***
## universitarios 1 5.448 25.850 -18.946 10.949 0.001958 **
## heladas 1 5.323 25.725 -19.165 10.697 0.002179 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## => Todas las variables son significativas (p <= 0.01 ). Fin de la selección.
mod_backward <- mod_actual
# --- Orden de eliminación y p-valores ---
kable(historial, digits = 6, row.names = FALSE,
caption = "Selección regresiva (backward): orden de eliminación (alfa = 0.01)") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| paso | variable_eliminada | F | p_valor |
|---|---|---|---|
| 1 | analfabetismo | 0.010481 | 0.919010 |
| 2 | area | 0.072682 | 0.788930 |
| 3 | ingresos | 0.967869 | 0.331277 |
| 4 | habitantes | 3.149462 | 0.083563 |
# --- Modelo final ---
cat("Fórmula del modelo final (backward):", deparse(formula(mod_backward)), "\n")
## Fórmula del modelo final (backward): esp_vida ~ asesinatos + universitarios + heladas
coef_bw <- as.data.frame(summary(mod_backward)$coefficients)
names(coef_bw) <- c("Estimación", "Error Estándar", "t", "p-valor")
kable(round(coef_bw, 6), caption = "Coeficientes del modelo final (backward)") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE) |>
column_spec(5, bold = TRUE, color = ifelse(coef_bw$`p-valor` < 0.05, "green", "red"))
| Estimación | Error Estándar | t | p-valor | |
|---|---|---|---|---|
| (Intercept) | 71.935735 | 1.026193 | 70.099582 | 0.000000 |
| asesinatos | -0.301891 | 0.037495 | -8.051591 | 0.000000 |
| universitarios | 0.048241 | 0.014579 | 3.308882 | 0.001958 |
| heladas | -0.007713 | 0.002358 | -3.270686 | 0.002179 |
gl_bw <- data.frame(`R²` = summary(mod_backward)$r.squared,
`R² ajustado` = summary(mod_backward)$adj.r.squared,
`Sigma` = summary(mod_backward)$sigma, check.names = FALSE)
kable(round(gl_bw, 6), caption = "Bondad de ajuste: modelo backward") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| R² | R² ajustado | Sigma |
|---|---|---|
| 0.747667 | 0.729204 | 0.705409 |
Interpretacion:
Orden de eliminación: primero sale analfabetirsmo, luego area, luego ingresos y po rultimo habitantes.. Cada variable se elimina cuando, dadas las demás, su aporte no es significativo al 1 %.
Vemos que el p-valor de analfbaetismo es altisimo. Esto es sin dudad por la multicolinealidad que hemos visto que tienen analfabetismo y universitarios. Ya que estan fuertemente e inversamente relacionados. Luego si el modelo primero contuviese analfabetismno y luego universitarios, yo creo que se qeudaría fuera universitarios y no al reves.
Detención: el proceso para cuando todas las variables restantes tienen p ≤ 0.01; las que quedan son asesinatos universitarios y heladas.
Modelo final (backward):
resp ~ asesinatos + universitarios + heladas, con R²
ajustado = 0.7292
Comparación con forward: El modelo NO coincide con el apartado b (del add1). Esto se debe a que backward evalúa cada variable en presencia de todas las demás, y forward solo en presencia de las ya incluidas; con predictores correlacionados (como ya hemos visto anteriormente que si sucede) los caminos pueden divergir como es el caso de estas variables y estos modelos.
Se combinan las dos direcciones: tras cada ingreso
(add1()), se revisa si las variables ya incluidas siguen
siendo significativas (drop1()), y se elimina la que deje
de cumplir el criterio del 1 %.
ajustar <- function(vars) {
if (length(vars) == 0) lm(reformulate("1", resp), data = esp)
else lm(reformulate(vars, resp), data = esp)
}
incluidas <- character(0)
mod_actual <- ajustar(incluidas)
trayectoria <- data.frame(paso = integer(0), accion = character(0),
variable = character(0), F = numeric(0),
p_valor = numeric(0), modelo = character(0),
stringsAsFactors = FALSE)
registrar <- function(accion, variable, F, p) {
trayectoria <<- rbind(trayectoria,
data.frame(paso = nrow(trayectoria) + 1, accion = accion, variable = variable,
F = F, p_valor = p,
modelo = paste(incluidas, collapse = " + "),
stringsAsFactors = FALSE))
}
max_iter <- 4 * length(predictores) # tope de seguridad contra ciclos
iter <- 0
repeat {
iter <- iter + 1
if (iter > max_iter) { cat("Se alcanzó el tope de iteraciones.\n"); break }
# ---------- 1) PASO DE INGRESO (add1) ----------
candidatas <- setdiff(predictores, incluidas)
entro <- FALSE
if (length(candidatas) > 0) {
tab_a <- add1(mod_actual, scope = reformulate(predictores), test = "F")
tab_a <- tab_a[candidatas, , drop = FALSE]
cat("\n=== add1: modelo actual =", deparse(formula(mod_actual)), "===\n")
print(tab_a[, c("Df", "Sum of Sq", "RSS", "AIC", "F value", "Pr(>F)")])
mejor <- rownames(tab_a)[which.min(tab_a[["Pr(>F)"]])]
p_min <- min(tab_a[["Pr(>F)"]])
if (p_min < alpha) {
incluidas <- c(incluidas, mejor)
mod_actual <- ajustar(incluidas)
registrar("ingresa", mejor, tab_a[mejor, "F value"], p_min)
cat("=> Ingresa:", mejor, "(p =", format(p_min, digits = 4), ")\n")
entro <- TRUE
}
}
# ---------- 2) PASO DE REVISIÓN (drop1) ----------
salio <- FALSE
repeat {
if (length(incluidas) == 0) break
tab_d <- drop1(mod_actual, test = "F")
tab_d <- tab_d[incluidas, , drop = FALSE]
cat("\n=== drop1: modelo actual =", deparse(formula(mod_actual)), "===\n")
print(tab_d[, c("Df", "Sum of Sq", "RSS", "AIC", "F value", "Pr(>F)")])
peor <- rownames(tab_d)[which.max(tab_d[["Pr(>F)"]])]
p_max <- max(tab_d[["Pr(>F)"]])
if (p_max > alpha) {
incluidas <- setdiff(incluidas, peor)
mod_actual <- ajustar(incluidas)
registrar("sale", peor, tab_d[peor, "F value"], p_max)
cat("=> Sale:", peor, "(p =", format(p_max, digits = 4), ")\n")
salio <- TRUE
} else break
}
# ---------- 3) CRITERIO DE PARADA ----------
if (!entro && !salio) {
cat("\n=> No es posible agregar ni eliminar variables. Fin de la selección.\n")
break
}
}
##
## === add1: modelo actual = esp_vida ~ 1 ===
## Df Sum of Sq RSS AIC F value Pr(>F)
## habitantes 1 0.365 80.487 30.164 0.1952 0.66082
## analfabetismo 1 28.769 52.083 10.578 23.7521 1.530e-05 ***
## ingresos 1 7.820 73.032 25.791 4.6044 0.03758 *
## asesinatos 1 50.882 29.970 -14.290 73.0022 8.152e-11 ***
## universitarios 1 28.097 52.755 11.155 22.9018 2.036e-05 ***
## heladas 1 4.823 76.029 27.601 2.7277 0.10591
## area 1 0.775 80.077 29.935 0.4160 0.52239
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## => Ingresa: asesinatos (p = 8.152e-11 )
##
## === drop1: modelo actual = esp_vida ~ asesinatos ===
## Df Sum of Sq RSS AIC F value Pr(>F)
## asesinatos 1 50.882 80.852 28.368 73.002 8.152e-11 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## === add1: modelo actual = esp_vida ~ asesinatos ===
## Df Sum of Sq RSS AIC F value Pr(>F)
## habitantes 1 3.2826 26.688 -17.510 5.1659 0.02821 *
## analfabetismo 1 0.1933 29.777 -12.581 0.2726 0.60435
## ingresos 1 1.0796 28.891 -13.941 1.5694 0.21722
## universitarios 1 4.2458 25.725 -19.165 6.9320 0.01180 *
## heladas 1 4.1207 25.850 -18.946 6.6953 0.01322 *
## area 1 0.4782 29.492 -13.014 0.6811 0.41388
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## === drop1: modelo actual = esp_vida ~ asesinatos ===
## Df Sum of Sq RSS AIC F value Pr(>F)
## asesinatos 1 50.882 80.852 28.368 73.002 8.152e-11 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## => No es posible agregar ni eliminar variables. Fin de la selección.
mod_stepwise <- mod_actual
# --- Trayectoria completa ---
kable(trayectoria, digits = 6, row.names = FALSE,
caption = "Stepwise bidireccional: trayectoria completa (alfa = 0.01)") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| paso | accion | variable | F | p_valor | modelo |
|---|---|---|---|---|---|
| 1 | ingresa | asesinatos | 73.00217 | 0 | asesinatos |
# --- Modelo final ---
cat("Fórmula del modelo final (stepwise):", deparse(formula(mod_stepwise)), "\n")
## Fórmula del modelo final (stepwise): esp_vida ~ asesinatos
coef_sw <- as.data.frame(summary(mod_stepwise)$coefficients)
names(coef_sw) <- c("Estimación", "Error Estándar", "t", "p-valor")
kable(round(coef_sw, 6), caption = "Coeficientes del modelo final (stepwise)") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE) |>
column_spec(5, bold = TRUE, color = ifelse(coef_sw$`p-valor` < 0.05, "green", "red"))
| Estimación | Error Estándar | t | p-valor | |
|---|---|---|---|---|
| (Intercept) | 73.057001 | 0.289915 | 251.994794 | 0 |
| asesinatos | -0.294114 | 0.034423 | -8.544131 | 0 |
gl_sw <- data.frame(`R²` = summary(mod_stepwise)$r.squared,
`R² ajustado` = summary(mod_stepwise)$adj.r.squared,
`Sigma` = summary(mod_stepwise)$sigma, check.names = FALSE)
kable(round(gl_sw, 6), caption = "Bondad de ajuste: modelo stepwise") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| R² | R² ajustado | Sigma |
|---|---|---|
| 0.629317 | 0.620697 | 0.834858 |
Interpretacion:
resp ~ asesinatos, con R² ajustado = 0.6207 (mismo que
forward obvio)Me voy a referir al modelo de solo asesinatos como el modelo forward y al de heladas, asesinatos y universiatrios como el modelo backward en este apartado.
predictores <- c("habitantes", "analfabetismo", "ingresos", "asesinatos",
"universitarios", "heladas", "area")
resp <- setdiff(names(esp), predictores)
stopifnot(length(resp) == 1)
# Modelo completo: su sigma^2 se usa como referencia para el Cp de Mallows
mod_completo <- lm(reformulate(predictores, resp), data = esp)
s2_completo <- summary(mod_completo)$sigma^2
n <- nrow(esp)
# Indicadores de un modelo (p = número de parámetros, incluido el intercepto)
indicadores <- function(mod) {
p <- length(coef(mod))
sse <- sum(resid(mod)^2)
c(p = p,
R2_aj = summary(mod)$adj.r.squared,
Cp = sse / s2_completo - (n - 2 * p),
AIC = AIC(mod),
BIC = BIC(mod))
}
tabla_e <- rbind(Forward = indicadores(mod_forward),
Backward = indicadores(mod_backward),
Stepwise = indicadores(mod_stepwise))
tabla_e <- as.data.frame(tabla_e)
kable(round(tabla_e, 4),
caption = "Comparación de modelos: R²aj ↑ (max), Cp ↓, AIC ↓, BIC ↓ (min)") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE) |>
column_spec(2, bold = TRUE)
| p | R2_aj | Cp | AIC | BIC | |
|---|---|---|---|---|---|
| Forward | 2 | 0.6207 | 19.2205 | 115.4142 | 120.8342 |
| Backward | 4 | 0.7292 | 3.9936 | 102.1074 | 111.1407 |
| Stepwise | 2 | 0.6207 | 19.2205 | 115.4142 | 120.8342 |
# Variables de cada modelo
vars_df <- data.frame(
Modelo = c("Forward", "Backward", "Stepwise"),
Variables = c(
paste(attr(terms(mod_forward), "term.labels"), collapse = ", "),
paste(attr(terms(mod_backward), "term.labels"), collapse = ", "),
paste(attr(terms(mod_stepwise), "term.labels"), collapse = ", ")
)
)
kable(vars_df, caption = "Variables incluidas en cada modelo final") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| Modelo | Variables |
|---|---|
| Forward | asesinatos |
| Backward | asesinatos, universitarios, heladas |
| Stepwise | asesinatos |
# Convención de add1/drop1/step: extractAIC() omite constantes
# extractAIC = n*log(SSE/n) + 2p ; AIC() = n*log(SSE/n) + n*(1+log(2*pi)) + 2(p+1)
tabla_conv <- t(sapply(list(Forward = mod_forward, Backward = mod_backward,
Stepwise = mod_stepwise),
function(m) c(`AIC()` = AIC(m),
`extractAIC()` = extractAIC(m)[2])))
kable(round(tabla_conv, 4),
caption = "AIC() frente a extractAIC(): difieren por una constante") |>
kable_styling(bootstrap_options = c("striped", "hover", "condensed"), full_width = FALSE)
| AIC() | extractAIC() | |
|---|---|---|
| Forward | 115.4142 | -14.2902 |
| Backward | 102.1074 | -27.5971 |
| Stepwise | 115.4142 | -14.2902 |
Selección del modelo (completa con tus resultados):