La Flora es uno de los barrios tradicionales del norte de Cali. Es estrato 5 en su gran mayoría, y en sus calles conviven edificios de apartamentos con casas amplias de urbanización, muchas de ellas con décadas encima. Esa convivencia de dos mercados, el del apartamento en edificio y el de la casa grande, es lo que hace interesante modelar sus precios: no siempre lo más grande es lo más caro por metro.
En este informe se filtra la base Datos_Vivienda (8.322 ofertas de vivienda usada en Cali) por el barrio La Flora, 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"))
# 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, pegado, en mayúsculas o con el prefijo “Urbanización”. Antes de filtrar conviene ver todas las grafías que contienen la palabra flora:
# Versión estandarizada del nombre: minúsculas y sin espacios sobrantes
datos$barrio_std <- tolower(trimws(datos$Barrio))
# Todas las formas en que aparece "flora" en la base
table(datos$Barrio[grepl("flora", datos$barrio_std)])
##
## flora flora industrial floralia
## 1 16 6
## la flora La Flora laflora
## 366 2 1
## norte la flora urbanizacion la flora
## 1 83
La tabla mezcla dos situaciones:
También conviene revisar la zona asignada a estos registros:
es_flora <- grepl("flora", datos$barrio_std) & !grepl("industrial|floralia", datos$barrio_std)
table(datos$Zona[es_flora])
##
## Zona Norte Zona Sur
## 453 1
La Flora está en el norte de la ciudad, así que el único registro marcado como “Zona Sur” es un error de digitación y se descarta.
datos_sub <- datos %>%
filter(grepl("flora", barrio_std),
!grepl("industrial|floralia", barrio_std),
Zona == "Zona Norte") %>%
mutate(Barrio = "La Flora",
Estrato = as.numeric(Estrato), # viene como texto desde el Excel
Tipo = factor(Tipo, levels = c("Apartamento", "Casa")))
nrow(datos_sub)
## [1] 453
table(datos_sub$Tipo)
##
## Apartamento Casa
## 330 123
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 |
|---|---|---|---|---|---|
| 2295 | Casa | 334 | 243 | 0 | 0 |
| 2643 | Apartamento | 420 | 104 | 0 | 2 |
| 2688 | Apartamento | 320 | 85 | 0 | 2 |
| 2905 | Casa | 530 | 455 | 4 | 0 |
| 3893 | Casa | 470 | 336 | 0 | 0 |
datos_sub <- datos_sub %>% filter(Habitaciones > 0, Banos > 0)
nrow(datos_sub)
## [1] 448
# 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 17
## Estrato Tipo piso
## 0 0 0
Quedan 448 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 (17
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 La Flora")
rbind(as.data.frame(resumen), as.data.frame(total))
| Tipo | ofertas | precio_promedio | precio_mediano | area_promedio | habitaciones | banos | precio_m2 |
|---|---|---|---|---|---|---|---|
| Apartamento | 328 | 334.9 | 320.0 | 99.7 | 2.9 | 2.5 | 3.48 |
| Casa | 120 | 511.0 | 482.5 | 310.9 | 4.5 | 3.9 | 1.72 |
| Total La Flora | 448 | 382.1 | 353.5 | 156.3 | 3.4 | 2.9 | 3.20 |
# Distribución del estrato
table(datos_sub$Estrato)
##
## 3 4 5 6
## 1 37 404 6
# 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.77 0.54 0.67 0.46
## Area_contruida 0.77 1.00 0.68 0.65 0.47
## Habitaciones 0.54 0.68 1.00 0.65 0.39
## Banos 0.67 0.65 0.65 1.00 0.55
## parqueaderos 0.46 0.47 0.39 0.55 1.00
## Estrato 0.03 -0.03 -0.05 0.00 0.00
## Estrato
## precio_millon 0.03
## Area_contruida -0.03
## Habitaciones -0.05
## Banos 0.00
## parqueaderos 0.00
## Estrato 1.00
ggplotly(
ggplot(datos_sub, aes(x = precio_millon)) +
geom_histogram(binwidth = 50, fill = "#2a78d6", color = "white", alpha = 0.9) +
labs(title = "Figura 1. Distribución del precio de la vivienda en La Flora",
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. En La Flora predominan los apartamentos (328 de 448 ofertas), con unos 100 m² en promedio y un precio mediano de 320 millones. Las 120 casas son otra cosa: promedian 311 m², el triple, pero su precio mediano (482.5 millones) apenas es una vez y media el de un apartamento. El metro cuadrado de casa cuesta la mitad que el de apartamento (1.72 frente a 3.48 millones por m²). Esa es la tensión central de este barrio, y los modelos tendrán que lidiar con ella.
El histograma se concentra entre 250 y 450 millones (precio mediano de 353.5), pero tiene una cola larga a la derecha: 20 ofertas superan los 600 millones y la más cara llega a 1400. El estrato casi no varía (90 % de las ofertas son estrato 5), así que su correlación con el precio es prácticamente nula (0.03).
En la Figura 3, el área tiene la relación más fuerte con el precio (r = 0.77). Las dos rectas cuentan la historia: la de los apartamentos sube con pendiente pronunciada y la de las casas, con cerca de la mitad de inclinación, es mucho más plana. La nube se abre como un abanico hacia las áreas grandes, una primera señal de heterocedasticidad.
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 unas funciones de apoyo:
| 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. La columna
Cook_max muestra la mayor distancia de Cook del modelo: un
valor cercano o superior a 1 indica que una sola observación está
moviendo la estimación.
Se eligen los tres predictores con mayor correlación con el precio: el área construida (r = 0.77), el número de baños (r = 0.67) y el número de habitaciones (r = 0.54).
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
## -285.51 -49.96 -8.95 54.23 491.18
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 245.68338 6.73725 36.47 <2e-16 ***
## Area_contruida 0.87290 0.03406 25.63 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 87.45 on 446 degrees of freedom
## Multiple R-squared: 0.5956, Adjusted R-squared: 0.5947
## F-statistic: 656.9 on 1 and 446 DF, p-value: < 2.2e-16
b1 <- coef(mod1)
La ecuación estimada es precio = 245.68 + 0.873 × área.
En La Flora, cada metro cuadrado adicional se asocia, en promedio, con 0.87 millones más en el precio de oferta, unos 8.7 millones por cada 10 m². El intercepto es alto (245.7 millones) y no describe una vivienda de 0 m². Indica que en este barrio buena parte del precio se paga por la ubicación antes de contar los metros: la recta arranca alta y sube despacio.
Ambos coeficientes son altamente significativos (p < 0.001). El R² de 0.596 dice que el área sola explica cerca del 60 % de la variación del precio.
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.961 | < 0.001 | No se cumple |
| Varianza constante | Breusch-Pagan | 79.159 | < 0.001 | No se cumple |
| Independencia de los errores | Durbin-Watson | 1.812 | 0.023 | No se cumple |
| Forma funcional lineal | RESET de Ramsey | 12.785 | < 0.001 | No se cumple |
# La observación más influyente del modelo
cook1 <- cooks.distance(mod1)
datos_sub[which.max(cook1), c("id", "Tipo", "precio_millon", "Area_contruida",
"Habitaciones", "Banos", "Estrato")]
| id | Tipo | precio_millon | Area_contruida | Habitaciones | Banos | Estrato |
|---|---|---|---|---|---|---|
| 8319 | Casa | 1400 | 838 | 5 | 5 | 5 |
max(cook1)
## [1] 0.9888881
Los diagnósticos muestran tres problemas. El primero es la varianza: en Residuals vs Fitted y Scale-Location los residuos se abren a medida que crece el precio ajustado, y Breusch-Pagan lo confirma con contundencia (p < 0.001). El segundo es la normalidad: el Q-Q plot se despega de la diagonal, sobre todo en la cola superior (Shapiro p < 0.001). El tercero es la influencia: una casa de 838 m² ofertada en 1400 millones tiene una distancia de Cook de 0.99, prácticamente en el umbral de 1. Esa sola vivienda tira de la recta. RESET también rechaza la forma lineal (p < 0.001).
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.595 | 65.4 | < 0.001 | < 0.001 | 0.023 | < 0.001 | 0.989 |
| Log-Lin | 0.508 | 67.2 | < 0.001 | 0.131 | 0.065 | < 0.001 | 0.380 |
| Lin-Log | 0.601 | 63.1 | < 0.001 | < 0.001 | < 0.001 | < 0.001 | 0.832 |
| Log-Log | 0.581 | 63.4 | 0.002 | 0.224 | 0.002 | < 0.001 | 0.095 |
La comparación no tiene un ganador absoluto, así que hay que sopesar. El Lin-Log tiene el R² ajustado más alto (0.601), pero su varianza sigue siendo no constante. Las especificaciones con el precio en logaritmo corrigen la heterocedasticidad, y entre ellas el Log-Log es claramente mejor que el Log-Lin. Frente al modelo lineal, el Log-Log cumple homocedasticidad (Breusch-Pagan p = 0.224), acerca los residuos a la normalidad, reduce el MAE de 65.4 a 63.4 millones y baja la distancia de Cook máxima de 0.99 a 0.10: en escala logarítmica, la casa de 838 m² deja de ser una amenaza. Cede un poco de R² ajustado, pero esa cifra no es comparable entre escalas.
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.6751 -0.1334 0.0083 0.1673 0.6529
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 3.77705 0.08547 44.19 <2e-16 ***
## log(Area_contruida) 0.43525 0.01748 24.91 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.2082 on 446 degrees of freedom
## Multiple R-squared: 0.5817, Adjusted R-squared: 0.5808
## F-statistic: 620.3 on 1 and 446 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.989 | 0.002 | No se cumple |
| Varianza constante | Breusch-Pagan | 1.481 | 0.224 | Se cumple |
| Independencia de los errores | Durbin-Watson | 1.723 | 0.002 | No se cumple |
| Forma funcional lineal | RESET de Ramsey | 14.623 | < 0.001 | No se cumple |
b1l <- coef(mod1_log)
Interpretación del modelo Log-Log. El coeficiente 0.435 es una elasticidad: si el área construida aumenta 1 %, el precio aumenta en promedio 0.44 %. Con un aumento del 10 % en el área, el precio sube cerca de 4.2 %. La elasticidad es baja: duplicar el área multiplica el precio apenas por 1.35. Es la misma tensión de la exploración: en La Flora, los metros adicionales se pagan cada vez menos, sobre todo cuando vienen en forma de casa grande.
Los residuos siguen sin ser del todo normales (Shapiro p = 0.002) y RESET aún detecta curvatura (p < 0.001). Parte de esa curvatura viene de que el barrio mezcla dos mercados (apartamentos y casas) que una sola variable no separa, y esa es la motivación de la dummy de la sección 3. Con 448 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
## -230.30 -58.43 -7.76 37.24 819.63
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 117.687 14.385 8.181 2.96e-15 ***
## Banos 92.537 4.749 19.484 < 2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 101.1 on 446 degrees of freedom
## Multiple R-squared: 0.4598, Adjusted R-squared: 0.4586
## F-statistic: 379.6 on 1 and 446 DF, p-value: < 2.2e-16
b2 <- coef(mod2)
La ecuación es precio = 117.69 + 92.54 × baños. Cada baño adicional se asocia con 92.5 millones más. Más que el valor de un baño en sí, el coeficiente captura lo que los baños delatan: tamaño, categoría y, a menudo, una remodelación reciente. El coeficiente es significativo (p < 0.001) y el modelo explica el 46 % de la variación del precio (R² = 0.460).
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.874 | < 0.001 | No se cumple |
| Varianza constante | Breusch-Pagan | 12.942 | < 0.001 | No se cumple |
| Independencia de los errores | Durbin-Watson | 1.729 | 0.002 | No se cumple |
| Forma funcional lineal | RESET de Ramsey | 1.814 | 0.164 | Se cumple |
Como los baños toman pocos valores, los residuos se ordenan en columnas verticales, algo esperable con un predictor discreto. La forma lineal es aceptable (RESET p = 0.164), pero la dispersión crece con el número de baños, y Breusch-Pagan rechaza la varianza constante (p < 0.001). Los residuos tampoco son normales (p < 0.001): hay viviendas con muchos baños y precios muy por encima de lo esperado.
cmp2 <- comparar_log("Banos", datos_sub)
cmp2
| Modelo | R2_ajustado | MAE | p_Shapiro | p_BreuschPagan | p_DurbinWatson | p_RESET | Cook_max |
|---|---|---|---|---|---|---|---|
| Lin-Lin | 0.459 | 68.6 | < 0.001 | < 0.001 | 0.002 | 0.164 | 0.417 |
| Log-Lin | 0.460 | 69.6 | < 0.001 | 0.582 | 0.079 | 0.416 | 0.090 |
| Lin-Log | 0.423 | 70.5 | < 0.001 | < 0.001 | < 0.001 | < 0.001 | 0.338 |
| Log-Log | 0.456 | 68.7 | < 0.001 | 0.276 | 0.021 | 0.014 | 0.081 |
mod2_log <- lm(log(precio_millon) ~ Banos, data = datos_sub)
summary(mod2_log)
##
## Call:
## lm(formula = log(precio_millon) ~ Banos, data = datos_sub)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.8167 -0.1449 0.0139 0.1332 0.9751
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 5.27158 0.03363 156.77 <2e-16 ***
## Banos 0.21701 0.01110 19.55 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.2362 on 446 degrees of freedom
## Multiple R-squared: 0.4614, Adjusted R-squared: 0.4602
## F-statistic: 382.1 on 1 and 446 DF, p-value: < 2.2e-16
par(mfrow = c(2, 2))
plot(mod2_log)
par(mfrow = c(1, 1))
sup2_log <- validar_supuestos(mod2_log)
sup2_log
| Supuesto | Prueba | Estadistico | p_valor | Resultado |
|---|---|---|---|---|
| Normalidad de los residuos | Shapiro-Wilk | 0.984 | < 0.001 | No se cumple |
| Varianza constante | Breusch-Pagan | 0.303 | 0.582 | Se cumple |
| Independencia de los errores | Durbin-Watson | 1.868 | 0.079 | Se cumple |
| Forma funcional lineal | RESET de Ramsey | 0.878 | 0.416 | Se cumple |
b2l <- coef(mod2_log)
Aquí gana el Log-Lin, con el precio en logaritmo y los baños en niveles. Es la única especificación que cumple a la vez varianza constante (Breusch-Pagan p = 0.582) y forma funcional (RESET p = 0.416), con el R² ajustado más alto (0.460). Su MAE (69.6) es casi igual al del lineal (68.6). El Log-Log también corrige la varianza, pero rompe la forma funcional, y el Lin-Log no corrige nada.
Interpretación del modelo Log-Lin. En un modelo con el precio en logaritmo y el predictor en niveles, el coeficiente se traduce en porcentaje con (e^b − 1) × 100. Cada baño adicional eleva el precio en un 24.2 %. Leerlo en porcentaje tiene más sentido que en millones fijos, porque un baño adicional no vale lo mismo en un apartamento de 250 millones que en una casa de 800. Conclusión: la transformación logarítmica del precio es pertinente.
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
## -262.82 -71.64 -7.87 54.30 900.89
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 142.92 18.22 7.843 3.27e-14 ***
## Habitaciones 71.24 5.18 13.752 < 2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 115.2 on 446 degrees of freedom
## Multiple R-squared: 0.2978, Adjusted R-squared: 0.2962
## F-statistic: 189.1 on 1 and 446 DF, p-value: < 2.2e-16
b3 <- coef(mod3)
La ecuación es precio = 142.92 + 71.24 × habitaciones. Cada habitación adicional se asocia con 71.2 millones más. El coeficiente es significativo (p < 0.001), pero el modelo explica solo el 30 % de la variación del precio (R² = 0.298), muy por debajo del área y los baños. En La Flora, contar habitaciones dice poco: una casa antigua de cinco cuartos puede costar menos que un apartamento moderno de tres.
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.888 | < 0.001 | No se cumple |
| Varianza constante | Breusch-Pagan | 9.836 | 0.002 | No se cumple |
| Independencia de los errores | Durbin-Watson | 1.761 | 0.005 | No se cumple |
| Forma funcional lineal | RESET de Ramsey | 15.707 | < 0.001 | No se cumple |
El modelo incumple homocedasticidad (Breusch-Pagan p = 0.002), normalidad (p < 0.001) y forma funcional (RESET p < 0.001). Los residuos más grandes son viviendas muy caras que el número de cuartos no logra anticipar.
cmp3 <- comparar_log("Habitaciones", datos_sub)
cmp3
| Modelo | R2_ajustado | MAE | p_Shapiro | p_BreuschPagan | p_DurbinWatson | p_RESET | Cook_max |
|---|---|---|---|---|---|---|---|
| Lin-Lin | 0.296 | 82.1 | < 0.001 | 0.002 | 0.005 | < 0.001 | 0.239 |
| Log-Lin | 0.275 | 81.4 | 0.004 | 0.312 | 0.147 | < 0.001 | 0.094 |
| Lin-Log | 0.259 | 84.4 | < 0.001 | 0.039 | 0.002 | < 0.001 | 0.298 |
| Log-Log | 0.243 | 81.7 | < 0.001 | 0.405 | 0.087 | < 0.001 | 0.300 |
mod3_log <- lm(log(precio_millon) ~ Habitaciones, data = datos_sub)
summary(mod3_log)
##
## Call:
## lm(formula = log(precio_millon) ~ Habitaciones, data = datos_sub)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.82359 -0.16435 -0.00366 0.18193 1.08861
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 5.35215 0.04330 123.60 <2e-16 ***
## Habitaciones 0.16069 0.01231 13.05 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.2738 on 446 degrees of freedom
## Multiple R-squared: 0.2765, Adjusted R-squared: 0.2748
## F-statistic: 170.4 on 1 and 446 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.990 | 0.004 | No se cumple |
| Varianza constante | Breusch-Pagan | 1.024 | 0.312 | Se cumple |
| Independencia de los errores | Durbin-Watson | 1.902 | 0.147 | Se cumple |
| Forma funcional lineal | RESET de Ramsey | 13.443 | < 0.001 | No se cumple |
b3l <- coef(mod3_log)
El logaritmo del precio resuelve la heterocedasticidad (Breusch-Pagan pasa de 0.002 a 0.312 en el Log-Lin) y acerca los residuos a la normalidad, pero ninguna especificación arregla la forma funcional: RESET rechaza en las cuatro. Se elige el Log-Lin porque cumple la varianza constante con el MAE más bajo (81.4 millones), aunque su R² ajustado (0.275) queda ligeramente por debajo del lineal.
Interpretación del modelo Log-Lin. Cada habitación adicional eleva el precio un 17.4 % en promedio. Conclusión: la transformación es pertinente para corregir la varianza, pero no convierte a las habitaciones en un buen predictor por sí solas. Su verdadero papel aparece en el modelo múltiple, cuando se compara a igualdad de área.
data.frame(
Modelo = c("1. Área construida", "2. Baños", "3. Habitaciones"),
Especificacion = c("Log-Log", "Log-Lin", "Log-Lin"),
R2_ajustado = round(c(summary(mod1_log)$adj.r.squared, summary(mod2_log)$adj.r.squared,
summary(mod3_log)$adj.r.squared), 3),
MAE_millones = round(c(mae(mod1_log), mae(mod2_log), mae(mod3_log)), 1)
)
| Modelo | Especificacion | R2_ajustado | MAE_millones |
|---|---|---|---|
| 1. Área construida | Log-Log | 0.581 | 63.4 |
| 2. Baños | Log-Lin | 0.460 | 69.6 |
| 3. Habitaciones | Log-Lin | 0.275 | 81.4 |
El área construida es el mejor predictor individual del precio en La Flora: explica más y se equivoca menos. En los tres casos, llevar el precio a logaritmo fue la decisión correcta, porque la varianza crecía con el precio en todos los modelos lineales. La forma del predictor sí cambió de un caso a otro: logaritmo para el área, que tiene rendimientos decrecientes, y niveles para baños y habitaciones, cuyo efecto se lee mejor como un porcentaje fijo por unidad.
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 de
parqueaderos):
datos_mod <- datos_sub %>%
select(id, precio_millon, Area_contruida, Habitaciones, Banos, parqueaderos, Estrato, Tipo) %>%
na.omit()
nrow(datos_mod)
## [1] 431
table(datos_mod$Tipo)
##
## Apartamento Casa
## 321 110
Se trabaja con 431 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
## -260.48 -48.31 -3.16 52.68 488.27
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 185.28226 15.21637 12.177 < 2e-16 ***
## Area_contruida 0.70813 0.04849 14.604 < 2e-16 ***
## Habitaciones -13.48226 5.45976 -2.469 0.0139 *
## Banos 43.52722 5.93482 7.334 1.13e-12 ***
## parqueaderos 5.23846 4.82095 1.087 0.2778
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 81.33 on 426 degrees of freedom
## Multiple R-squared: 0.6539, Adjusted R-squared: 0.6507
## F-statistic: 201.2 on 4 and 426 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 65.1 % de la variación del precio (R² ajustado), más que cualquiera de los modelos simples.
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.950 | < 0.001 | No se cumple |
| Varianza constante | Breusch-Pagan | 102.656 | < 0.001 | No se cumple |
| Independencia de los errores | Durbin-Watson | 1.650 | < 0.001 | No se cumple |
| Forma funcional lineal | RESET de Ramsey | 1.436 | 0.239 | Se cumple |
# Multicolinealidad: un VIF mayor a 5 sería preocupante
vif(mm1)
## Area_contruida Habitaciones Banos parqueaderos
## 2.227919 2.181479 2.285302 1.476864
# Observación más influyente
cook_mm1 <- cooks.distance(mm1)
datos_mod[which.max(cook_mm1), ]
| id | precio_millon | Area_contruida | Habitaciones | Banos | parqueaderos | Estrato | Tipo |
|---|---|---|---|---|---|---|---|
| 8319 | 1400 | 838 | 5 | 5 | 1 | 5 | Casa |
max(cook_mm1)
## [1] 1.101167
El VIF está por debajo de 5 en todas las variables, así que la multicolinealidad es moderada y no infla los errores estándar de manera grave. La forma funcional es aceptable (RESET p = 0.239), pero hay dos problemas serios. La heterocedasticidad es fuerte (Breusch-Pagan p < 0.001) y una observación supera la distancia de Cook de 1 (1.10): de nuevo la casa de 838 m² y 1400 millones, que en Residuals vs Leverage aparece justo sobre la curva punteada que marca la distancia de Cook de 1. Los residuos tampoco son normales (p < 0.001).
No se elimina esa vivienda, porque no hay evidencia de que sea un error: es una casa muy grande y muy cara, y existen. La alternativa es una transformación que le quite peso.
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.64582 -0.13279 0.01357 0.14917 0.64600
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 3.957677 0.102543 38.595 < 2e-16 ***
## log(Area_contruida) 0.374347 0.028750 13.021 < 2e-16 ***
## Habitaciones -0.047690 0.013602 -3.506 0.000503 ***
## Banos 0.092975 0.014704 6.323 6.49e-10 ***
## parqueaderos 0.008919 0.011673 0.764 0.445273
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.1956 on 426 degrees of freedom
## Multiple R-squared: 0.6239, Adjusted R-squared: 0.6203
## F-statistic: 176.6 on 4 and 426 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.984 | < 0.001 | No se cumple |
| Varianza constante | Breusch-Pagan | 9.071 | 0.059 | Se cumple |
| Independencia de los errores | Durbin-Watson | 1.595 | < 0.001 | No se cumple |
| Forma funcional lineal | RESET de Ramsey | 4.752 | 0.009 | 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.651 | 60.6 | < 0.001 | < 0.001 | < 0.001 | 0.239 | 1.101 |
| MM1 log | 0.620 | 60.9 | < 0.001 | 0.059 | < 0.001 | 0.009 | 0.087 |
c1l <- coef(mm1_log)
p1l <- summary(mm1_log)$coefficients[, 4]
Esta vez la decisión tiene costos y beneficios. La versión logarítmica gana en dos frentes importantes: la heterocedasticidad deja de ser significativa al 5 % (Breusch-Pagan p = 0.059) y la distancia de Cook máxima cae de 1.10 a 0.09, así que la casa influyente pierde casi todo su peso. A cambio, pierde un poco de R² ajustado y RESET pasa a rechazar (p = 0.009). El MAE es prácticamente igual (60.9 frente a 60.6).
Se prefiere la versión logarítmica. Una estimación que depende de una sola casa y unos errores estándar distorsionados por la heterocedasticidad son problemas más graves para interpretar los coeficientes que una leve curvatura residual.
Interpretación (versión log). Con lo demás constante, un 10 % más de área se asocia con un 3.6 % más de precio. Cada habitación adicional, a igual área, reduce el precio un 4.7 % (p < 0.001), mientras que cada baño lo eleva un 9.7 % (p < 0.001). Los parqueaderos siguen sin ser significativos (p = 0.445).
El segundo modelo incorpora qué se vende y en qué estrato, además del tamaño (área, baños y parqueaderos):
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
## -278.18 -47.25 0.58 46.69 517.03
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 38.37144 62.40791 0.615 0.538983
## Area_contruida 0.78876 0.05448 14.477 < 2e-16 ***
## Banos 41.24338 5.52454 7.465 4.74e-13 ***
## parqueaderos 7.52367 4.78665 1.572 0.116740
## TipoCasa -55.85021 14.37288 -3.886 0.000118 ***
## Estrato 21.57644 12.41934 1.737 0.083054 .
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 80.31 on 425 degrees of freedom
## Multiple R-squared: 0.6633, Adjusted R-squared: 0.6594
## F-statistic: 167.5 on 5 and 425 DF, p-value: < 2.2e-16
c2 <- coef(mm2)
p2 <- summary(mm2)$coefficients[, 4]
El R² ajustado es 0.659, algo mayor que el del modelo múltiple 1, aunque este modelo no incluye las habitaciones. El tipo de inmueble aporta 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.942 | < 0.001 | No se cumple |
| Varianza constante | Breusch-Pagan | 99.391 | < 0.001 | No se cumple |
| Independencia de los errores | Durbin-Watson | 1.713 | 0.001 | No se cumple |
| Forma funcional lineal | RESET de Ramsey | 3.005 | 0.051 | Se cumple |
vif(mm2)
## Area_contruida Banos parqueaderos Tipo Estrato
## 2.884754 2.030940 1.493188 2.623951 1.002261
La multicolinealidad es baja (VIF < 5) y la forma funcional queda justo en el límite (RESET p = 0.051). La heterocedasticidad sigue siendo fuerte (Breusch-Pagan p < 0.001), los residuos no son normales (p < 0.001) y la casa grande vuelve a acercarse al umbral de influencia (Cook máximo de 0.93).
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.67480 -0.11706 0.02259 0.13686 0.72856
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 3.10821 0.20931 14.850 < 2e-16 ***
## log(Area_contruida) 0.46348 0.03549 13.059 < 2e-16 ***
## Banos 0.07618 0.01381 5.517 6.00e-08 ***
## parqueaderos 0.01500 0.01141 1.315 0.1892
## TipoCasa -0.20975 0.03936 -5.329 1.61e-07 ***
## Estrato 0.07085 0.02952 2.400 0.0168 *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.191 on 425 degrees of freedom
## Multiple R-squared: 0.642, Adjusted R-squared: 0.6378
## F-statistic: 152.5 on 5 and 425 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.977 | < 0.001 | No se cumple |
| Varianza constante | Breusch-Pagan | 17.675 | 0.003 | No se cumple |
| Independencia de los errores | Durbin-Watson | 1.712 | 0.001 | No se cumple |
| Forma funcional lineal | RESET de Ramsey | 1.283 | 0.278 | 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.659 | 58.2 | < 0.001 | < 0.001 | 0.001 | 0.051 | 0.934 |
| MM2 log | 0.638 | 57.3 | < 0.001 | 0.003 | 0.001 | 0.278 | 0.077 |
c2l <- coef(mm2_log)
p2l <- summary(mm2_log)$coefficients[, 4]
La versión logarítmica es mejor. Tiene el MAE más bajo hasta ahora (57.3 millones), conserva una forma funcional aceptable (RESET p = 0.278) y reduce la distancia de Cook máxima a 0.08. La heterocedasticidad no desaparece del todo (Breusch-Pagan p = 0.003), pero se reduce de forma drástica: el estadístico pasa de 99.4 a 17.7. Lo que queda se puede atender con errores estándar robustos, como se hace en la sección 3.4.
Interpretación (versión log). 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=-1428
## log(precio_millon) ~ log(Area_contruida) + Habitaciones + Banos +
## parqueaderos + Tipo + Estrato
##
## Df Sum of Sq RSS AIC
## - parqueaderos 1 0.0462 15.233 -1428.7
## <none> 15.187 -1428.0
## - Estrato 1 0.1826 15.369 -1424.8
## - Habitaciones 1 0.3205 15.507 -1421.0
## - Tipo 1 0.9196 16.106 -1404.7
## - Banos 1 1.3700 16.556 -1392.8
## - log(Area_contruida) 1 6.4848 21.671 -1276.7
##
## Step: AIC=-1428.69
## log(precio_millon) ~ log(Area_contruida) + Habitaciones + Banos +
## Tipo + Estrato
##
## Df Sum of Sq RSS AIC
## <none> 15.233 -1428.7
## + parqueaderos 1 0.0462 15.187 -1428.0
## - Estrato 1 0.1828 15.415 -1425.5
## - Habitaciones 1 0.3374 15.570 -1421.2
## - Tipo 1 0.8956 16.128 -1406.1
## - Banos 1 1.6819 16.915 -1385.5
## - log(Area_contruida) 1 6.6956 21.928 -1273.7
# Resumen de los pasos realizados
modelo_step$anova
| Step | Df | Deviance | Resid. Df | Resid. Dev | AIC |
|---|---|---|---|---|---|
| NA | NA | 424 | 15.18651 | -1427.997 | |
| - parqueaderos | 1 | 0.0462044 | 425 | 15.23272 | -1428.688 |
# 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) + Banos + Tipo + Habitaciones +
## Estrato
summary(modelo_step)
##
## Call:
## lm(formula = log(precio_millon) ~ log(Area_contruida) + Habitaciones +
## Banos + Tipo + Estrato, data = datos_mod)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.6584 -0.1219 0.0213 0.1455 0.7046
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 3.04926 0.20764 14.686 < 2e-16 ***
## log(Area_contruida) 0.50281 0.03679 13.668 < 2e-16 ***
## Habitaciones -0.04056 0.01322 -3.068 0.00229 **
## Banos 0.09309 0.01359 6.850 2.60e-11 ***
## TipoCasa -0.19546 0.03910 -4.999 8.45e-07 ***
## Estrato 0.06618 0.02930 2.258 0.02442 *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.1893 on 425 degrees of freedom
## Multiple R-squared: 0.6484, Adjusted R-squared: 0.6442
## F-statistic: 156.7 on 5 and 425 DF, p-value: < 2.2e-16
cs <- coef(modelo_step)
ps <- summary(modelo_step)$coefficients[, 4]
Qué decidió el algoritmo. Solo sobró una variable: parqueaderos, que ya era no significativa en todos los modelos. Al retirarla, el AIC bajó de -1428.0 a -1428.7, y ningún otro cambio lo mejoraba. La selección hacia adelante, que parte del modelo vacío, llega al mismo conjunto de variables.
El modelo final es:
log(precio) = 3.049 + 0.503 · log(área) - 0.041 · habitaciones + 0.093 · baños - 0.195 · Casa + 0.066 · 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.979 | < 0.001 | No se cumple |
| Varianza constante | Breusch-Pagan | 14.757 | 0.011 | No se cumple |
| Independencia de los errores | Durbin-Watson | 1.696 | < 0.001 | No se cumple |
| Forma funcional lineal | RESET de Ramsey | 1.394 | 0.249 | Se cumple |
vif(modelo_step)
## log(Area_contruida) Habitaciones Banos Tipo
## 4.998291 2.360819 2.211193 3.494696
## Estrato
## 1.004074
# Errores estándar robustos (HC3): corrigen la inferencia ante heterocedasticidad
rob <- coeftest(modelo_step, vcov. = hccm(modelo_step, type = "hc3"))
rob
##
## t test of coefficients:
##
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 3.049262 0.268902 11.3397 < 2.2e-16 ***
## log(Area_contruida) 0.502811 0.041986 11.9758 < 2.2e-16 ***
## Habitaciones -0.040561 0.014585 -2.7811 0.005659 **
## Banos 0.093087 0.014902 6.2465 1.020e-09 ***
## TipoCasa -0.195459 0.036225 -5.3957 1.136e-07 ***
## Estrato 0.066179 0.037999 1.7416 0.082305 .
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
El modelo seleccionado cumple la forma funcional (RESET p = 0.249), pero conserva algo de heterocedasticidad (Breusch-Pagan p = 0.011) y residuos no normales (p < 0.001). El VIF del logaritmo del área ronda 5.0, en el límite de lo aceptable. Esto se explica porque área y tipo van de la mano: las casas son las viviendas grandes.
Por la heterocedasticidad, la inferencia correcta es la de los errores estándar robustos (HC3). Con ellos, área, habitaciones, baños y tipo mantienen su significancia, pero el estrato deja de ser significativo al 5 % (p robusto = 0.082). La lectura honesta es que, en un barrio casi todo estrato 5, el efecto del estrato no se puede afirmar con confianza.
¿Cuánto debería pedirse por dos viviendas típicas de La Flora? 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(100, 200),
Habitaciones = c(3, 4),
Banos = c(2, 3),
parqueaderos = c(1, 2),
Estrato = c(5, 5))
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 | 100 | 3 | 2 | 1 | 5 | 317.4 | 309.3 | 325.7 | 218.6 | 460.9 |
| Casa | 200 | 4 | 3 | 2 | 5 | 389.9 | 373.5 | 406.9 | 268.1 | 567.0 |
Interpretación. Para un apartamento de 100 m², 3 habitaciones, 2 baños, estrato 5, el modelo estima 317 millones. Para una casa de 200 m², 4 habitaciones, 3 baños, estrato 5, estima 390 millones. La casa tiene el doble de área y un baño más, pero su precio estimado es apenas un 23 % mayor. La elasticidad baja del área y el descuento por ser casa se combinan para producir ese resultado.
El intervalo de confianza (309 a 326 millones para el apartamento) es estrecho porque estima el precio promedio de todos los apartamentos con esas características. El intervalo de predicción (219 a 461 millones) es mucho más ancho porque se refiere a una oferta concreta, con sus acabados, su piso y el estado de su edificio. Una oferta de ese apartamento por encima de 461 millones estaría fuera de lo que el mercado del barrio sugiere.
Los 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.578 | 63.3 | 63.8 | 16.6 |
| MM1 lineal | 0.651 | 60.6 | 62.1 | 15.9 |
| MM1 log | 0.620 | 60.9 | 62.0 | 16.0 |
| MM2 lineal (dummy) | 0.659 | 58.2 | 60.2 | 15.2 |
| MM2 log (dummy) | 0.638 | 57.3 | 58.9 | 15.0 |
| Stepwise (log) | 0.644 | 57.4 | 58.6 | 15.1 |
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 57.4 millones por vivienda, cerca del 15.1 % del precio medio del barrio (381.5 millones). El mejor modelo simple fallaba por 63.3 millones: añadir distribución, tipo y estrato recorta el error en 5.9 millones por vivienda, y el R² ajustado sube de 0.578 a 0.644.
En validación cruzada, el MAE del stepwise es de 58.6 millones, un poco por encima del valor dentro de muestra, como es normal. No hay señales de sobreajuste. Los modelos con dummy (MM2 y stepwise) le ganan al MM1 tanto dentro como fuera de muestra: incorporar el tipo de vivienda y el estrato mejora la predicción más que sumar medidas de tamaño. El MM2 log y el stepwise quedan prácticamente empatados (diferencias de menos de un millón). El stepwise se elige con el AIC, no con el MAE, y ambos son buenas opciones.
En la Figura 4, los puntos siguen la diagonal y se dispersan más en los precios altos. Las casas grandes y caras siguen siendo las más difíciles de anticipar.
Tipo:Area, que muestra la
Figura 3). Esas son las vías naturales para mejorar el modelo.