El Caney es un barrio residencial del sur de Cali, de estrato 4 en su mayoría, donde conviven conjuntos de apartamentos con casas. Esa mezcla lo vuelve un buen laboratorio para la regresión: hay suficiente variedad de tamaños, tipos de inmueble y precios para preguntarle a los datos qué hace que una vivienda cueste más que otra.
En este informe se filtra la base Datos_Vivienda (8.322 ofertas de vivienda usada en Cali) por el barrio El Caney, sin filtrar por tipo de inmueble, y se resuelven dos bloques:
predict() y el cálculo del
MAE.# Si falta alguna librería, instálala una sola vez con:
# install.packages(c("readxl", "dplyr", "ggplot2", "plotly", "lmtest", "car"))
library(readxl) # leer el archivo de Excel
library(dplyr) # filtrar, transformar y resumir datos
library(ggplot2) # gráficos
library(plotly) # gráficos interactivos
library(lmtest) # pruebas de supuestos: Breusch-Pagan, Durbin-Watson y RESET
library(car) # VIF (multicolinealidad) y errores estándar robustos
El archivo Datos_Vivienda.xlsx debe estar en la misma
carpeta que este .Rmd.
datos <- read_excel("Datos_Vivienda.xlsx",
col_types = c("text", "text", "text",
"numeric", "numeric", "numeric",
"numeric", "numeric", "text", "text",
"numeric", "numeric"),
na = c("", "NA")) # en el Excel, los faltantes vienen escritos como "NA"
# Se fijan los nombres de las 12 columnas (en el mismo orden del archivo) para que
# el código no dependa de mayúsculas o tildes. Si tu archivo ya trae estos nombres,
# esta línea no cambia nada.
names(datos) <- c("Zona", "piso", "Estrato", "precio_millon", "Area_contruida",
"parqueaderos", "Banos", "Habitaciones", "Tipo", "Barrio",
"cordenada_longitud", "Cordenada_latitud")
# Identificador único por fila, como en el ejemplo de clase
datos$id <- 1:nrow(datos)
dim(datos)
## [1] 8319 13
En la base, un mismo barrio aparece escrito de varias formas: con o sin artículo, en mayúsculas o minúsculas, o con un apellido de sector. Antes de filtrar conviene ver todas las grafías que contienen la palabra caney:
# Versión estandarizada del nombre: minúsculas y sin espacios sobrantes
datos$barrio_std <- tolower(trimws(datos$Barrio))
# Todas las formas en que aparece El Caney en la base
table(datos$Barrio[grepl("caney", datos$barrio_std)])
##
## caney caney especial el caney El Caney
## 88 5 208 1
“El Caney”, “el caney” y “caney” son el mismo barrio escrito de tres maneras, y “caney especial” es un sector dentro de El Caney. Si filtráramos solo por “el caney” perderíamos casi un tercio de las ofertas. Por eso el filtro busca la palabra caney en cualquier parte del nombre y luego le asigna una sola etiqueta a todo el grupo.
datos_sub <- datos %>%
filter(grepl("caney", barrio_std)) %>%
mutate(Barrio = "El Caney",
Estrato = as.numeric(Estrato), # viene como texto desde el Excel
Tipo = factor(Tipo, levels = c("Apartamento", "Casa")))
nrow(datos_sub)
## [1] 302
table(datos_sub$Tipo)
##
## Apartamento Casa
## 184 118
Una vivienda en venta con cero habitaciones o cero baños es un error de digitación, y además impide aplicar logaritmos (log(0) no existe). Esos registros se revisan y se retiran:
datos_sub %>%
filter(Habitaciones == 0 | Banos == 0) %>%
select(id, Tipo, precio_millon, Area_contruida, Habitaciones, Banos)
| id | Tipo | precio_millon | Area_contruida | Habitaciones | Banos |
|---|---|---|---|---|---|
| 1382 | Casa | 370 | 280 | 0 | 3 |
| 1425 | Casa | 395 | 220 | 0 | 0 |
| 1594 | Apartamento | 152 | 60 | 0 | 2 |
datos_sub <- datos_sub %>% filter(Habitaciones > 0, Banos > 0)
nrow(datos_sub)
## [1] 299
# Valores faltantes por variable
colSums(is.na(datos_sub[, c("precio_millon", "Area_contruida", "Habitaciones",
"Banos", "parqueaderos", "Estrato", "Tipo", "piso")]))
## precio_millon Area_contruida Habitaciones Banos parqueaderos
## 0 0 0 0 63
## Estrato Tipo piso
## 0 0 76
Quedan 299 ofertas, muy por encima del mínimo de 30.
Las variables centrales (precio, área, habitaciones, baños, estrato y
tipo) están completas. Faltan datos en parqueaderos (63
registros) y en piso. El piso no se usará: no aplica a las
casas y está vacío en muchos registros.
# Resumen por tipo de vivienda y total del barrio
resumen <- datos_sub %>%
group_by(Tipo) %>%
summarise(ofertas = n(),
precio_promedio = round(mean(precio_millon), 1),
precio_mediano = median(precio_millon),
area_promedio = round(mean(Area_contruida), 1),
habitaciones = round(mean(Habitaciones), 1),
banos = round(mean(Banos), 1),
precio_m2 = round(median(precio_millon / Area_contruida), 2))
total <- datos_sub %>%
summarise(ofertas = n(),
precio_promedio = round(mean(precio_millon), 1),
precio_mediano = median(precio_millon),
area_promedio = round(mean(Area_contruida), 1),
habitaciones = round(mean(Habitaciones), 1),
banos = round(mean(Banos), 1),
precio_m2 = round(median(precio_millon / Area_contruida), 2)) %>%
mutate(Tipo = "Total El Caney")
rbind(as.data.frame(resumen), as.data.frame(total))
| Tipo | ofertas | precio_promedio | precio_mediano | area_promedio | habitaciones | banos | precio_m2 |
|---|---|---|---|---|---|---|---|
| Apartamento | 183 | 189.7 | 160 | 69.9 | 2.6 | 2.1 | 2.66 |
| Casa | 116 | 337.1 | 335 | 190.8 | 4.3 | 3.5 | 1.85 |
| Total El Caney | 299 | 246.9 | 230 | 116.8 | 3.3 | 2.7 | 2.50 |
# Correlaciones entre el precio y las variables cuantitativas
cor_tabla <- round(cor(datos_sub[, c("precio_millon", "Area_contruida", "Habitaciones",
"Banos", "parqueaderos", "Estrato")],
use = "complete.obs"), 2)
cor_tabla
## precio_millon Area_contruida Habitaciones Banos parqueaderos
## precio_millon 1.00 0.82 0.69 0.71 0.50
## Area_contruida 0.82 1.00 0.66 0.70 0.57
## Habitaciones 0.69 0.66 1.00 0.75 0.40
## Banos 0.71 0.70 0.75 1.00 0.41
## parqueaderos 0.50 0.57 0.40 0.41 1.00
## Estrato 0.23 0.07 0.09 0.15 0.00
## Estrato
## precio_millon 0.23
## Area_contruida 0.07
## Habitaciones 0.09
## Banos 0.15
## parqueaderos 0.00
## Estrato 1.00
ggplotly(
ggplot(datos_sub, aes(x = precio_millon)) +
geom_histogram(binwidth = 40, fill = "#2a78d6", color = "white", alpha = 0.9) +
labs(title = "Figura 1. Distribución del precio de la vivienda en El Caney",
x = "Precio (millones de pesos)", y = "Número de ofertas") +
theme_minimal()
)
ggplotly(
ggplot(datos_sub, aes(x = Tipo, y = precio_millon, fill = Tipo)) +
geom_boxplot(alpha = 0.8, outlier.colour = "black") +
scale_fill_manual(values = c("Apartamento" = "#2a78d6", "Casa" = "#eb6834")) +
labs(title = "Figura 2. Precio según el tipo de vivienda",
x = "", y = "Precio (millones de pesos)") +
theme_minimal()
)
ggplotly(
ggplot(datos_sub, aes(x = Area_contruida, y = precio_millon, color = Tipo)) +
geom_point(alpha = 0.7, size = 2) +
geom_smooth(method = "lm", se = FALSE, linewidth = 0.8) +
scale_color_manual(values = c("Apartamento" = "#2a78d6", "Casa" = "#eb6834")) +
labs(title = paste("Figura 3. Precio vs. área construida | r =",
round(cor(datos_sub$precio_millon, datos_sub$Area_contruida), 2)),
x = "Área construida (m²)", y = "Precio (millones de pesos)") +
theme_minimal()
)
Lo que muestran los datos. El mercado de El Caney tiene dos caras. Los apartamentos son la mayoría (183 ofertas), pequeños (unos 70 m² en promedio) y con un precio mediano de 160 millones. Las casas son menos (116), casi tres veces más grandes, y su precio mediano sube a 335 millones.
El histograma tiene una cola larga a la derecha: la mitad de las ofertas cuesta menos de 230 millones, pero la cola se estira hasta 790 millones. Esa asimetría anticipa que el logaritmo del precio puede ser útil más adelante.
La Figura 3 muestra la relación más fuerte de la base. A mayor área, mayor precio, con una correlación de 0.82. Sin embargo, la nube se abre como un abanico a medida que crece el área, lo que es una primera señal de heterocedasticidad. Hay además un dato curioso: el metro cuadrado de casa sale más barato que el de apartamento (1.85 frente a 2.66 millones por m²). Las casas del barrio son grandes, y el mercado no paga cada metro adicional al mismo ritmo.
Cada modelo se va a evaluar con las mismas cuatro pruebas formales,
además de los cuatro gráficos de diagnóstico de plot().
Para no repetir código se definen tres funciones:
| Supuesto | Prueba | Hipótesis nula (H0) | Se cumple si |
|---|---|---|---|
| Normalidad de los residuos | Shapiro-Wilk | Los residuos son normales | p > 0.05 |
| Varianza constante | Breusch-Pagan | Homocedasticidad | p > 0.05 |
| Independencia | Durbin-Watson | No hay autocorrelación | p > 0.05 |
| Forma funcional | RESET de Ramsey | El modelo lineal está bien especificado | p > 0.05 |
Una advertencia sobre Durbin-Watson: esta prueba fue pensada para datos ordenados en el tiempo. Aquí las filas siguen el orden en que vienen en el archivo, que no tiene sentido temporal, así que su resultado depende de cómo esté ordenada la base. Un p-valor bajo indica que ofertas contiguas en el archivo se parecen entre sí (por ejemplo, porque se publicaron o cargaron juntas), no que exista una dinámica en el tiempo. Se reporta, pero con esa cautela.
# 0) Formato de números y p-valores (para las tablas y para el texto)
num <- function(x, d = 2) formatC(as.numeric(x), format = "f", digits = d)
pct <- function(b, d = 1) formatC((exp(as.numeric(b)) - 1) * 100, format = "f", digits = d)
sgn <- function(x, d = 3) ifelse(x < 0, paste("-", num(abs(x), d)), paste("+", num(x, d)))
pnum <- function(p) {
if (is.character(p)) return(p)
ifelse(p < 0.001, "< 0.001", formatC(as.numeric(p), format = "f", digits = 3))
}
pval <- function(p) { s <- pnum(p); ifelse(grepl("<", s), s, paste("=", s)) }
# 1) Las cuatro pruebas de supuestos en una sola tabla
validar_supuestos <- function(modelo) {
pruebas <- list(shapiro.test(residuals(modelo)),
bptest(modelo),
dwtest(modelo),
resettest(modelo, power = 2:3))
p <- sapply(pruebas, function(x) x$p.value)
est <- sapply(pruebas, function(x) unname(x$statistic))
data.frame(Supuesto = c("Normalidad de los residuos", "Varianza constante",
"Independencia de los errores", "Forma funcional lineal"),
Prueba = c("Shapiro-Wilk", "Breusch-Pagan", "Durbin-Watson", "RESET de Ramsey"),
Estadistico = round(est, 3),
p_valor = pnum(p),
Resultado = ifelse(p > 0.05, "Se cumple", "No se cumple"))
}
# 2) MAE en la escala original (millones), aunque el modelo esté en logaritmos.
# Así se pueden comparar modelos con y sin log en la misma unidad.
mae <- function(modelo) {
y_log <- grepl("^log\\(", deparse(formula(modelo)[[2]]))
real <- model.response(model.frame(modelo))
pred <- fitted(modelo)
if (y_log) { real <- exp(real); pred <- exp(pred) }
mean(abs(real - pred))
}
# 3) Tabla comparativa de varios modelos
comparar_modelos <- function(modelos) {
filas <- lapply(names(modelos), function(nombre) {
m <- modelos[[nombre]]
s <- validar_supuestos(m)
data.frame(Modelo = nombre,
R2_ajustado = round(summary(m)$adj.r.squared, 3),
MAE = round(mae(m), 1),
p_Shapiro = s$p_valor[1],
p_BreuschPagan = s$p_valor[2],
p_DurbinWatson = s$p_valor[3],
p_RESET = s$p_valor[4],
Cook_max = round(max(cooks.distance(m)), 3))
})
tabla <- do.call(rbind, filas)
rownames(tabla) <- NULL
tabla
}
# 4) Las cuatro especificaciones (con y sin logaritmo) de un modelo simple
comparar_log <- function(variable, datos) {
formulas <- c("Lin-Lin" = paste0("precio_millon ~ ", variable),
"Log-Lin" = paste0("log(precio_millon) ~ ", variable),
"Lin-Log" = paste0("precio_millon ~ log(", variable, ")"),
"Log-Log" = paste0("log(precio_millon) ~ log(", variable, ")"))
modelos <- lapply(formulas, function(f) lm(as.formula(f), data = datos))
comparar_modelos(modelos)
}
En las tablas comparativas, Lin-Lin es
precio ~ x, Log-Lin es
log(precio) ~ x, Lin-Log es
precio ~ log(x) y Log-Log es
log(precio) ~ log(x). El R² ajustado solo es comparable
entre modelos con la misma variable dependiente. Por eso el criterio
para comparar escalas distintas es el MAE en millones,
junto con el cumplimiento de los supuestos.
Se eligen los tres predictores con mayor correlación con el precio: el área construida (r = 0.82), el número de baños (r = 0.71) y el número de habitaciones (r = 0.69).
mod1 <- lm(precio_millon ~ Area_contruida, data = datos_sub)
summary(mod1)
##
## Call:
## lm(formula = precio_millon ~ Area_contruida, data = datos_sub)
##
## Residuals:
## Min 1Q Median 3Q Max
## -140.16 -38.69 -18.69 30.20 229.28
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 127.19777 5.89249 21.59 <2e-16 ***
## Area_contruida 1.02489 0.04129 24.82 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 58.52 on 297 degrees of freedom
## Multiple R-squared: 0.6747, Adjusted R-squared: 0.6736
## F-statistic: 616.1 on 1 and 297 DF, p-value: < 2.2e-16
b1 <- coef(mod1)
La ecuación estimada es precio = 127.20 + 1.025 × área.
En El Caney, cada metro cuadrado adicional se asocia, en promedio, con 1.02 millones más en el precio de oferta. Ampliar una vivienda en 10 m² equivale, según el modelo, a unos 10.2 millones. El intercepto (127.2 millones) no describe una vivienda real, porque nadie vende un inmueble de 0 m². Funciona más bien como el piso de precio que pone la ubicación antes de contar los metros.
Ambos coeficientes son altamente significativos (p < 0.001). El R² de 0.675 dice que el área sola explica cerca del 67 % de la variación del precio, una cifra alta para un solo predictor.
par(mfrow = c(2, 2))
plot(mod1)
par(mfrow = c(1, 1))
sup1 <- validar_supuestos(mod1)
sup1
| Supuesto | Prueba | Estadistico | p_valor | Resultado |
|---|---|---|---|---|
| Normalidad de los residuos | Shapiro-Wilk | 0.930 | < 0.001 | No se cumple |
| Varianza constante | Breusch-Pagan | 11.133 | < 0.001 | No se cumple |
| Independencia de los errores | Durbin-Watson | 1.517 | < 0.001 | No se cumple |
| Forma funcional lineal | RESET de Ramsey | 58.274 | < 0.001 | No se cumple |
Los gráficos confirman lo que insinuaba la nube de puntos. En Residuals vs Fitted, los residuos dibujan un arco: el modelo sobreestima las viviendas más pequeñas y las más grandes y subestima las intermedias. Una recta no describe bien la relación. Scale-Location tiene una línea roja que sube: la varianza no es constante, y Breusch-Pagan lo confirma (p < 0.001). El Q-Q plot se despega de la diagonal en la cola superior, por las casas caras que el modelo subestima, y Shapiro-Wilk rechaza la normalidad (p < 0.001). RESET (p < 0.001) avisa que una recta no es la mejor forma funcional. Ningún punto supera la distancia de Cook de 1 en Residuals vs Leverage, así que no hay una observación que por sí sola arrastre la recta.
cmp1 <- comparar_log("Area_contruida", datos_sub)
cmp1
| Modelo | R2_ajustado | MAE | p_Shapiro | p_BreuschPagan | p_DurbinWatson | p_RESET | Cook_max |
|---|---|---|---|---|---|---|---|
| Lin-Lin | 0.674 | 46.2 | < 0.001 | < 0.001 | < 0.001 | < 0.001 | 0.425 |
| Log-Lin | 0.601 | 53.3 | < 0.001 | 0.529 | < 0.001 | < 0.001 | 0.233 |
| Lin-Log | 0.754 | 34.4 | < 0.001 | < 0.001 | 0.021 | < 0.001 | 0.514 |
| Log-Log | 0.755 | 38.8 | < 0.001 | 0.462 | < 0.001 | < 0.001 | 0.045 |
La tabla da una lección clara. Pasar el precio a logaritmo (Log-Lin y Log-Log) corrige la heterocedasticidad: el p-valor de Breusch-Pagan pasa de < 0.001 a 0.462 en el Log-Log. Pasar el área a logaritmo (Lin-Log y Log-Log) mejora el ajuste, porque capta que el precio crece más despacio en las viviendas grandes. El Lin-Log tiene el MAE más bajo, pero conserva la varianza no constante. El Log-Log reúne lo mejor de ambos: el R² ajustado más alto (0.755), varianza constante y un MAE de 38.8 millones, frente a 46.2 del modelo lineal.
mod1_log <- lm(log(precio_millon) ~ log(Area_contruida), data = datos_sub)
summary(mod1_log)
##
## Call:
## lm(formula = log(precio_millon) ~ log(Area_contruida), data = datos_sub)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.65825 -0.12179 -0.03633 0.11585 0.77326
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 2.61150 0.09368 27.88 <2e-16 ***
## log(Area_contruida) 0.61514 0.02031 30.29 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.2002 on 297 degrees of freedom
## Multiple R-squared: 0.7554, Adjusted R-squared: 0.7546
## F-statistic: 917.5 on 1 and 297 DF, p-value: < 2.2e-16
par(mfrow = c(2, 2))
plot(mod1_log)
par(mfrow = c(1, 1))
sup1_log <- validar_supuestos(mod1_log)
sup1_log
| Supuesto | Prueba | Estadistico | p_valor | Resultado |
|---|---|---|---|---|
| Normalidad de los residuos | Shapiro-Wilk | 0.955 | < 0.001 | No se cumple |
| Varianza constante | Breusch-Pagan | 0.542 | 0.462 | Se cumple |
| Independencia de los errores | Durbin-Watson | 1.592 | < 0.001 | No se cumple |
| Forma funcional lineal | RESET de Ramsey | 63.575 | < 0.001 | No se cumple |
b1l <- coef(mod1_log)
Interpretación del modelo Log-Log. El coeficiente 0.615 es una elasticidad: si el área construida aumenta 1 %, el precio aumenta en promedio 0.62 %. Con un aumento del 10 % en el área, el precio sube cerca de 6.0 %. Como la elasticidad es menor que 1, el precio crece menos que proporcionalmente con el tamaño. Duplicar el área no duplica el precio: lo multiplica por 2^0.62 ≈ 1.53.
En los diagnósticos, el abanico desaparece y la línea de Scale-Location se aplana. Los residuos siguen sin ser perfectamente normales (Shapiro p < 0.001) y RESET aún detecta algo de curvatura. El arco que persiste en Residuals vs Fitted tiene una explicación: el barrio mezcla dos mercados, apartamentos y casas, que una sola variable no puede separar. Esa es la motivación de la dummy de la sección 3. Con 299 observaciones, el teorema del límite central respalda la inferencia sobre los coeficientes. Conclusión: la transformación logarítmica es pertinente y el Log-Log es el modelo simple preferido para el área.
mod2 <- lm(precio_millon ~ Banos, data = datos_sub)
summary(mod2)
##
## Call:
## lm(formula = precio_millon ~ Banos, data = datos_sub)
##
## Residuals:
## Min 1Q Median 3Q Max
## -124.50 -50.67 -25.89 37.72 296.90
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 51.455 11.294 4.556 7.63e-06 ***
## Banos 73.608 3.972 18.534 < 2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 69.87 on 297 degrees of freedom
## Multiple R-squared: 0.5363, Adjusted R-squared: 0.5347
## F-statistic: 343.5 on 1 and 297 DF, p-value: < 2.2e-16
b2 <- coef(mod2)
La ecuación es precio = 51.46 + 73.61 × baños. Cada baño adicional se asocia con 73.6 millones más en el precio. El efecto no es literal, porque instalar un baño no valoriza una casa en esa cifra. El número de baños funciona como un termómetro del tamaño y la categoría de la vivienda: un inmueble con cuatro baños suele ser una casa amplia, no un apartamento pequeño. El coeficiente es significativo (p < 0.001) y el modelo explica el 54 % de la variación del precio (R² = 0.536).
par(mfrow = c(2, 2))
plot(mod2)
par(mfrow = c(1, 1))
sup2 <- validar_supuestos(mod2)
sup2
| Supuesto | Prueba | Estadistico | p_valor | Resultado |
|---|---|---|---|---|
| Normalidad de los residuos | Shapiro-Wilk | 0.894 | < 0.001 | No se cumple |
| Varianza constante | Breusch-Pagan | 0.756 | 0.385 | Se cumple |
| Independencia de los errores | Durbin-Watson | 1.586 | < 0.001 | No se cumple |
| Forma funcional lineal | RESET de Ramsey | 0.966 | 0.382 | Se cumple |
Como los baños toman pocos valores (1, 2, 3…), los gráficos de residuos forman columnas verticales, algo esperable con un predictor discreto. Este modelo sale mejor librado que el del área: Breusch-Pagan no rechaza la varianza constante (p = 0.385) y RESET tampoco rechaza la forma lineal (p = 0.382). La normalidad sí falla (p < 0.001), por las mismas casas caras de la cola superior del Q-Q plot.
cmp2 <- comparar_log("Banos", datos_sub)
cmp2
| Modelo | R2_ajustado | MAE | p_Shapiro | p_BreuschPagan | p_DurbinWatson | p_RESET | Cook_max |
|---|---|---|---|---|---|---|---|
| Lin-Lin | 0.535 | 55.9 | < 0.001 | 0.385 | < 0.001 | 0.382 | 0.386 |
| Log-Lin | 0.500 | 58.1 | < 0.001 | 0.005 | < 0.001 | < 0.001 | 0.258 |
| Lin-Log | 0.524 | 55.7 | < 0.001 | 0.861 | < 0.001 | 0.010 | 0.334 |
| Log-Log | 0.518 | 54.4 | < 0.001 | < 0.001 | < 0.001 | 0.066 | 0.042 |
# Se estima la versión Log-Log para revisar sus supuestos
mod2_log <- lm(log(precio_millon) ~ log(Banos), data = datos_sub)
sup2_log <- validar_supuestos(mod2_log)
sup2_log
| Supuesto | Prueba | Estadistico | p_valor | Resultado |
|---|---|---|---|---|
| Normalidad de los residuos | Shapiro-Wilk | 0.951 | < 0.001 | No se cumple |
| Varianza constante | Breusch-Pagan | 19.655 | < 0.001 | No se cumple |
| Independencia de los errores | Durbin-Watson | 1.551 | < 0.001 | No se cumple |
| Forma funcional lineal | RESET de Ramsey | 2.745 | 0.066 | Se cumple |
Aquí el logaritmo no ayuda, y vale la pena notarlo porque no es una receta universal. El modelo lineal tiene el R² ajustado más alto (0.535) y cumple homocedasticidad y forma funcional. Las versiones con logaritmo en el precio rompen la varianza constante (Breusch-Pagan del Log-Log: p < 0.001), y el Log-Lin además empeora RESET. El Lin-Log conserva la varianza constante, pero rompe la forma funcional (RESET p = 0.010). La ganancia en MAE del Log-Log es mínima (54.4 frente a 55.9 millones) y no compensa perder un supuesto. Conclusión: para los baños se mantiene el modelo lineal sin transformar.
mod3 <- lm(precio_millon ~ Habitaciones, data = datos_sub)
summary(mod3)
##
## Call:
## lm(formula = precio_millon ~ Habitaciones, data = datos_sub)
##
## Residuals:
## Min 1Q Median 3Q Max
## -154.02 -47.90 -17.02 40.73 376.61
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 45.149 12.264 3.681 0.000275 ***
## Habitaciones 61.374 3.509 17.492 < 2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 72.01 on 297 degrees of freedom
## Multiple R-squared: 0.5074, Adjusted R-squared: 0.5058
## F-statistic: 306 on 1 and 297 DF, p-value: < 2.2e-16
b3 <- coef(mod3)
La ecuación es precio = 45.15 + 61.37 × habitaciones. Cada habitación adicional se asocia con 61.4 millones más. Igual que con los baños, las habitaciones resumen el tamaño general del inmueble. El efecto es significativo (p < 0.001) y el modelo explica el 51 % de la variación (R² = 0.507), el menor de los tres modelos simples.
par(mfrow = c(2, 2))
plot(mod3)
par(mfrow = c(1, 1))
sup3 <- validar_supuestos(mod3)
sup3
| Supuesto | Prueba | Estadistico | p_valor | Resultado |
|---|---|---|---|---|
| Normalidad de los residuos | Shapiro-Wilk | 0.939 | < 0.001 | No se cumple |
| Varianza constante | Breusch-Pagan | 13.028 | < 0.001 | No se cumple |
| Independencia de los errores | Durbin-Watson | 1.766 | 0.021 | No se cumple |
| Forma funcional lineal | RESET de Ramsey | 9.155 | < 0.001 | No se cumple |
Este modelo incumple casi todo. La varianza crece con el número de habitaciones (Breusch-Pagan p < 0.001), los residuos no son normales (p < 0.001) y RESET rechaza la forma lineal (p < 0.001). Pasar de 2 a 3 habitaciones no vale lo mismo que pasar de 6 a 7, y una recta no puede capturar ese rendimiento decreciente.
cmp3 <- comparar_log("Habitaciones", datos_sub)
cmp3
| Modelo | R2_ajustado | MAE | p_Shapiro | p_BreuschPagan | p_DurbinWatson | p_RESET | Cook_max |
|---|---|---|---|---|---|---|---|
| Lin-Lin | 0.506 | 55.0 | < 0.001 | < 0.001 | 0.021 | < 0.001 | 0.297 |
| Log-Lin | 0.504 | 59.6 | < 0.001 | 0.121 | < 0.001 | < 0.001 | 0.134 |
| Lin-Log | 0.532 | 51.1 | < 0.001 | 0.002 | 0.073 | 0.277 | 0.272 |
| Log-Log | 0.559 | 53.0 | 0.003 | 0.315 | 0.016 | 0.023 | 0.049 |
mod3_log <- lm(log(precio_millon) ~ log(Habitaciones), data = datos_sub)
summary(mod3_log)
##
## Call:
## lm(formula = log(precio_millon) ~ log(Habitaciones), data = datos_sub)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.57451 -0.17588 -0.01412 0.17720 0.71366
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 4.39082 0.05544 79.20 <2e-16 ***
## log(Habitaciones) 0.91457 0.04697 19.47 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.2683 on 297 degrees of freedom
## Multiple R-squared: 0.5607, Adjusted R-squared: 0.5593
## F-statistic: 379.1 on 1 and 297 DF, p-value: < 2.2e-16
par(mfrow = c(2, 2))
plot(mod3_log)
par(mfrow = c(1, 1))
sup3_log <- validar_supuestos(mod3_log)
sup3_log
| Supuesto | Prueba | Estadistico | p_valor | Resultado |
|---|---|---|---|---|
| Normalidad de los residuos | Shapiro-Wilk | 0.985 | 0.003 | No se cumple |
| Varianza constante | Breusch-Pagan | 1.009 | 0.315 | Se cumple |
| Independencia de los errores | Durbin-Watson | 1.754 | 0.016 | No se cumple |
| Forma funcional lineal | RESET de Ramsey | 3.837 | 0.023 | No se cumple |
b3l <- coef(mod3_log)
El Log-Log es la mejor especificación. Tiene el R² ajustado más alto (0.559 frente a 0.506), recupera la varianza constante (Breusch-Pagan p = 0.315) y acerca mucho más los residuos a la normalidad (Shapiro p = 0.003, frente a < 0.001 en el lineal). RESET todavía marca algo de curvatura (p = 0.023), aunque bastante menos que en la versión lineal.
Interpretación del modelo Log-Log. La elasticidad es 0.915: un 1 % más de habitaciones se asocia con un 0.91 % más de precio. Como las habitaciones son enteras, es más útil traducirlo a un caso concreto. Pasar de 3 a 4 habitaciones (un aumento del 33 %) eleva el precio esperado en torno a 30.1 %. Pasar de 6 a 7 (un aumento del 17 %) lo eleva solo un 15.1 %. Ese es el rendimiento decreciente que la recta no podía ver. Conclusión: la transformación logarítmica es pertinente.
data.frame(
Modelo = c("1. Área construida", "2. Baños", "3. Habitaciones"),
Especificacion = c("Log-Log", "Lin-Lin", "Log-Log"),
R2_ajustado = round(c(summary(mod1_log)$adj.r.squared, summary(mod2)$adj.r.squared,
summary(mod3_log)$adj.r.squared), 3),
MAE_millones = round(c(mae(mod1_log), mae(mod2), mae(mod3_log)), 1)
)
| Modelo | Especificacion | R2_ajustado | MAE_millones |
|---|---|---|---|
| 1. Área construida | Log-Log | 0.755 | 38.8 |
| 2. Baños | Lin-Lin | 0.535 | 55.9 |
| 3. Habitaciones | Log-Log | 0.559 | 53.0 |
El área construida es, por mucho, el mejor predictor individual del precio en El Caney: explica más varianza y se equivoca menos. Los tres modelos simples dejan una lección de método. El logaritmo fue la solución para el área y las habitaciones, que tienen rendimientos decrecientes y varianza creciente, pero empeoró el modelo de los baños, donde la relación ya era lineal. La transformación se valida con los datos; no se aplica por costumbre.
Para que los modelos múltiples y la selección paso a paso sean
comparables, todos deben estimarse sobre las mismas
filas. Se construye una base con las variables candidatas y se
eliminan los registros incompletos (los faltantes vienen casi todos de
parqueaderos):
datos_mod <- datos_sub %>%
select(precio_millon, Area_contruida, Habitaciones, Banos, parqueaderos, Estrato, Tipo) %>%
na.omit()
nrow(datos_mod)
## [1] 236
table(datos_mod$Tipo)
##
## Apartamento Casa
## 130 106
Se trabaja con 236 viviendas completas.
Este primer modelo usa solo variables cuantitativas que describen el inmueble: área, habitaciones, baños y parqueaderos.
mm1 <- lm(precio_millon ~ Area_contruida + Habitaciones + Banos + parqueaderos, data = datos_mod)
summary(mm1)
##
## Call:
## lm(formula = precio_millon ~ Area_contruida + Habitaciones +
## Banos + parqueaderos, data = datos_mod)
##
## Residuals:
## Min 1Q Median 3Q Max
## -133.22 -37.26 -13.15 33.95 229.26
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 69.98498 13.30580 5.260 3.29e-07 ***
## Area_contruida 0.66957 0.06663 10.049 < 2e-16 ***
## Habitaciones 14.88324 4.78087 3.113 0.00209 **
## Banos 16.76691 5.59143 2.999 0.00301 **
## parqueaderos 8.81134 8.29081 1.063 0.28899
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 55 on 231 degrees of freedom
## Multiple R-squared: 0.7233, Adjusted R-squared: 0.7185
## F-statistic: 151 on 4 and 231 DF, p-value: < 2.2e-16
c1 <- coef(mm1)
p1 <- summary(mm1)$coefficients[, 4]
En un modelo múltiple, cada coeficiente se lee manteniendo constantes las demás variables:
El modelo explica el 71.9 % de la variación del precio (R² ajustado), más que cualquiera de los modelos simples lineales.
par(mfrow = c(2, 2))
plot(mm1)
par(mfrow = c(1, 1))
sup_mm1 <- validar_supuestos(mm1)
sup_mm1
| Supuesto | Prueba | Estadistico | p_valor | Resultado |
|---|---|---|---|---|
| Normalidad de los residuos | Shapiro-Wilk | 0.944 | < 0.001 | No se cumple |
| Varianza constante | Breusch-Pagan | 16.574 | 0.002 | No se cumple |
| Independencia de los errores | Durbin-Watson | 1.674 | 0.006 | No se cumple |
| Forma funcional lineal | RESET de Ramsey | 26.809 | < 0.001 | No se cumple |
# Multicolinealidad: un VIF mayor a 5 sería preocupante
vif(mm1)
## Area_contruida Habitaciones Banos parqueaderos
## 2.589264 2.521327 2.787338 1.471493
El VIF de todas las variables está por debajo de 5, así que, aunque área, baños y habitaciones están correlacionadas, la multicolinealidad no infla los errores estándar de manera grave. Los supuestos sobre el error, en cambio, fallan: hay heterocedasticidad (Breusch-Pagan p = 0.002), no normalidad (p < 0.001) y problemas de forma funcional (RESET p < 0.001). El patrón es el mismo del modelo simple del área.
Siguiendo lo aprendido en la modelación simple, se toma el logaritmo del precio y del área. Habitaciones, baños y parqueaderos se dejan en niveles, porque son conteos pequeños.
mm1_log <- lm(log(precio_millon) ~ log(Area_contruida) + Habitaciones + Banos + parqueaderos,
data = datos_mod)
summary(mm1_log)
##
## Call:
## lm(formula = log(precio_millon) ~ log(Area_contruida) + Habitaciones +
## Banos + parqueaderos, data = datos_mod)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.57829 -0.13154 -0.04265 0.11960 0.63988
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 2.987844 0.123767 24.141 <2e-16 ***
## log(Area_contruida) 0.487720 0.035969 13.559 <2e-16 ***
## Habitaciones 0.039083 0.017419 2.244 0.0258 *
## Banos 0.033818 0.019793 1.709 0.0889 .
## parqueaderos 0.005475 0.028758 0.190 0.8492
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.1958 on 231 degrees of freedom
## Multiple R-squared: 0.7612, Adjusted R-squared: 0.7571
## F-statistic: 184.1 on 4 and 231 DF, p-value: < 2.2e-16
par(mfrow = c(2, 2))
plot(mm1_log)
par(mfrow = c(1, 1))
validar_supuestos(mm1_log)
| Supuesto | Prueba | Estadistico | p_valor | Resultado |
|---|---|---|---|---|
| Normalidad de los residuos | Shapiro-Wilk | 0.971 | < 0.001 | No se cumple |
| Varianza constante | Breusch-Pagan | 2.576 | 0.631 | Se cumple |
| Independencia de los errores | Durbin-Watson | 1.677 | 0.006 | No se cumple |
| Forma funcional lineal | RESET de Ramsey | 55.886 | < 0.001 | No se cumple |
comparar_modelos(list("MM1 lineal" = mm1, "MM1 log" = mm1_log))
| Modelo | R2_ajustado | MAE | p_Shapiro | p_BreuschPagan | p_DurbinWatson | p_RESET | Cook_max |
|---|---|---|---|---|---|---|---|
| MM1 lineal | 0.719 | 42.9 | < 0.001 | 0.002 | 0.006 | < 0.001 | 0.266 |
| MM1 log | 0.757 | 41.1 | < 0.001 | 0.631 | 0.006 | < 0.001 | 0.162 |
c1l <- coef(mm1_log)
p1l <- summary(mm1_log)$coefficients[, 4]
La versión logarítmica gana en todos los frentes. Sube el R² ajustado, baja el MAE y la heterocedasticidad desaparece (Breusch-Pagan pasa de 0.002 a 0.631). La distancia de Cook máxima también baja, señal de que las casas grandes pesan menos sobre la estimación.
Interpretación (versión log). Con habitaciones, baños y parqueaderos constantes, un 10 % más de área se asocia con un 4.8 % más de precio. Cada habitación adicional suma un 4.0 % al precio (p = 0.026). El baño adicional (3.4 %, p = 0.089) y los parqueaderos (p = 0.849) pierden significancia al 5 %: en logaritmos, el área absorbe casi toda la información de tamaño.
El segundo modelo cambia de enfoque. Además del tamaño (área, baños y parqueaderos), incorpora qué se vende y dónde en términos socioeconómicos:
TipoCasa = 1 si el inmueble es
casa y 0 si es apartamento. La categoría base es
Apartamento.# Codificación de la dummy: Apartamento es la base (fila de ceros)
contrasts(datos_mod$Tipo)
## Casa
## Apartamento 0
## Casa 1
mm2 <- lm(precio_millon ~ Area_contruida + Banos + parqueaderos + Tipo + Estrato, data = datos_mod)
summary(mm2)
##
## Call:
## lm(formula = precio_millon ~ Area_contruida + Banos + parqueaderos +
## Tipo + Estrato, data = datos_mod)
##
## Residuals:
## Min 1Q Median 3Q Max
## -114.50 -34.56 -12.38 33.85 263.39
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -61.60094 32.93421 -1.870 0.062695 .
## Area_contruida 0.62564 0.06791 9.213 < 2e-16 ***
## Banos 16.33529 4.93152 3.312 0.001074 **
## parqueaderos 13.93999 7.95923 1.751 0.081206 .
## TipoCasa 37.83930 10.48183 3.610 0.000376 ***
## Estrato 39.95840 7.80675 5.118 6.5e-07 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 52.58 on 230 degrees of freedom
## Multiple R-squared: 0.7482, Adjusted R-squared: 0.7427
## F-statistic: 136.7 on 5 and 230 DF, p-value: < 2.2e-16
c2 <- coef(mm2)
p2 <- summary(mm2)$coefficients[, 4]
El modelo explica el 74.3 % de la variación del precio (R² ajustado), más que el modelo múltiple 1, aunque no incluye las habitaciones. El tipo de inmueble y el estrato aportan información que el tamaño no contiene.
par(mfrow = c(2, 2))
plot(mm2)
par(mfrow = c(1, 1))
sup_mm2 <- validar_supuestos(mm2)
sup_mm2
| Supuesto | Prueba | Estadistico | p_valor | Resultado |
|---|---|---|---|---|
| Normalidad de los residuos | Shapiro-Wilk | 0.937 | < 0.001 | No se cumple |
| Varianza constante | Breusch-Pagan | 11.346 | 0.045 | No se cumple |
| Independencia de los errores | Durbin-Watson | 1.793 | 0.053 | Se cumple |
| Forma funcional lineal | RESET de Ramsey | 18.027 | < 0.001 | No se cumple |
vif(mm2)
## Area_contruida Banos parqueaderos Tipo Estrato
## 2.942175 2.371985 1.483589 2.320447 1.069346
La multicolinealidad es baja (VIF < 5). La heterocedasticidad es más leve que en el modelo 1, pero todavía rechaza al 5 % (Breusch-Pagan p = 0.045). La normalidad y la forma funcional siguen fallando.
mm2_log <- lm(log(precio_millon) ~ log(Area_contruida) + Banos + parqueaderos + Tipo + Estrato,
data = datos_mod)
summary(mm2_log)
##
## Call:
## lm(formula = log(precio_millon) ~ log(Area_contruida) + Banos +
## parqueaderos + Tipo + Estrato, data = datos_mod)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.5147 -0.1304 -0.0244 0.1250 0.6293
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 2.54680 0.16937 15.036 < 2e-16 ***
## log(Area_contruida) 0.45759 0.04071 11.241 < 2e-16 ***
## Banos 0.04058 0.01716 2.365 0.0189 *
## parqueaderos 0.02769 0.02788 0.993 0.3216
## TipoCasa 0.07505 0.04229 1.774 0.0773 .
## Estrato 0.15297 0.02847 5.374 1.89e-07 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.187 on 230 degrees of freedom
## Multiple R-squared: 0.7833, Adjusted R-squared: 0.7786
## F-statistic: 166.3 on 5 and 230 DF, p-value: < 2.2e-16
par(mfrow = c(2, 2))
plot(mm2_log)
par(mfrow = c(1, 1))
validar_supuestos(mm2_log)
| Supuesto | Prueba | Estadistico | p_valor | Resultado |
|---|---|---|---|---|
| Normalidad de los residuos | Shapiro-Wilk | 0.976 | < 0.001 | No se cumple |
| Varianza constante | Breusch-Pagan | 2.369 | 0.796 | Se cumple |
| Independencia de los errores | Durbin-Watson | 1.839 | 0.102 | Se cumple |
| Forma funcional lineal | RESET de Ramsey | 37.118 | < 0.001 | No se cumple |
comparar_modelos(list("MM2 lineal" = mm2, "MM2 log" = mm2_log))
| Modelo | R2_ajustado | MAE | p_Shapiro | p_BreuschPagan | p_DurbinWatson | p_RESET | Cook_max |
|---|---|---|---|---|---|---|---|
| MM2 lineal | 0.743 | 40.4 | < 0.001 | 0.045 | 0.053 | < 0.001 | 0.434 |
| MM2 log | 0.779 | 38.7 | < 0.001 | 0.796 | 0.102 | < 0.001 | 0.171 |
c2l <- coef(mm2_log)
p2l <- summary(mm2_log)$coefficients[, 4]
Otra vez la versión en logaritmos es superior. Tiene un R² ajustado de 0.779, el MAE más bajo hasta ahora (38.7 millones) y varianza constante (Breusch-Pagan p = 0.796).
Interpretación (versión log). En un modelo con el precio en logaritmo, el coeficiente de una dummy se traduce en porcentaje con (e^b − 1) × 100:
Los dos modelos anteriores se armaron con criterio propio. La selección paso a paso deja que los datos decidan. Se parte del modelo completo (todas las variables candidatas, en la especificación logarítmica validada) y en cada paso se quita o se agrega la variable que más reduce el AIC (criterio de información de Akaike). El proceso se detiene cuando ningún cambio mejora el AIC.
modelo_completo <- lm(log(precio_millon) ~ log(Area_contruida) + Habitaciones + Banos +
parqueaderos + Tipo + Estrato, data = datos_mod)
modelo_step <- step(modelo_completo, direction = "both", trace = 1)
## Start: AIC=-789.53
## log(precio_millon) ~ log(Area_contruida) + Habitaciones + Banos +
## parqueaderos + Tipo + Estrato
##
## Df Sum of Sq RSS AIC
## - parqueaderos 1 0.0284 7.8668 -790.68
## - Banos 1 0.0332 7.8716 -790.53
## <none> 7.8384 -789.53
## - Tipo 1 0.0746 7.9131 -789.30
## - Habitaciones 1 0.2020 8.0404 -785.53
## - Estrato 1 1.0207 8.8592 -762.64
## - log(Area_contruida) 1 3.8858 11.7242 -696.51
##
## Step: AIC=-790.68
## log(precio_millon) ~ log(Area_contruida) + Habitaciones + Banos +
## Tipo + Estrato
##
## Df Sum of Sq RSS AIC
## - Banos 1 0.0374 7.9042 -791.56
## - Tipo 1 0.0627 7.9295 -790.80
## <none> 7.8668 -790.68
## + parqueaderos 1 0.0284 7.8384 -789.53
## - Habitaciones 1 0.2081 8.0749 -786.52
## - Estrato 1 0.9937 8.8605 -764.61
## - log(Area_contruida) 1 4.8156 12.6824 -679.97
##
## Step: AIC=-791.56
## log(precio_millon) ~ log(Area_contruida) + Habitaciones + Tipo +
## Estrato
##
## Df Sum of Sq RSS AIC
## <none> 7.9042 -791.56
## - Tipo 1 0.0866 7.9908 -790.99
## + Banos 1 0.0374 7.8668 -790.68
## + parqueaderos 1 0.0326 7.8716 -790.53
## - Habitaciones 1 0.3837 8.2880 -782.37
## - Estrato 1 1.0672 8.9715 -763.67
## - log(Area_contruida) 1 5.2437 13.1480 -673.47
# Resumen de los pasos realizados
modelo_step$anova
| Step | Df | Deviance | Resid. Df | Resid. Dev | AIC |
|---|---|---|---|---|---|
| NA | NA | 229 | 7.838436 | -789.5310 | |
| - parqueaderos | 1 | 0.0283702 | 230 | 7.866806 | -790.6784 |
| - Banos | 1 | 0.0374345 | 231 | 7.904241 | -791.5580 |
# Comprobación: la selección hacia adelante, partiendo del modelo vacío
modelo_nulo <- lm(log(precio_millon) ~ 1, data = datos_mod)
modelo_fwd <- step(modelo_nulo, scope = formula(modelo_completo), direction = "forward", trace = 0)
formula(modelo_fwd)
## log(precio_millon) ~ log(Area_contruida) + Estrato + Habitaciones +
## Tipo
summary(modelo_step)
##
## Call:
## lm(formula = log(precio_millon) ~ log(Area_contruida) + Habitaciones +
## Tipo + Estrato, data = datos_mod)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.53771 -0.13173 -0.02293 0.11974 0.65873
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 2.51096 0.16293 15.412 < 2e-16 ***
## log(Area_contruida) 0.46045 0.03720 12.379 < 2e-16 ***
## Habitaciones 0.04906 0.01465 3.349 0.000948 ***
## TipoCasa 0.06500 0.04087 1.590 0.113093
## Estrato 0.15446 0.02766 5.585 6.55e-08 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.185 on 231 degrees of freedom
## Multiple R-squared: 0.787, Adjusted R-squared: 0.7833
## F-statistic: 213.3 on 4 and 231 DF, p-value: < 2.2e-16
cs <- coef(modelo_step)
ps <- summary(modelo_step)$coefficients[, 4]
Qué decidió el algoritmo. En el primer paso salió parqueaderos, la variable que menos aportaba (ya era no significativa en todos los modelos). En el segundo salió baños, cuya información queda cubierta por el área y las habitaciones. El AIC bajó de -789.5 a -791.6. La selección hacia adelante, que parte del modelo vacío, llega al mismo conjunto de variables, lo que da confianza en el resultado.
El modelo final es:
log(precio) = 2.511 + 0.460 · log(área) + 0.049 · habitaciones + 0.065 · Casa + 0.154 · estrato
par(mfrow = c(2, 2))
plot(modelo_step)
par(mfrow = c(1, 1))
sup_step <- validar_supuestos(modelo_step)
sup_step
| Supuesto | Prueba | Estadistico | p_valor | Resultado |
|---|---|---|---|---|
| Normalidad de los residuos | Shapiro-Wilk | 0.976 | < 0.001 | No se cumple |
| Varianza constante | Breusch-Pagan | 2.797 | 0.592 | Se cumple |
| Independencia de los errores | Durbin-Watson | 1.831 | 0.092 | Se cumple |
| Forma funcional lineal | RESET de Ramsey | 27.936 | < 0.001 | No se cumple |
vif(modelo_step)
## log(Area_contruida) Habitaciones Tipo Estrato
## 3.194630 2.092598 2.849919 1.084467
# Errores estándar robustos (HC3): una red de seguridad ante la falta de normalidad
coeftest(modelo_step, vcov. = hccm(modelo_step, type = "hc3"))
##
## t test of coefficients:
##
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 2.510964 0.192059 13.0739 < 2.2e-16 ***
## log(Area_contruida) 0.460453 0.051395 8.9591 < 2.2e-16 ***
## Habitaciones 0.049059 0.017899 2.7409 0.006607 **
## TipoCasa 0.064998 0.052301 1.2428 0.215210
## Estrato 0.154465 0.029812 5.1812 4.801e-07 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
El modelo seleccionado mantiene la varianza constante (Breusch-Pagan p = 0.592) y un VIF bajo. La normalidad (p < 0.001) y la forma funcional (RESET p < 0.001) siguen sin cumplirse del todo, una limitación común en datos de precios. Por eso se recalculan los errores estándar de forma robusta (HC3). Las conclusiones no cambian: área, habitaciones y estrato siguen siendo significativos, y la dummy Casa sigue sin serlo.
¿Cuánto debería pedirse por dos viviendas típicas de El Caney? Se usa
el modelo seleccionado con predict(). Como el modelo está
en logaritmos, la predicción se devuelve a millones con
exp(). Así se obtiene el precio mediano
esperado, que es justo la cantidad que minimiza el error absoluto
(MAE).
nuevas <- data.frame(
Tipo = factor(c("Apartamento", "Casa"), levels = levels(datos_mod$Tipo)),
Area_contruida = c(80, 160),
Habitaciones = c(3, 4),
Banos = c(2, 3),
parqueaderos = c(1, 2),
Estrato = c(4, 4))
pred_conf <- exp(predict(modelo_step, newdata = nuevas, interval = "confidence", level = 0.95))
pred_pred <- exp(predict(modelo_step, newdata = nuevas, interval = "prediction", level = 0.95))
data.frame(nuevas,
Precio_estimado = round(pred_pred[, "fit"], 1),
IC95_media_inf = round(pred_conf[, "lwr"], 1),
IC95_media_sup = round(pred_conf[, "upr"], 1),
IP95_inf = round(pred_pred[, "lwr"], 1),
IP95_sup = round(pred_pred[, "upr"], 1))
| Tipo | Area_contruida | Habitaciones | Banos | parqueaderos | Estrato | Precio_estimado | IC95_media_inf | IC95_media_sup | IP95_inf | IP95_sup |
|---|---|---|---|---|---|---|---|---|---|---|
| Apartamento | 80 | 3 | 2 | 1 | 4 | 199.1 | 192.2 | 206.2 | 138.0 | 287.1 |
| Casa | 160 | 4 | 3 | 2 | 4 | 307.0 | 296.0 | 318.5 | 212.9 | 442.9 |
Interpretación. Para un apartamento de 80 m², 3 habitaciones, estrato 4, el modelo estima un precio de 199 millones. Para una casa de 160 m², 4 habitaciones, estrato 4, la estimación sube a 307 millones.
Los dos intervalos responden preguntas distintas. El intervalo de confianza (192 a 206 millones para el apartamento) es estrecho porque estima el precio promedio de todos los apartamentos con esas características. El intervalo de predicción (138 a 287 millones) es mucho más ancho porque se refiere a una oferta concreta, que además depende de los acabados, la vista, el conjunto y la urgencia del vendedor. Un aviso que pida más de 287 millones por ese apartamento estaría por encima de lo que sugiere el mercado del barrio.
Baños y parqueaderos se incluyen en nuevas por
completitud, pero el modelo seleccionado no los usa.
El MAE (error absoluto medio) mide, en millones de pesos, cuánto se equivoca el modelo en promedio. Se calcula de dos formas: sobre los mismos datos con que se estimó cada modelo (dentro de muestra), y con validación cruzada de 10 pliegues (fuera de muestra). En la validación cruzada, la base se divide en 10 partes. Cada parte se usa una vez como prueba mientras el modelo se estima con las otras nueve, de modo que cada vivienda se predice con un modelo que nunca la vio. Es la prueba honesta, y es más estable que un único corte entre entrenamiento y prueba.
# Referencia: el mejor modelo simple, reestimado en la misma base
ref_simple <- lm(log(precio_millon) ~ log(Area_contruida), data = datos_mod)
modelos_finales <- list("Simple Log-Log (área)" = ref_simple,
"MM1 lineal" = mm1,
"MM1 log" = mm1_log,
"MM2 lineal (dummy)" = mm2,
"MM2 log (dummy)" = mm2_log,
"Stepwise (log)" = modelo_step)
# MAE fuera de muestra con validación cruzada de 10 pliegues
set.seed(123)
k <- 10
pliegues <- sample(rep(1:k, length.out = nrow(datos_mod)))
mae_cv <- function(modelo) {
errores <- c()
for (i in 1:k) {
entrenamiento <- datos_mod[pliegues != i, ]
prueba <- datos_mod[pliegues == i, ]
m <- lm(formula(modelo), data = entrenamiento)
pred <- predict(m, newdata = prueba)
if (grepl("^log\\(", deparse(formula(modelo)[[2]]))) pred <- exp(pred)
errores <- c(errores, abs(prueba$precio_millon - pred))
}
mean(errores)
}
tabla_mae <- data.frame(
Modelo = names(modelos_finales),
R2_ajustado = sapply(modelos_finales, function(m) round(summary(m)$adj.r.squared, 3)),
MAE_dentro = sapply(modelos_finales, function(m) round(mae(m), 1)),
MAE_val_cruzada = sapply(modelos_finales, function(m) round(mae_cv(m), 1)))
tabla_mae$MAE_relativo_pct <- round(100 * sapply(modelos_finales, mae) / mean(datos_mod$precio_millon), 1)
rownames(tabla_mae) <- NULL
tabla_mae
| Modelo | R2_ajustado | MAE_dentro | MAE_val_cruzada | MAE_relativo_pct |
|---|---|---|---|---|
| Simple Log-Log (área) | 0.744 | 40.9 | 41.2 | 15.5 |
| MM1 lineal | 0.719 | 42.9 | 44.0 | 16.2 |
| MM1 log | 0.757 | 41.1 | 42.3 | 15.6 |
| MM2 lineal (dummy) | 0.743 | 40.4 | 41.8 | 15.3 |
| MM2 log (dummy) | 0.779 | 38.7 | 40.3 | 14.6 |
| Stepwise (log) | 0.783 | 38.9 | 39.8 | 14.7 |
ggplotly(
ggplot(data.frame(real = datos_mod$precio_millon, estimado = exp(fitted(modelo_step)),
Tipo = datos_mod$Tipo),
aes(x = estimado, y = real, color = Tipo)) +
geom_point(alpha = 0.7, size = 2) +
geom_abline(slope = 1, intercept = 0, linetype = "dashed", color = "grey40") +
scale_color_manual(values = c("Apartamento" = "#2a78d6", "Casa" = "#eb6834")) +
labs(title = "Figura 4. Precio real vs. estimado (modelo stepwise)",
x = "Precio estimado (millones)", y = "Precio real (millones)") +
theme_minimal()
)
Interpretación. El modelo stepwise se equivoca en promedio 38.9 millones por vivienda, alrededor del 14.7 % del precio medio del barrio (264.1 millones). El mejor modelo simple fallaba por 40.9 millones, así que añadir habitaciones, tipo y estrato recorta el error en 2.0 millones por vivienda. La mejora es modesta porque el área ya hacía casi todo el trabajo, pero el R² ajustado sube de 0.744 a 0.783 y el modelo gana en interpretación.
En validación cruzada, el MAE del modelo stepwise es de 39.8 millones, apenas por encima del valor dentro de muestra, así que el modelo no está sobreajustado. Además, es el más bajo de la tabla: fuera de muestra, el stepwise es el que mejor predice. Los modelos con dummy (MM2 y stepwise) le ganan al MM1, sobre todo fuera de muestra: saber si es casa o apartamento y en qué estrato está ayuda a predecir mejor que sumar más medidas de tamaño. Entre el MM2 log y el stepwise la diferencia de MAE es mínima, pero el stepwise usa una variable menos, y ante dos modelos casi igual de precisos conviene el más simple.
En la Figura 4, los puntos se alinean alrededor de la diagonal y se abren en los precios altos. Las casas más caras son las más difíciles de predecir.
Tipo:Area) o variables ausentes como la antigüedad, los
acabados o el conjunto cerrado. Los errores robustos protegen las
conclusiones actuales; incorporar esas variables o interacciones es el
siguiente paso para mejorar el modelo.