Este informe modela el precio de oferta (en millones de pesos) de los
apartamentos usados del barrio La Flora, en la Zona
Norte de Cali, a partir de la base Datos_Vivienda. Primero
se plantean tres modelos de regresión lineal simple y luego dos modelos
de regresión lineal múltiple. En cada caso se interpretan los
coeficientes, se validan los supuestos del modelo (linealidad,
normalidad, homocedasticidad, independencia y, en los múltiples,
multicolinealidad) y se evalúa si una transformación logarítmica mejora
el cumplimiento de esos supuestos. Al final se hace una predicción
hipotética con predict() y se comparan los modelos
múltiples con el error absoluto medio (MAE).
library(readxl) # leer el archivo de Excel
library(dplyr) # manipulación de datos
library(ggplot2) # gráficos
library(plotly) # convierte los gráficos de ggplot en interactivos
library(knitr) # tablas en el informe
library(kableExtra) # formato de tablas
library(lmtest) # pruebas de Breusch-Pagan y Durbin-Watson
library(car) # VIF (multicolinealidad)
library(sandwich) # errores estándar robustos
library(MASS) # transformación de Box-Cox
# Cargar la base indicando el tipo de cada columna
datos <- read_excel("Datos_Vivienda.xlsx",
col_types = c("text", "text", "text",
"numeric", "numeric", "numeric",
"numeric", "numeric", "text", "text",
"numeric", "numeric"))
# Identificador único por fila
datos <- data.frame(id = 1:nrow(datos), datos)
# Normalizar el nombre del barrio (minúsculas y sin espacios sobrantes)
datos$barrio_norm <- tolower(trimws(datos$Barrio))
# Variantes de escritura del barrio La Flora que aparecen en la base
unique(datos$barrio_norm[grepl("flora", datos$barrio_norm)])
## [1] "la flora" "floralia" "urbanizacion la flora"
## [4] "flora industrial" "laflora" "norte la flora"
## [7] "flora"
En la base el barrio aparece escrito de varias formas. Se conservan
las variantes que corresponden a La Flora (la flora,
laflora, urbanizacion la flora,
norte la flora, flora) y se excluyen
Floralia y Flora Industrial, que son
barrios distintos. Además se filtra sólo el tipo
Apartamento.
variantes <- c("la flora", "laflora", "urbanizacion la flora", "norte la flora", "flora")
datos_sub <- datos %>%
filter(barrio_norm %in% variantes, Tipo == "Apartamento")
nrow(datos_sub) # registros encontrados
## [1] 331
# piso y Estrato se leyeron como texto: se convierten a número ("NA" pasa a NA)
datos_sub$piso <- suppressWarnings(as.numeric(datos_sub$piso))
datos_sub$Estrato <- as.numeric(datos_sub$Estrato)
# Registros con datos faltantes o inconsistentes
sum(is.na(datos_sub$parqueaderos)) # sin dato de parqueaderos
## [1] 7
sum(datos_sub$Habitaciones == 0) # apartamentos con 0 habitaciones (inconsistente)
## [1] 2
sum(is.na(datos_sub$piso)) # sin dato de piso
## [1] 74
# Se eliminan los registros sin parqueadero reportado y con 0 habitaciones
datos_sub <- datos_sub %>%
filter(!is.na(parqueaderos), Habitaciones > 0)
# Variable DUMMY: 1 si el apartamento tiene 2 o más parqueaderos, 0 si tiene uno
datos_sub$D_parq <- ifelse(datos_sub$parqueaderos >= 2, 1, 0)
nrow(datos_sub) # registros para el análisis
## [1] 322
table(datos_sub$D_parq)
##
## 0 1
## 204 118
La variable piso tiene muchos faltantes; no se eliminan
esos registros aquí para no perder información en los modelos simples.
En los modelos múltiples que usan piso, R descarta
automáticamente las filas sin ese dato (se reporta el tamaño de muestra
efectivo).
datos_sub %>%
summarise(n = n(),
precio_prom = mean(precio_millon),
precio_mediana = median(precio_millon),
area_prom = mean(Area_contruida),
banos_prom = mean(Banos),
habitaciones_prom = mean(Habitaciones)) %>%
kable(digits = 1, caption = "Tabla 1. Resumen de los apartamentos de La Flora") %>%
kable_styling(full_width = FALSE)
| n | precio_prom | precio_mediana | area_prom | banos_prom | habitaciones_prom |
|---|---|---|---|---|---|
| 322 | 336.5 | 320 | 99.2 | 2.5 | 2.9 |
# Correlaciones entre el precio y las variables candidatas
vars <- c("precio_millon", "Area_contruida", "Banos", "Habitaciones", "parqueaderos", "piso", "Estrato")
round(cor(datos_sub[, vars], use = "complete.obs"), 2)
## precio_millon Area_contruida Banos Habitaciones parqueaderos
## precio_millon 1.00 0.66 0.60 0.15 0.53
## Area_contruida 0.66 1.00 0.62 0.41 0.53
## Banos 0.60 0.62 1.00 0.49 0.50
## Habitaciones 0.15 0.41 0.49 1.00 0.24
## parqueaderos 0.53 0.53 0.50 0.24 1.00
## piso 0.43 0.19 0.19 0.04 0.37
## Estrato 0.10 0.10 0.08 -0.02 0.13
## piso Estrato
## precio_millon 0.43 0.10
## Area_contruida 0.19 0.10
## Banos 0.19 0.08
## Habitaciones 0.04 -0.02
## parqueaderos 0.37 0.13
## piso 1.00 0.06
## Estrato 0.06 1.00
# ggplotly() convierte el gráfico en interactivo (zoom y valores al pasar el mouse)
ggplotly(
ggplot(datos_sub, aes(x = precio_millon)) +
geom_histogram(binwidth = 40, fill = "#2c3e50", color = "white") +
labs(title = "Figura 1. Distribución del precio", x = "Precio (millones)", y = "Frecuencia") +
theme_minimal()
)
El precio se concentra entre 250 y 400 millones y tiene una cola larga hacia la derecha (pocos apartamentos de más de 600 millones). Esa asimetría anticipa posibles problemas de normalidad y de varianza no constante, y es la razón principal para probar transformaciones logarítmicas.
Para no repetir el mismo código en cada modelo, se define una función que aplica las pruebas formales y otra que dibuja los gráficos de diagnóstico.
# Pruebas formales de supuestos sobre los residuos de un modelo lm
validar <- function(modelo) {
sw <- shapiro.test(residuals(modelo)) # H0: residuos normales
bp <- bptest(modelo) # H0: varianza constante (homocedasticidad)
dw <- dwtest(modelo) # H0: no autocorrelación
data.frame(
Supuesto = c("Normalidad (Shapiro-Wilk)",
"Homocedasticidad (Breusch-Pagan)",
"Independencia (Durbin-Watson)"),
Estadistico = round(c(sw$statistic, bp$statistic, dw$statistic), 3),
p_valor = signif(c(sw$p.value, bp$p.value, dw$p.value), 3),
Conclusion = ifelse(c(sw$p.value, bp$p.value, dw$p.value) < 0.05,
"Se rechaza H0 (no cumple)", "No se rechaza H0 (cumple)")
)
}
# Gráficos de diagnóstico interactivos (2 x 2):
# residuos vs ajustados, QQ normal, escala-ubicación y residuos vs leverage.
# Al pasar el mouse sobre un punto se ve el id del apartamento, útil para ubicar atípicos.
diagnostico <- function(modelo) {
base <- eval(modelo$call$data) # datos con que se estimó el modelo
d <- data.frame(
id = base$id[as.integer(names(residuals(modelo)))],
ajustados = fitted(modelo),
residuos = residuals(modelo),
estand = rstandard(modelo), # residuos estandarizados
leverage = hatvalues(modelo),
cook = cooks.distance(modelo)
)
d$teorico <- qqnorm(d$estand, plot.it = FALSE)$x # cuantiles teóricos normales
g1 <- ggplot(d, aes(ajustados, residuos, text = paste("id:", id))) +
geom_point(color = "#2c3e50", alpha = 0.6) +
geom_hline(yintercept = 0, linetype = "dashed", color = "#e74c3c") +
labs(x = "Valores ajustados", y = "Residuos") + theme_minimal()
g2 <- ggplot(d, aes(teorico, estand, text = paste("id:", id))) +
geom_point(color = "#2c3e50", alpha = 0.6) +
geom_abline(slope = 1, intercept = 0, linetype = "dashed", color = "#e74c3c") +
labs(x = "Cuantiles teóricos", y = "Residuos estandarizados") + theme_minimal()
g3 <- ggplot(d, aes(ajustados, sqrt(abs(estand)), text = paste("id:", id))) +
geom_point(color = "#2c3e50", alpha = 0.6) +
labs(x = "Valores ajustados", y = "√|Residuos estandarizados|") + theme_minimal()
g4 <- ggplot(d, aes(leverage, estand, text = paste("id:", id, "<br>Cook:", round(cook, 3)))) +
geom_point(color = "#2c3e50", alpha = 0.6) +
geom_hline(yintercept = 0, linetype = "dashed", color = "#e74c3c") +
labs(x = "Leverage", y = "Residuos estandarizados") + theme_minimal()
titulos <- c("Residuos vs ajustados", "QQ normal", "Escala-ubicación", "Residuos vs leverage")
subplot(ggplotly(g1, tooltip = c("text", "x", "y")), ggplotly(g2, tooltip = c("text", "x", "y")),
ggplotly(g3, tooltip = c("text", "x", "y")), ggplotly(g4, tooltip = c("text", "x", "y")),
nrows = 2, margin = c(0.06, 0.06, 0.09, 0.09), titleX = TRUE, titleY = TRUE) %>%
layout(showlegend = FALSE,
annotations = lapply(1:4, function(i) list(
text = paste0("<b>", titulos[i], "</b>"), showarrow = FALSE,
xref = "paper", yref = "paper",
x = ifelse(i %% 2 == 1, 0.22, 0.78), y = ifelse(i <= 2, 1.01, 0.42),
xanchor = "center", yanchor = "bottom")))
}
Nota sobre Durbin-Watson: esta prueba detecta autocorrelación según el orden de las filas. Como los datos son de corte transversal (ofertas sin un orden temporal), un rechazo puede reflejar sólo cómo está ordenada la base (por ejemplo, por ubicación) y no un problema serio del modelo. Se reporta, pero se interpreta con cautela.
Se plantean tres modelos simples con el precio como variable dependiente y, como explicativas, el área construida, el número de baños y el número de habitaciones. Para cada uno se estima el modelo en niveles, se validan sus supuestos y luego se estima la versión con logaritmos para ver si los supuestos mejoran.
\[Precio_i = \beta_0 + \beta_1 \, Area_i + u_i\]
m1 <- lm(precio_millon ~ Area_contruida, data = datos_sub)
summary(m1)
##
## Call:
## lm(formula = precio_millon ~ Area_contruida, data = datos_sub)
##
## Residuals:
## Min 1Q Median 3Q Max
## -192.566 -42.439 1.843 50.623 142.237
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 131.4656 14.1738 9.275 <0.0000000000000002 ***
## Area_contruida 2.0669 0.1381 14.962 <0.0000000000000002 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 64.83 on 320 degrees of freedom
## Multiple R-squared: 0.4116, Adjusted R-squared: 0.4098
## F-statistic: 223.9 on 1 and 320 DF, p-value: < 0.00000000000000022
ggplotly(
ggplot(datos_sub, aes(x = Area_contruida, y = precio_millon)) +
geom_point(aes(text = paste("id:", id)), color = "#2c3e50", alpha = 0.6) +
geom_smooth(method = "lm", color = "#e74c3c", se = TRUE) +
labs(title = "Figura 2. Precio vs área construida", x = "Área (m²)", y = "Precio (millones)") +
theme_minimal()
)
La ecuación estimada es Precio = 131.47 + 2.07 × Área.
diagnostico(m1)
kable(validar(m1), caption = "Supuestos – Modelo 1 (niveles)") %>% kable_styling(full_width = FALSE)
| Supuesto | Estadistico | p_valor | Conclusion | |
|---|---|---|---|---|
| W | Normalidad (Shapiro-Wilk) | 0.988 | 0.00965 | Se rechaza H0 (no cumple) |
| BP | Homocedasticidad (Breusch-Pagan) | 64.454 | 0.00000 | Se rechaza H0 (no cumple) |
| DW | Independencia (Durbin-Watson) | 1.682 | 0.00207 | Se rechaza H0 (no cumple) |
En niveles el modelo no cumple homocedasticidad: la prueba de Breusch-Pagan rechaza con un p-valor muy pequeño y el gráfico de residuos vs ajustados muestra un embudo (los errores crecen con el precio). Tampoco cumple normalidad al 5%, aunque el QQ-plot muestra que la desviación se concentra en las colas (apartamentos atípicamente grandes o caros). Durbin-Watson rechaza la independencia, pero, como se explicó, en datos de corte transversal esto refleja el orden de la base (las ofertas aparecen ordenadas aproximadamente por longitud geográfica) más que una autocorrelación real.
\[\ln(Precio_i) = \beta_0 + \beta_1 \ln(Area_i) + u_i\]
m1_log <- lm(log(precio_millon) ~ log(Area_contruida), data = datos_sub)
summary(m1_log)
##
## Call:
## lm(formula = log(precio_millon) ~ log(Area_contruida), data = datos_sub)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.72422 -0.12006 0.02698 0.14731 0.35621
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 2.52083 0.23263 10.84 <0.0000000000000002 ***
## log(Area_contruida) 0.71460 0.05083 14.06 <0.0000000000000002 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.1969 on 320 degrees of freedom
## Multiple R-squared: 0.3818, Adjusted R-squared: 0.3799
## F-statistic: 197.7 on 1 and 320 DF, p-value: < 0.00000000000000022
diagnostico(m1_log)
kable(validar(m1_log), caption = "Supuestos – Modelo 1 (log-log)") %>% kable_styling(full_width = FALSE)
| Supuesto | Estadistico | p_valor | Conclusion | |
|---|---|---|---|---|
| W | Normalidad (Shapiro-Wilk) | 0.952 | 0.00000 | Se rechaza H0 (no cumple) |
| BP | Homocedasticidad (Breusch-Pagan) | 3.338 | 0.06770 | No se rechaza H0 (cumple) |
| DW | Independencia (Durbin-Watson) | 1.710 | 0.00445 | Se rechaza H0 (no cumple) |
Interpretación: en el modelo log-log el coeficiente es una elasticidad: un aumento de 1% en el área construida se asocia con un aumento de 0.71% en el precio. Como es menor que 1, el precio crece menos que proporcionalmente con el área (el precio por m² baja en los apartamentos más grandes).
¿Es pertinente la transformación? Parcialmente. El logaritmo corrige la heterocedasticidad (Breusch-Pagan ya no rechaza al 5%), pero empeora la normalidad de los residuos (el p-valor de Shapiro-Wilk baja aún más), porque el logaritmo abre la cola de los apartamentos pequeños y baratos. El R² baja ligeramente (0.382). Si el objetivo es hacer inferencia sobre la pendiente, la versión log-log es preferible por la varianza constante; con un tamaño de muestra de más de 300 datos, la falta de normalidad tiene menos consecuencias gracias al teorema del límite central.
\[Precio_i = \beta_0 + \beta_1 \, Banos_i + u_i\]
m2 <- lm(precio_millon ~ Banos, data = datos_sub)
summary(m2)
##
## Call:
## lm(formula = precio_millon ~ Banos, data = datos_sub)
##
## Residuals:
## Min 1Q Median 3Q Max
## -205.878 -43.175 -1.222 42.216 310.434
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 164.815 14.298 11.53 <0.0000000000000002 ***
## Banos 68.688 5.506 12.47 <0.0000000000000002 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 69.32 on 320 degrees of freedom
## Multiple R-squared: 0.3272, Adjusted R-squared: 0.3251
## F-statistic: 155.6 on 1 and 320 DF, p-value: < 0.00000000000000022
ggplotly(
ggplot(datos_sub, aes(x = factor(Banos), y = precio_millon)) +
geom_boxplot(fill = "#95a5a6") +
labs(title = "Figura 3. Precio según número de baños", x = "Baños", y = "Precio (millones)") +
theme_minimal()
)
La ecuación estimada es Precio = 164.81 + 68.69 × Baños.
diagnostico(m2)
kable(validar(m2), caption = "Supuestos – Modelo 2 (niveles)") %>% kable_styling(full_width = FALSE)
| Supuesto | Estadistico | p_valor | Conclusion | |
|---|---|---|---|---|
| W | Normalidad (Shapiro-Wilk) | 0.988 | 0.01120 | Se rechaza H0 (no cumple) |
| BP | Homocedasticidad (Breusch-Pagan) | 9.198 | 0.00242 | Se rechaza H0 (no cumple) |
| DW | Independencia (Durbin-Watson) | 1.840 | 0.07500 | No se rechaza H0 (cumple) |
En niveles se rechaza la homocedasticidad (la dispersión del precio crece con el número de baños, visible en la Figura 3) y también la normalidad al 5%. Durbin-Watson no rechaza al 5%.
Como Banos es una variable de conteo con pocos valores,
se transforma sólo la variable dependiente:
\[\ln(Precio_i) = \beta_0 + \beta_1 \, Banos_i + u_i\]
m2_log <- lm(log(precio_millon) ~ Banos, data = datos_sub)
summary(m2_log)
##
## Call:
## lm(formula = log(precio_millon) ~ Banos, data = datos_sub)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.77711 -0.13154 0.01121 0.14301 0.58030
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 5.31161 0.04364 121.71 <0.0000000000000002 ***
## Banos 0.19048 0.01681 11.33 <0.0000000000000002 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.2116 on 320 degrees of freedom
## Multiple R-squared: 0.2864, Adjusted R-squared: 0.2842
## F-statistic: 128.4 on 1 and 320 DF, p-value: < 0.00000000000000022
diagnostico(m2_log)
kable(validar(m2_log), caption = "Supuestos – Modelo 2 (log-lin)") %>% kable_styling(full_width = FALSE)
| Supuesto | Estadistico | p_valor | Conclusion | |
|---|---|---|---|---|
| W | Normalidad (Shapiro-Wilk) | 0.981 | 0.000269 | Se rechaza H0 (no cumple) |
| BP | Homocedasticidad (Breusch-Pagan) | 0.141 | 0.708000 | No se rechaza H0 (cumple) |
| DW | Independencia (Durbin-Watson) | 1.844 | 0.080100 | No se rechaza H0 (cumple) |
Interpretación: cada baño adicional se asocia con un aumento aproximado de 19% en el precio (de forma exacta, 21%, calculado como 100·(e^β − 1)).
¿Es pertinente la transformación? Sí, para la varianza: el logaritmo resuelve por completo la heterocedasticidad (Breusch-Pagan con p-valor alto). La normalidad sigue sin cumplirse e incluso empeora un poco, de nuevo por los apartamentos más baratos. En balance, el modelo log-lin es más confiable para inferencia que el de niveles.
\[Precio_i = \beta_0 + \beta_1 \, Habitaciones_i + u_i\]
m3 <- lm(precio_millon ~ Habitaciones, data = datos_sub)
summary(m3)
##
## Call:
## lm(formula = precio_millon ~ Habitaciones, data = datos_sub)
##
## Residuals:
## Min 1Q Median 3Q Max
## -187.78 -54.21 -17.78 62.22 388.66
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 267.085 26.661 10.018 < 0.0000000000000002 ***
## Habitaciones 23.564 8.907 2.646 0.00856 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 83.6 on 320 degrees of freedom
## Multiple R-squared: 0.0214, Adjusted R-squared: 0.01835
## F-statistic: 6.999 on 1 and 320 DF, p-value: 0.008558
La ecuación estimada es Precio = 267.09 + 23.56 × Habitaciones.
diagnostico(m3)
kable(validar(m3), caption = "Supuestos – Modelo 3 (niveles)") %>% kable_styling(full_width = FALSE)
| Supuesto | Estadistico | p_valor | Conclusion | |
|---|---|---|---|---|
| W | Normalidad (Shapiro-Wilk) | 0.976 | 0.0000289 | Se rechaza H0 (no cumple) |
| BP | Homocedasticidad (Breusch-Pagan) | 7.401 | 0.0065200 | Se rechaza H0 (no cumple) |
| DW | Independencia (Durbin-Watson) | 1.855 | 0.0939000 | No se rechaza H0 (cumple) |
En niveles se rechazan normalidad y homocedasticidad.
\[\ln(Precio_i) = \beta_0 + \beta_1 \, Habitaciones_i + u_i\]
m3_log <- lm(log(precio_millon) ~ Habitaciones, data = datos_sub)
summary(m3_log)
##
## Call:
## lm(formula = log(precio_millon) ~ Habitaciones, data = datos_sub)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.78022 -0.15323 -0.02254 0.20061 0.77158
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 5.61795 0.07930 70.847 <0.0000000000000002 ***
## Habitaciones 0.05764 0.02649 2.176 0.0303 *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.2487 on 320 degrees of freedom
## Multiple R-squared: 0.01458, Adjusted R-squared: 0.0115
## F-statistic: 4.733 on 1 and 320 DF, p-value: 0.03032
diagnostico(m3_log)
kable(validar(m3_log), caption = "Supuestos – Modelo 3 (log-lin)") %>% kable_styling(full_width = FALSE)
| Supuesto | Estadistico | p_valor | Conclusion | |
|---|---|---|---|---|
| W | Normalidad (Shapiro-Wilk) | 0.992 | 0.1040 | No se rechaza H0 (cumple) |
| BP | Homocedasticidad (Breusch-Pagan) | 3.168 | 0.0751 | No se rechaza H0 (cumple) |
| DW | Independencia (Durbin-Watson) | 1.846 | 0.0808 | No se rechaza H0 (cumple) |
Interpretación: cada habitación adicional se asocia con un precio 5.8% mayor, en promedio (significativo al 5%).
¿Es pertinente la transformación? Sí: en la versión log-lin se cumplen normalidad, homocedasticidad e independencia al 5%. Es el único modelo simple en el que la transformación corrige todos los supuestos. Sin embargo, el poder explicativo sigue siendo muy bajo, por lo que, aunque es un modelo estadísticamente “bien comportado”, es un mal modelo para predecir el precio.
comp_simple <- data.frame(
Modelo = c("1. Precio ~ Área", "1. ln(Precio) ~ ln(Área)",
"2. Precio ~ Baños", "2. ln(Precio) ~ Baños",
"3. Precio ~ Habitaciones", "3. ln(Precio) ~ Habitaciones"),
R2 = sapply(list(m1, m1_log, m2, m2_log, m3, m3_log), function(m) summary(m)$r.squared),
p_Shapiro = sapply(list(m1, m1_log, m2, m2_log, m3, m3_log), function(m) shapiro.test(residuals(m))$p.value),
p_BreuschPagan = sapply(list(m1, m1_log, m2, m2_log, m3, m3_log), function(m) bptest(m)$p.value)
)
kable(comp_simple, digits = 4, caption = "Tabla 2. Resumen de modelos simples") %>% kable_styling(full_width = FALSE)
| Modelo | R2 | p_Shapiro | p_BreuschPagan |
|---|---|---|---|
|
0.4116 | 0.0096 | 0.0000 |
|
0.3818 | 0.0000 | 0.0677 |
|
0.3272 | 0.0112 | 0.0024 |
|
0.2864 | 0.0003 | 0.7077 |
|
0.0214 | 0.0000 | 0.0065 |
|
0.0146 | 0.1045 | 0.0751 |
El área construida es el mejor predictor individual del precio, seguido por el número de baños. En los tres casos la transformación logarítmica ayuda sobre todo con la homocedasticidad, que es el supuesto más afectado en niveles. Ningún modelo simple supera un R² de 0.42, lo que justifica pasar a modelos múltiples.
Los modelos múltiples incluyen piso, que tiene datos
faltantes. Para que todos los modelos (y el proceso step) se
estimen sobre las mismas observaciones, se trabaja con los registros
completos.
datos_m <- datos_sub %>%
filter(!is.na(piso)) %>%
dplyr::select(id, precio_millon, Area_contruida, Banos, Habitaciones, piso, Estrato, parqueaderos, D_parq)
nrow(datos_m)
## [1] 250
Se parte de un modelo completo con todas las variables candidatas,
incluida la dummy de parqueaderos, y se aplica step() en
ambas direcciones (agregar y quitar variables), que elige el modelo con
menor criterio de información de Akaike (AIC).
modelo_completo <- lm(precio_millon ~ Area_contruida + Banos + Habitaciones + piso + Estrato + D_parq,
data = datos_m)
summary(modelo_completo)
##
## Call:
## lm(formula = precio_millon ~ Area_contruida + Banos + Habitaciones +
## piso + Estrato + D_parq, data = datos_m)
##
## Residuals:
## Min 1Q Median 3Q Max
## -157.161 -29.498 -0.437 33.397 133.821
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 142.6983 57.4289 2.485 0.0136 *
## Area_contruida 1.8376 0.2118 8.676 0.000000000000000598 ***
## Banos 39.1106 6.3188 6.190 0.000000002547821623 ***
## Habitaciones -36.2946 6.8393 -5.307 0.000000251302615797 ***
## piso 5.6111 0.9228 6.081 0.000000004611287319 ***
## Estrato -3.5456 10.8174 -0.328 0.7434
## D_parq 13.7817 8.3624 1.648 0.1006
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 50.2 on 243 degrees of freedom
## Multiple R-squared: 0.628, Adjusted R-squared: 0.6188
## F-statistic: 68.38 on 6 and 243 DF, p-value: < 0.00000000000000022
seleccion <- step(modelo_completo, direction = "both", trace = 1)
## Start: AIC=1964.9
## precio_millon ~ Area_contruida + Banos + Habitaciones + piso +
## Estrato + D_parq
##
## Df Sum of Sq RSS AIC
## - Estrato 1 271 612622 1963.0
## <none> 612352 1964.9
## - D_parq 1 6845 619196 1965.7
## - Habitaciones 1 70967 683319 1990.3
## - piso 1 93172 705523 1998.3
## - Banos 1 96540 708892 1999.5
## - Area_contruida 1 189696 802047 2030.4
##
## Step: AIC=1963.01
## precio_millon ~ Area_contruida + Banos + Habitaciones + piso +
## D_parq
##
## Df Sum of Sq RSS AIC
## <none> 612622 1963.0
## - D_parq 1 6674 619297 1963.7
## + Estrato 1 271 612352 1964.9
## - Habitaciones 1 70719 683341 1988.3
## - piso 1 93094 705717 1996.4
## - Banos 1 96325 708947 1997.5
## - Area_contruida 1 189438 802061 2028.4
El procedimiento elimina la variable Estrato (casi todos los apartamentos de La Flora son estrato 5, así que no aporta variación) y conserva el área, los baños, las habitaciones, el piso y la dummy de parqueaderos. Ese modelo seleccionado es el Modelo múltiple 1.
\[Precio_i = \beta_0 + \beta_1 Area_i + \beta_2 Banos_i + \beta_3 Habitaciones_i + \beta_4 Piso_i + \beta_5 D\_parq_i + u_i\]
donde \(D\_parq_i = 1\) si el apartamento tiene 2 o más parqueaderos y 0 si tiene uno.
mm1 <- lm(precio_millon ~ Area_contruida + Banos + Habitaciones + piso + D_parq, data = datos_m)
summary(mm1)
##
## Call:
## lm(formula = precio_millon ~ Area_contruida + Banos + Habitaciones +
## piso + D_parq, data = datos_m)
##
## Residuals:
## Min 1Q Median 3Q Max
## -157.260 -29.817 -0.382 33.112 133.629
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 125.2226 21.2975 5.880 0.000000013447839984 ***
## Area_contruida 1.8344 0.2112 8.686 0.000000000000000549 ***
## Banos 39.0504 6.3046 6.194 0.000000002473451924 ***
## Habitaciones -36.1167 6.8052 -5.307 0.000000250016693855 ***
## piso 5.6086 0.9211 6.089 0.000000004378526281 ***
## D_parq 13.5676 8.3216 1.630 0.104
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 50.11 on 244 degrees of freedom
## Multiple R-squared: 0.6279, Adjusted R-squared: 0.6202
## F-statistic: 82.33 on 5 and 244 DF, p-value: < 0.00000000000000022
Manteniendo todo lo demás constante (ceteris paribus):
diagnostico(mm1)
kable(validar(mm1), caption = "Supuestos – Modelo múltiple 1") %>% kable_styling(full_width = FALSE)
| Supuesto | Estadistico | p_valor | Conclusion | |
|---|---|---|---|---|
| W | Normalidad (Shapiro-Wilk) | 0.996 | 0.81700 | No se rechaza H0 (cumple) |
| BP | Homocedasticidad (Breusch-Pagan) | 24.299 | 0.00019 | Se rechaza H0 (no cumple) |
| DW | Independencia (Durbin-Watson) | 1.938 | 0.30400 | No se rechaza H0 (cumple) |
# Multicolinealidad: VIF > 5 (o > 10) indicaría un problema
vif(mm1)
## Area_contruida Banos Habitaciones piso D_parq
## 1.863310 1.970917 1.366785 1.160396 1.637304
Como hay heterocedasticidad, los coeficientes siguen siendo insesgados, pero los errores estándar usuales no son confiables. Se recalculan con errores estándar robustos (HC1):
coeftest(mm1, vcov = vcovHC(mm1, type = "HC1"))
##
## t test of coefficients:
##
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 125.22256 24.25290 5.1632 0.000000503843 ***
## Area_contruida 1.83444 0.28037 6.5429 0.000000000352 ***
## Banos 39.05036 6.72273 5.8087 0.000000019546 ***
## Habitaciones -36.11667 7.00489 -5.1559 0.000000521818 ***
## piso 5.60855 0.88586 6.3312 0.000000001158 ***
## D_parq 13.56760 9.74653 1.3920 0.1652
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Con errores robustos las conclusiones se mantienen: área, baños, habitaciones y piso siguen siendo significativos, y la dummy de parqueaderos sigue sin serlo.
Se busca primero qué transformación de la variable dependiente sugiere el método de Box-Cox (λ = 0 equivale a logaritmo; λ = 1, a no transformar):
bc <- boxcox(mm1, lambda = seq(-1, 2, 0.05), plotit = FALSE) # log-verosimilitud para cada λ
lambda_opt <- bc$x[which.max(bc$y)]
lambda_opt
## [1] 0.65
ggplotly(
ggplot(data.frame(lambda = bc$x, logLik = bc$y), aes(lambda, logLik)) +
geom_line(color = "#2c3e50") +
geom_vline(xintercept = lambda_opt, linetype = "dashed", color = "#e74c3c") +
geom_vline(xintercept = c(0, 1), linetype = "dotted", color = "gray50") +
labs(title = "Figura. Box-Cox: log-verosimilitud según λ (0 = log, 1 = niveles)",
x = "λ", y = "Log-verosimilitud") +
theme_minimal()
)
El λ óptimo es 0.65, entre el logaritmo (0) y los niveles (1). Esto indica que transformar puede ayudar, pero el logaritmo no es necesariamente la mejor opción. Para comprobarlo se estima el modelo múltiple 2 en logaritmos.
Se aplica de nuevo step(), partiendo del modelo completo
con el precio y el área en logaritmos:
modelo_completo_log <- lm(log(precio_millon) ~ log(Area_contruida) + Banos + Habitaciones + piso + Estrato + D_parq,
data = datos_m)
seleccion_log <- step(modelo_completo_log, direction = "both", trace = 0)
formula(seleccion_log)
## log(precio_millon) ~ log(Area_contruida) + Banos + Habitaciones +
## piso
En la especificación logarítmica el step elimina tanto el estrato como la dummy de parqueaderos.
\[\ln(Precio_i) = \beta_0 + \beta_1 \ln(Area_i) + \beta_2 Banos_i + \beta_3 Habitaciones_i + \beta_4 Piso_i + u_i\]
mm2 <- lm(log(precio_millon) ~ log(Area_contruida) + Banos + Habitaciones + piso, data = datos_m)
summary(mm2)
##
## Call:
## lm(formula = log(precio_millon) ~ log(Area_contruida) + Banos +
## Habitaciones + piso, data = datos_m)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.61057 -0.08883 0.00184 0.10842 0.38029
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 3.037839 0.256292 11.853 < 0.0000000000000002 ***
## log(Area_contruida) 0.585374 0.062620 9.348 < 0.0000000000000002 ***
## Banos 0.117771 0.018813 6.260 0.000000001710 ***
## Habitaciones -0.112574 0.021052 -5.348 0.000000204272 ***
## piso 0.018840 0.002717 6.934 0.000000000036 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.1549 on 245 degrees of freedom
## Multiple R-squared: 0.6033, Adjusted R-squared: 0.5968
## F-statistic: 93.15 on 4 and 245 DF, p-value: < 0.00000000000000022
Ceteris paribus:
diagnostico(mm2)
kable(validar(mm2), caption = "Supuestos – Modelo múltiple 2 (log)") %>% kable_styling(full_width = FALSE)
| Supuesto | Estadistico | p_valor | Conclusion | |
|---|---|---|---|---|
| W | Normalidad (Shapiro-Wilk) | 0.988 | 0.03560 | Se rechaza H0 (no cumple) |
| BP | Homocedasticidad (Breusch-Pagan) | 17.552 | 0.00151 | Se rechaza H0 (no cumple) |
| DW | Independencia (Durbin-Watson) | 1.931 | 0.28600 | No se rechaza H0 (cumple) |
vif(mm2)
## log(Area_contruida) Banos Habitaciones piso
## 1.676325 1.835317 1.367786 1.055836
comp_mult <- data.frame(
Modelo = c("Múltiple 1 (niveles)", "Múltiple 2 (log)"),
n = c(nobs(mm1), nobs(mm2)),
R2_ajustado = c(summary(mm1)$adj.r.squared, summary(mm2)$adj.r.squared),
p_Shapiro = c(shapiro.test(residuals(mm1))$p.value, shapiro.test(residuals(mm2))$p.value),
p_BreuschPagan = c(bptest(mm1)$p.value, bptest(mm2)$p.value),
p_DurbinWatson = c(dwtest(mm1)$p.value, dwtest(mm2)$p.value)
)
kable(comp_mult, digits = 4, caption = "Tabla 3. Supuestos de los modelos múltiples") %>% kable_styling(full_width = FALSE)
| Modelo | n | R2_ajustado | p_Shapiro | p_BreuschPagan | p_DurbinWatson |
|---|---|---|---|---|---|
| Múltiple 1 (niveles) | 250 | 0.6202 | 0.8167 | 0.0002 | 0.3037 |
| Múltiple 2 (log) | 250 | 0.5968 | 0.0356 | 0.0015 | 0.2858 |
En el modelo múltiple la transformación logarítmica no mejora los supuestos: la heterocedasticidad persiste en ambos modelos, y el logaritmo además hace que se pierda la normalidad de los residuos, que sí se cumplía en niveles (Shapiro-Wilk rechaza al 5% en el modelo log). Esto coincide con el resultado de Box-Cox (λ ≈ 0.7, más cerca de 1 que de 0). Por eso, en el modelo múltiple se prefiere la especificación en niveles con errores estándar robustos. El modelo log sigue siendo útil por su interpretación en porcentajes y elasticidades.
Se predice el precio de un apartamento hipotético en La Flora con estas características: 100 m², 2 baños, 3 habitaciones, piso 5 y 2 parqueaderos (D_parq = 1).
nuevo <- data.frame(Area_contruida = 100, Banos = 2, Habitaciones = 3, piso = 5, D_parq = 1)
# Modelo múltiple 1 (niveles): predicción puntual e intervalos
pred_mm1_conf <- predict(mm1, newdata = nuevo, interval = "confidence", level = 0.95)
pred_mm1_pred <- predict(mm1, newdata = nuevo, interval = "prediction", level = 0.95)
pred_mm1_conf
## fit lwr upr
## 1 320.0275 305.1217 334.9332
pred_mm1_pred
## fit lwr upr
## 1 320.0275 220.2101 419.8449
# Modelo múltiple 2 (log): la predicción sale en ln(precio), se devuelve a millones con exp()
pred_mm2 <- predict(mm2, newdata = nuevo, interval = "prediction", level = 0.95)
exp(pred_mm2)
## fit lwr upr
## 1 306.618 225.653 416.6335
Interpretación:
El error absoluto medio mide, en millones de pesos, cuánto se equivoca en promedio el modelo:
\[MAE = \frac{1}{n}\sum_{i=1}^{n} |y_i - \hat{y}_i|\]
Para evaluar los modelos con datos que no usaron para estimarse, se
dividen las observaciones en entrenamiento (80%) y
prueba (20%). Los modelos se reestiman con el conjunto
de entrenamiento y el MAE se calcula sobre el de prueba. En el modelo
logarítmico, las predicciones se pasan a millones con exp()
antes de calcular el error.
set.seed(123) # semilla para que la partición sea reproducible
idx <- sample(1:nrow(datos_m), size = round(0.8 * nrow(datos_m)))
train <- datos_m[idx, ]
test <- datos_m[-idx, ]
# Reestimar los modelos sólo con entrenamiento
mm1_tr <- lm(precio_millon ~ Area_contruida + Banos + Habitaciones + piso + D_parq, data = train)
mm2_tr <- lm(log(precio_millon) ~ log(Area_contruida) + Banos + Habitaciones + piso, data = train)
# Predicciones sobre el conjunto de prueba (en millones)
pred1_test <- predict(mm1_tr, newdata = test)
pred2_test <- exp(predict(mm2_tr, newdata = test))
# MAE dentro de muestra (con todos los datos) y fuera de muestra (prueba)
mae <- function(real, pred) mean(abs(real - pred))
tabla_mae <- data.frame(
Modelo = c("Múltiple 1 (niveles)", "Múltiple 2 (log)"),
MAE_dentro_muestra = c(mae(datos_m$precio_millon, fitted(mm1)),
mae(datos_m$precio_millon, exp(fitted(mm2)))),
MAE_prueba = c(mae(test$precio_millon, pred1_test),
mae(test$precio_millon, pred2_test))
)
tabla_mae$MAE_prueba_pct_media <- 100 * tabla_mae$MAE_prueba / mean(test$precio_millon)
kable(tabla_mae, digits = 2, caption = "Tabla 4. MAE de los modelos múltiples (millones de pesos)") %>%
kable_styling(full_width = FALSE)
| Modelo | MAE_dentro_muestra | MAE_prueba | MAE_prueba_pct_media |
|---|---|---|---|
| Múltiple 1 (niveles) | 39.53 | 38.08 | 10.94 |
| Múltiple 2 (log) | 39.11 | 38.50 | 11.06 |
# Gráfico de valores reales vs predichos en el conjunto de prueba
g_pred <- data.frame(Real = rep(test$precio_millon, 2),
Predicho = c(pred1_test, pred2_test),
Modelo = rep(c("Múltiple 1 (niveles)", "Múltiple 2 (log)"), each = nrow(test))) %>%
ggplot(aes(x = Real, y = Predicho, color = Modelo)) +
geom_point(alpha = 0.7) +
geom_abline(slope = 1, intercept = 0, linetype = "dashed") +
scale_color_manual(values = c("#2c3e50", "#e67e22")) +
labs(title = "Figura 4. Precio real vs predicho (conjunto de prueba)",
x = "Precio real (millones)", y = "Precio predicho (millones)") +
theme_minimal()
ggplotly(g_pred) # clic en la leyenda para ocultar/mostrar cada modelo
Interpretación: en el conjunto de prueba, el modelo múltiple 1 se equivoca en promedio en 38.1 millones por apartamento (cerca del 10.9% del precio medio), y el modelo logarítmico en 38.5 millones. El modelo en niveles tiene el menor error fuera de muestra, aunque la diferencia entre ambos es pequeña. Los MAE dentro y fuera de muestra son parecidos, lo que indica que los modelos no están sobreajustados. En la Figura 4 los puntos se alejan más de la diagonal en los precios altos, que es justamente la heterocedasticidad detectada en la validación de supuestos.