En este informe se modela el precio de venta (en millones de pesos) de los apartamentos ofertados en el barrio Cristales de Cali, usando la base Datos_Vivienda. Se plantean modelos de regresión lineal simple y múltiple, se validan sus supuestos, se evalúa la pertinencia de transformaciones logarítmicas, se seleccionan variables con stepwise, se hace una predicción hipotética y se calcula el error absoluto medio (MAE).
# install.packages(c("readxl","dplyr","ggplot2","knitr","kableExtra","lmtest","car")) # solo la primera vez
library(readxl)
library(dplyr)
library(ggplot2)
library(knitr)
library(kableExtra)
library(lmtest) # bptest, dwtest, resettest
library(car) # vif, boxCox, outlierTest
datos <- read_excel("Datos_Vivienda.xlsx")
datos <- data.frame(id = 1:nrow(datos), datos)
# Algunas columnas (piso, parqueaderos) vienen como texto con "NA": se pasan a número
datos <- datos %>%
mutate(across(c(piso, Estrato, precio_millon, Area_contruida, parqueaderos,
Banos, Habitaciones), ~ suppressWarnings(as.numeric(.x))))
str(datos)
## 'data.frame': 8319 obs. of 13 variables:
## $ id : int 1 2 3 4 5 6 7 8 9 10 ...
## $ Zona : chr "Zona Sur" "Zona Oeste" "Zona Sur" "Zona Sur" ...
## $ piso : num 2 2 3 NA 2 3 2 5 9 6 ...
## $ Estrato : num 6 4 5 6 6 6 6 5 4 6 ...
## $ precio_millon : num 880 1200 250 1280 1300 513 870 310 240 690 ...
## $ Area_contruida : num 237 800 86 346 600 160 490 82.5 80 150 ...
## $ parqueaderos : num 2 3 NA 4 4 2 3 1 1 2 ...
## $ Banos : num 5 6 2 6 7 4 6 2 2 5 ...
## $ Habitaciones : num 4 7 3 5 5 4 5 3 3 4 ...
## $ Tipo : chr "Casa" "Casa" "Apartamento" "Apartamento" ...
## $ Barrio : chr "pance" "miraflores" "multicentro" "ciudad jardin" ...
## $ cordenada_longitud: num -76.5 -76.5 -76.5 -76.5 -76.5 ...
## $ Cordenada_latitud : num 3.43 3.43 3.43 3.43 3.43 ...
Se elige un solo barrio (el que más registros tiene y supera ampliamente los 30 datos): “los cristales”. No se mezclan las otras variantes de escritura.
barrio_elegido <- "los cristales" # alternativa: "cristales" (72 apartamentos)
datos_sub <- datos %>%
filter(tolower(trimws(Barrio)) == barrio_elegido,
Tipo == "Apartamento")
cat("Apartamentos en", barrio_elegido, ":", nrow(datos_sub), "\n")
## Apartamentos en los cristales : 137
head(datos_sub) # primeras filas del barrio filtrado
| id | Zona | piso | Estrato | precio_millon | Area_contruida | parqueaderos | Banos | Habitaciones | Tipo | Barrio | cordenada_longitud | Cordenada_latitud |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1049 | Zona Oeste | 3 | 6 | 335 | 84 | 2 | 2 | 2 | Apartamento | los cristales | -76.50849 | 3.42043 |
| 3950 | Zona Oeste | NA | 5 | 530 | 123 | 1 | 3 | 3 | Apartamento | los cristales | -76.52900 | 3.41300 |
| 4331 | Zona Oeste | 2 | 6 | 530 | 123 | 2 | 4 | 3 | Apartamento | los cristales | -76.53098 | 3.43489 |
| 4381 | Zona Oeste | 3 | 6 | 335 | 85 | 2 | 2 | 3 | Apartamento | los cristales | -76.53102 | 3.42192 |
| 4858 | Zona Oeste | 3 | 6 | 385 | 103 | 2 | 2 | 3 | Apartamento | los cristales | -76.53340 | 3.42287 |
| 4859 | Zona Oeste | 5 | 6 | 330 | 85 | 2 | 2 | 2 | Apartamento | los cristales | -76.53340 | 3.42287 |
colSums(is.na(datos_sub))
## id Zona piso Estrato
## 0 0 28 0
## precio_millon Area_contruida parqueaderos Banos
## 0 0 2 0
## Habitaciones Tipo Barrio cordenada_longitud
## 0 0 0 0
## Cordenada_latitud
## 0
piso tiene muchos datos faltantes, por lo que no
se usa como predictor. En las demás variables se eliminan las
pocas filas con NA.
Antes de modelar se tratan los datos faltantes (NA). Para los
estadísticos descriptivos se usa na.rm = TRUE, que ignora
los NA en el cálculo:
vars <- c("precio_millon", "Area_contruida", "Banos", "Habitaciones", "parqueaderos")
data.frame(
Variable = vars,
NA_totales = sapply(datos_sub[, vars], function(x) sum(is.na(x))),
Media = sapply(datos_sub[, vars], mean, na.rm = TRUE),
Mediana = sapply(datos_sub[, vars], median, na.rm = TRUE),
Desv_est = sapply(datos_sub[, vars], sd, na.rm = TRUE),
Minimo = sapply(datos_sub[, vars], min, na.rm = TRUE),
Maximo = sapply(datos_sub[, vars], max, na.rm = TRUE),
row.names = NULL) %>%
mutate(across(where(is.numeric), ~ round(.x, 2)))
| Variable | NA_totales | Media | Mediana | Desv_est | Minimo | Maximo |
|---|---|---|---|---|---|---|
| precio_millon | 0 | 544.88 | 450 | 259.03 | 210 | 1800 |
| Area_contruida | 0 | 141.26 | 123 | 63.74 | 66 | 460 |
| Banos | 0 | 3.12 | 3 | 1.11 | 0 | 6 |
| Habitaciones | 0 | 2.99 | 3 | 0.76 | 0 | 5 |
| parqueaderos | 2 | 1.93 | 2 | 0.51 | 1 | 4 |
Como lm() no tiene el argumento na.rm, las
filas con NA se retiran de la base antes de ajustar los modelos:
datos_sub <- datos_sub %>%
select(id, precio_millon, Area_contruida, Banos, Habitaciones, parqueaderos, Estrato) %>%
na.omit() %>%
filter(Area_contruida > 0, precio_millon > 0)
# Variable DUMMY: 1 si el apartamento es estrato 6, 0 si es estrato 4 o 5
datos_sub$estrato6 <- ifelse(datos_sub$Estrato == 6, 1, 0)
cat("Observaciones finales:", nrow(datos_sub))
## Observaciones finales: 135
summary(datos_sub[, -1])
## precio_millon Area_contruida Banos Habitaciones
## Min. : 210.0 Min. : 66.0 Min. :0.000 Min. :1.000
## 1st Qu.: 390.0 1st Qu.:103.0 1st Qu.:2.000 1st Qu.:3.000
## Median : 460.0 Median :123.0 Median :3.000 Median :3.000
## Mean : 547.6 Mean :141.8 Mean :3.156 Mean :3.015
## 3rd Qu.: 625.0 3rd Qu.:165.0 3rd Qu.:4.000 3rd Qu.:3.000
## Max. :1800.0 Max. :460.0 Max. :6.000 Max. :5.000
## parqueaderos Estrato estrato6
## Min. :1.000 Min. :4.000 Min. :0.0000
## 1st Qu.:2.000 1st Qu.:5.000 1st Qu.:0.0000
## Median :2.000 Median :6.000 Median :1.0000
## Mean :1.933 Mean :5.659 Mean :0.6815
## 3rd Qu.:2.000 3rd Qu.:6.000 3rd Qu.:1.0000
## Max. :4.000 Max. :6.000 Max. :1.0000
ggplot(datos_sub, aes(precio_millon)) +
geom_histogram(bins = 20, fill = "steelblue", color = "black", alpha = .7) +
labs(title = "Figura 1. Distribución del precio", x = "Precio (millones)", y = "Frecuencia") +
theme_minimal()
ggplot(datos_sub, aes(Area_contruida, precio_millon)) +
geom_point(color = "purple", alpha = .7) + geom_smooth(method = "loess", color = "grey40") +
labs(title = "Figura 2. Precio vs. área construida", x = "Área (m²)", y = "Precio (millones)") +
theme_minimal()
round(cor(datos_sub[, c("precio_millon","Area_contruida","Banos","Habitaciones","parqueaderos","estrato6")], use = "complete.obs"), 2)
## precio_millon Area_contruida Banos Habitaciones parqueaderos
## precio_millon 1.00 0.84 0.67 0.36 0.61
## Area_contruida 0.84 1.00 0.64 0.43 0.61
## Banos 0.67 0.64 1.00 0.66 0.43
## Habitaciones 0.36 0.43 0.66 1.00 0.37
## parqueaderos 0.61 0.61 0.43 0.37 1.00
## estrato6 0.26 0.20 0.19 0.10 0.26
## estrato6
## precio_millon 0.26
## Area_contruida 0.20
## Banos 0.19
## Habitaciones 0.10
## parqueaderos 0.26
## estrato6 1.00
Se plantean tres modelos de regresión simple con el precio como variable respuesta y como predictores el área construida, el número de baños y el número de parqueaderos.
# Diagnóstico de supuestos: gráficos y pruebas formales
diagnostico <- function(mod) {
par(mfrow = c(2, 2)); plot(mod); par(mfrow = c(1, 1))
data.frame(
Supuesto = c("Normalidad de residuos (Shapiro-Wilk)",
"Homocedasticidad (Breusch-Pagan)",
"Independencia (Durbin-Watson)",
"Linealidad / forma funcional (RESET)"),
`p-valor` = round(c(shapiro.test(residuals(mod))$p.value,
bptest(mod)$p.value,
dwtest(mod)$p.value,
resettest(mod)$p.value), 4),
check.names = FALSE)
}
# Tabla resumen para comparar un modelo
resumen_mod <- function(mod, nombre) {
data.frame(Modelo = nombre,
R2_ajustado = round(summary(mod)$adj.r.squared, 4),
Shapiro_p = round(shapiro.test(residuals(mod))$p.value, 4),
BreuschPagan_p = round(bptest(mod)$p.value, 4),
RESET_p = round(resettest(mod)$p.value, 4))
}
Criterio de decisión: con α = 0,05, no se rechaza el supuesto si el p-valor es mayor a 0,05.
m1 <- lm(precio_millon ~ Area_contruida, data = datos_sub)
summary(m1)
##
## Call:
## lm(formula = precio_millon ~ Area_contruida, data = datos_sub)
##
## Residuals:
## Min 1Q Median 3Q Max
## -443.02 -43.39 -14.96 46.64 530.62
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 62.6419 29.4356 2.128 0.0352 *
## Area_contruida 3.4205 0.1893 18.068 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 140.4 on 133 degrees of freedom
## Multiple R-squared: 0.7105, Adjusted R-squared: 0.7084
## F-statistic: 326.5 on 1 and 133 DF, p-value: < 2.2e-16
Interpretación: el intercepto es 62.64 y la pendiente es 3.421: por cada m² adicional de área construida, el precio esperado cambia en 3.421 millones de pesos. El R² es 0.711, es decir, el área explica aproximadamente el 71.1% de la variabilidad del precio. (Comenta significancia del coeficiente según el p-valor.)
Validación de supuestos (modelo lineal):
diagnostico(m1)
| Supuesto | p-valor |
|---|---|
| Normalidad de residuos (Shapiro-Wilk) | 0.0000 |
| Homocedasticidad (Breusch-Pagan) | 0.0000 |
| Independencia (Durbin-Watson) | 0.2924 |
| Linealidad / forma funcional (RESET) | 0.0982 |
Transformaciones logarítmicas: se compara con el modelo log-lineal (log del precio) y el log-log.
m1_ll <- lm(log(precio_millon) ~ Area_contruida, data = datos_sub) # log-lin
m1_gg <- lm(log(precio_millon) ~ log(Area_contruida), data = datos_sub) # log-log
car::boxCox(m1, lambda = seq(-1, 1, 0.1)) # lambda cercano a 0 sugiere log en la respuesta
rbind(resumen_mod(m1, "Lineal"), resumen_mod(m1_ll, "Log-lineal"), resumen_mod(m1_gg, "Log-log"))
| Modelo | R2_ajustado | Shapiro_p | BreuschPagan_p | RESET_p | |
|---|---|---|---|---|---|
| BP | Lineal | 0.7084 | 0.0000 | 0e+00 | 0.0982 |
| BP1 | Log-lineal | 0.6616 | 0.0143 | 1e-04 | 0.0000 |
| BP2 | Log-log | 0.7230 | 0.0000 | 9e-04 | 0.8383 |
diagnostico(m1_gg)
| Supuesto | p-valor |
|---|---|
| Normalidad de residuos (Shapiro-Wilk) | 0.0000 |
| Homocedasticidad (Breusch-Pagan) | 0.0009 |
| Independencia (Durbin-Watson) | 0.2243 |
| Linealidad / forma funcional (RESET) | 0.8383 |
summary(m1_gg)
##
## Call:
## lm(formula = log(precio_millon) ~ log(Area_contruida), data = datos_sub)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.95615 -0.08911 -0.00685 0.11066 0.57023
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 1.85639 0.23369 7.944 7.33e-13 ***
## log(Area_contruida) 0.89478 0.04778 18.728 < 2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.2074 on 133 degrees of freedom
## Multiple R-squared: 0.7251, Adjusted R-squared: 0.723
## F-statistic: 350.8 on 1 and 133 DF, p-value: < 2.2e-16
Conclusión modelo 1: (compara los p-valores de Shapiro y Breusch-Pagan entre las tres versiones; escoge la que mejor cumpla supuestos. En el log-log, el coeficiente es una elasticidad: un aumento de 1% en el área se asocia con un cambio de 0.89% en el precio).
m2 <- lm(precio_millon ~ Banos, data = datos_sub)
summary(m2)
##
## Call:
## lm(formula = precio_millon ~ Banos, data = datos_sub)
##
## Residuals:
## Min 1Q Median 3Q Max
## -323.50 -110.38 -12.67 64.62 796.09
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 41.42 52.03 0.796 0.427
## Banos 160.41 15.61 10.276 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 194.8 on 133 degrees of freedom
## Multiple R-squared: 0.4426, Adjusted R-squared: 0.4384
## F-statistic: 105.6 on 1 and 133 DF, p-value: < 2.2e-16
Interpretación: cada baño adicional se asocia con un cambio esperado de 160.41 millones en el precio; R² = 0.443.
diagnostico(m2)
| Supuesto | p-valor |
|---|---|
| Normalidad de residuos (Shapiro-Wilk) | 0.0000 |
| Homocedasticidad (Breusch-Pagan) | 0.0000 |
| Independencia (Durbin-Watson) | 0.2379 |
| Linealidad / forma funcional (RESET) | 0.0000 |
# Banos puede ser 0, por eso se usa log(Banos + 1) en el log-log
m2_ll <- lm(log(precio_millon) ~ Banos, data = datos_sub)
m2_gg <- lm(log(precio_millon) ~ log(Banos + 1), data = datos_sub)
car::boxCox(m2, lambda = seq(-1, 1, 0.1))
rbind(resumen_mod(m2, "Lineal"), resumen_mod(m2_ll, "Log-lineal"), resumen_mod(m2_gg, "Log-log"))
| Modelo | R2_ajustado | Shapiro_p | BreuschPagan_p | RESET_p | |
|---|---|---|---|---|---|
| BP | Lineal | 0.4384 | 0.0000 | 0.0000 | 0.0000 |
| BP1 | Log-lineal | 0.4953 | 0.0529 | 0.0091 | 0.0535 |
| BP2 | Log-log | 0.4351 | 0.0012 | 0.2672 | 0.0001 |
summary(m2_ll)
##
## Call:
## lm(formula = log(precio_millon) ~ Banos, data = datos_sub)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.83307 -0.14870 -0.04145 0.15627 0.80176
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 5.40559 0.07476 72.30 <2e-16 ***
## Banos 0.25820 0.02243 11.51 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.2799 on 133 degrees of freedom
## Multiple R-squared: 0.4991, Adjusted R-squared: 0.4953
## F-statistic: 132.5 on 1 and 133 DF, p-value: < 2.2e-16
diagnostico(m2_ll)
| Supuesto | p-valor |
|---|---|
| Normalidad de residuos (Shapiro-Wilk) | 0.0529 |
| Homocedasticidad (Breusch-Pagan) | 0.0091 |
| Independencia (Durbin-Watson) | 0.1721 |
| Linealidad / forma funcional (RESET) | 0.0535 |
Conclusión modelo 2: (en el log-lineal, cada baño adicional cambia el precio en aproximadamente 29.5%. Decide cuál versión es mejor).
m3 <- lm(precio_millon ~ parqueaderos, data = datos_sub)
summary(m3)
##
## Call:
## lm(formula = precio_millon ~ parqueaderos, data = datos_sub)
##
## Residuals:
## Min 1Q Median 3Q Max
## -358.42 -155.92 -43.42 107.55 816.58
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -55.45 70.57 -0.786 0.433
## parqueaderos 311.94 35.32 8.832 5.26e-15 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 207.2 on 133 degrees of freedom
## Multiple R-squared: 0.3697, Adjusted R-squared: 0.3649
## F-statistic: 78 on 1 and 133 DF, p-value: 5.262e-15
Interpretación: cada parqueadero adicional se asocia con un cambio esperado de 311.94 millones en el precio; R² = 0.37.
diagnostico(m3)
| Supuesto | p-valor |
|---|---|
| Normalidad de residuos (Shapiro-Wilk) | 0.0000 |
| Homocedasticidad (Breusch-Pagan) | 0.0056 |
| Independencia (Durbin-Watson) | 0.1260 |
| Linealidad / forma funcional (RESET) | 0.0000 |
m3_ll <- lm(log(precio_millon) ~ parqueaderos, data = datos_sub)
m3_gg <- lm(log(precio_millon) ~ log(parqueaderos), data = datos_sub)
car::boxCox(m3, lambda = seq(-1, 1, 0.1))
rbind(resumen_mod(m3, "Lineal"), resumen_mod(m3_ll, "Log-lineal"), resumen_mod(m3_gg, "Log-log"))
| Modelo | R2_ajustado | Shapiro_p | BreuschPagan_p | RESET_p | |
|---|---|---|---|---|---|
| BP | Lineal | 0.3649 | 0.0000 | 0.0056 | 0.0000 |
| BP1 | Log-lineal | 0.3343 | 0.0514 | 0.4933 | 0.0124 |
| BP2 | Log-log | 0.2652 | 0.0422 | 0.0519 | 0.0000 |
diagnostico(m3_gg)
| Supuesto | p-valor |
|---|---|
| Normalidad de residuos (Shapiro-Wilk) | 0.0422 |
| Homocedasticidad (Breusch-Pagan) | 0.0519 |
| Independencia (Durbin-Watson) | 0.1409 |
| Linealidad / forma funcional (RESET) | 0.0000 |
Conclusión modelo 3: (decide si la transformación mejora los supuestos).
rbind(resumen_mod(m1, "Precio ~ Área"), resumen_mod(m1_gg, "log(Precio) ~ log(Área)"),
resumen_mod(m2, "Precio ~ Baños"), resumen_mod(m2_ll, "log(Precio) ~ Baños"),
resumen_mod(m3, "Precio ~ Parqueaderos"), resumen_mod(m3_gg, "log(Precio) ~ log(Parq.)"))
| Modelo | R2_ajustado | Shapiro_p | BreuschPagan_p | RESET_p | |
|---|---|---|---|---|---|
| BP | Precio ~ Área | 0.7084 | 0.0000 | 0.0000 | 0.0982 |
| BP1 | log(Precio) ~ log(Área) | 0.7230 | 0.0000 | 0.0009 | 0.8383 |
| BP2 | Precio ~ Baños | 0.4384 | 0.0000 | 0.0000 | 0.0000 |
| BP3 | log(Precio) ~ Baños | 0.4953 | 0.0529 | 0.0091 | 0.0535 |
| BP4 | Precio ~ Parqueaderos | 0.3649 | 0.0000 | 0.0056 | 0.0000 |
| BP5 | log(Precio) ~ log(Parq.) | 0.2652 | 0.0422 | 0.0519 | 0.0000 |
Conclusión: (el área construida suele ser el mejor predictor individual; menciona que los residuos de los modelos en escala original suelen mostrar heterocedasticidad y asimetría, y que la transformación logarítmica ayuda a corregirlo).
Para calcular el MAE de forma honesta se separa el 80% de los datos para estimar los modelos y el 20% para evaluarlos.
set.seed(123)
idx <- sample(1:nrow(datos_sub), size = round(0.8 * nrow(datos_sub)))
train <- datos_sub[idx, ]
test <- datos_sub[-idx, ]
c(entrenamiento = nrow(train), prueba = nrow(test))
## entrenamiento prueba
## 108 27
Modelo completo con la variable dummy
estrato6 (1 = estrato 6, 0 = estratos 4 y 5):
full1 <- lm(precio_millon ~ Area_contruida + Banos + Habitaciones + parqueaderos + estrato6,
data = train)
summary(full1)
##
## Call:
## lm(formula = precio_millon ~ Area_contruida + Banos + Habitaciones +
## parqueaderos + estrato6, data = train)
##
## Residuals:
## Min 1Q Median 3Q Max
## -329.65 -68.03 -3.87 67.53 478.98
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -48.6699 70.9699 -0.686 0.49441
## Area_contruida 2.5102 0.2986 8.406 2.67e-13 ***
## Banos 80.8655 20.9557 3.859 0.00020 ***
## Habitaciones -69.5479 26.0975 -2.665 0.00895 **
## parqueaderos 88.7487 33.8189 2.624 0.01002 *
## estrato6 34.1047 28.5838 1.193 0.23558
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 134.6 on 102 degrees of freedom
## Multiple R-squared: 0.778, Adjusted R-squared: 0.7671
## F-statistic: 71.48 on 5 and 102 DF, p-value: < 2.2e-16
vif(full1) # valores > 5 sugieren multicolinealidad
## Area_contruida Banos Habitaciones parqueaderos estrato6
## 2.422825 2.832540 1.941352 1.698378 1.081800
mm1 <- step(full1, direction = "both", trace = 1)
## Start: AIC=1064.78
## precio_millon ~ Area_contruida + Banos + Habitaciones + parqueaderos +
## estrato6
##
## Df Sum of Sq RSS AIC
## - estrato6 1 25804 1874670 1064.3
## <none> 1848866 1064.8
## - parqueaderos 1 124828 1973693 1069.8
## - Habitaciones 1 128729 1977594 1070.0
## - Banos 1 269915 2118780 1077.5
## - Area_contruida 1 1280728 3129594 1119.6
##
## Step: AIC=1064.28
## precio_millon ~ Area_contruida + Banos + Habitaciones + parqueaderos
##
## Df Sum of Sq RSS AIC
## <none> 1874670 1064.3
## + estrato6 1 25804 1848866 1064.8
## - Habitaciones 1 126837 2001507 1069.3
## - parqueaderos 1 140602 2015272 1070.1
## - Banos 1 283211 2157881 1077.5
## - Area_contruida 1 1293788 3168458 1119.0
summary(mm1)
##
## Call:
## lm(formula = precio_millon ~ Area_contruida + Banos + Habitaciones +
## parqueaderos, data = train)
##
## Residuals:
## Min 1Q Median 3Q Max
## -319.52 -57.19 -11.19 53.69 485.99
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -44.2313 71.0179 -0.623 0.534780
## Area_contruida 2.5216 0.2991 8.431 2.21e-13 ***
## Banos 82.6274 20.9466 3.945 0.000146 ***
## Habitaciones -69.0252 26.1475 -2.640 0.009584 **
## parqueaderos 93.5268 33.6499 2.779 0.006474 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 134.9 on 103 degrees of freedom
## Multiple R-squared: 0.7749, Adjusted R-squared: 0.7661
## F-statistic: 88.63 on 4 and 103 DF, p-value: < 2.2e-16
Interpretación: (Interpreta cada coeficiente “manteniendo las demás variables constantes”. Para la dummy: un apartamento de estrato 6 tiene en promedio (la dummy fue eliminada por step) millones más/menos que uno de estrato 4–5, con el resto igual).
vif(mm1)
## Area_contruida Banos Habitaciones parqueaderos
## 2.420318 2.818475 1.940805 1.674563
diagnostico(mm1)
| Supuesto | p-valor |
|---|---|
| Normalidad de residuos (Shapiro-Wilk) | 0.0000 |
| Homocedasticidad (Breusch-Pagan) | 0.0000 |
| Independencia (Durbin-Watson) | 0.5589 |
| Linealidad / forma funcional (RESET) | 0.6623 |
Se evalúa si conviene transformar la respuesta:
car::boxCox(full1, lambda = seq(-1, 1, 0.1))
Si el intervalo de λ incluye valores cercanos a 0, el logaritmo del precio es pertinente. Se plantea el modelo log-log (el área en logaritmo, el resto en nivel porque son conteos o dummy):
full2 <- lm(log(precio_millon) ~ log(Area_contruida) + Banos + Habitaciones +
parqueaderos + estrato6, data = train)
summary(full2)
##
## Call:
## lm(formula = log(precio_millon) ~ log(Area_contruida) + Banos +
## Habitaciones + parqueaderos + estrato6, data = train)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.74796 -0.11665 -0.00754 0.13542 0.52701
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 2.61895 0.30417 8.610 9.54e-14 ***
## log(Area_contruida) 0.68428 0.07830 8.740 4.96e-14 ***
## Banos 0.12185 0.03097 3.934 0.000153 ***
## Habitaciones -0.13040 0.03768 -3.461 0.000788 ***
## parqueaderos 0.11571 0.04701 2.461 0.015519 *
## estrato6 0.06827 0.04143 1.648 0.102411
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.1951 on 102 degrees of freedom
## Multiple R-squared: 0.7867, Adjusted R-squared: 0.7762
## F-statistic: 75.22 on 5 and 102 DF, p-value: < 2.2e-16
mm2 <- step(full2, direction = "both", trace = 1,
scope = list(lower = ~ estrato6, upper = formula(full2)))
## Start: AIC=-347.12
## log(precio_millon) ~ log(Area_contruida) + Banos + Habitaciones +
## parqueaderos + estrato6
##
## Df Sum of Sq RSS AIC
## <none> 3.8845 -347.12
## - parqueaderos 1 0.23072 4.1152 -342.88
## - Habitaciones 1 0.45609 4.3405 -337.13
## - Banos 1 0.58944 4.4739 -333.86
## - log(Area_contruida) 1 2.90877 6.7932 -288.75
summary(mm2)
##
## Call:
## lm(formula = log(precio_millon) ~ log(Area_contruida) + Banos +
## Habitaciones + parqueaderos + estrato6, data = train)
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.74796 -0.11665 -0.00754 0.13542 0.52701
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 2.61895 0.30417 8.610 9.54e-14 ***
## log(Area_contruida) 0.68428 0.07830 8.740 4.96e-14 ***
## Banos 0.12185 0.03097 3.934 0.000153 ***
## Habitaciones -0.13040 0.03768 -3.461 0.000788 ***
## parqueaderos 0.11571 0.04701 2.461 0.015519 *
## estrato6 0.06827 0.04143 1.648 0.102411
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.1951 on 102 degrees of freedom
## Multiple R-squared: 0.7867, Adjusted R-squared: 0.7762
## F-statistic: 75.22 on 5 and 102 DF, p-value: < 2.2e-16
Interpretación: (el coeficiente de log(Área) es una elasticidad. Para variables en nivel, el efecto porcentual es (exp(β) − 1) × 100. Para la dummy: 7.1% de diferencia en el precio de los estrato 6 frente a 4–5, con el resto constante).
vif(mm2)
## log(Area_contruida) Banos Habitaciones parqueaderos
## 2.588942 2.944782 1.926244 1.561912
## estrato6
## 1.081472
diagnostico(mm2)
| Supuesto | p-valor |
|---|---|
| Normalidad de residuos (Shapiro-Wilk) | 0.0356 |
| Homocedasticidad (Breusch-Pagan) | 0.0085 |
| Independencia (Durbin-Watson) | 0.5639 |
| Linealidad / forma funcional (RESET) | 0.2298 |
car::outlierTest(mm2)
## rstudent unadjusted p-value Bonferroni p
## 32 -4.306837 3.8455e-05 0.0041532
rbind(resumen_mod(mm1, "Múltiple 1 (precio)"), resumen_mod(mm2, "Múltiple 2 (log-precio)"))
| Modelo | R2_ajustado | Shapiro_p | BreuschPagan_p | RESET_p | |
|---|---|---|---|---|---|
| BP | Múltiple 1 (precio) | 0.7661 | 0.0000 | 0.0000 | 0.6623 |
| BP1 | Múltiple 2 (log-precio) | 0.7762 | 0.0356 | 0.0085 | 0.2298 |
Conclusión: (¿la transformación logarítmica mejoró normalidad y homocedasticidad? Los R² ajustados de modelos con distinta variable respuesta no son directamente comparables; por eso se usa el MAE en la escala original en la sección 5).
Se predice el precio de un apartamento hipotético de 150 m², 3 baños, 3 habitaciones, 2 parqueaderos y estrato 6 (puedes cambiar los valores).
nuevo <- data.frame(Area_contruida = 150, Banos = 3, Habitaciones = 3,
parqueaderos = 2, estrato6 = 1)
# Modelo 1 (escala original)
p1 <- predict(mm1, newdata = nuevo, interval = "prediction", level = 0.95)
# Modelo 2 (escala log): se devuelve a millones con exp()
p2 <- exp(predict(mm2, newdata = nuevo, interval = "prediction", level = 0.95))
data.frame(Modelo = c("Múltiple 1", "Múltiple 2 (log)"),
Prediccion_millones = round(c(p1[, "fit"], p2[, "fit"]), 1),
Limite_inferior = round(c(p1[, "lwr"], p2[, "lwr"]), 1),
Limite_superior = round(c(p1[, "upr"], p2[, "upr"]), 1))
| Modelo | Prediccion_millones | Limite_inferior | Limite_superior |
|---|---|---|---|
| Múltiple 1 | 561.9 | 292.9 | 830.8 |
| Múltiple 2 (log) | 556.5 | 376.5 | 822.5 |
Interpretación: (un apartamento con esas características se estima en X millones; con 95% de confianza el precio individual estaría entre los límites. Compara ambos modelos y comenta si el intervalo del modelo 1 tiene sentido, por ejemplo si el límite inferior es muy bajo).
El error absoluto medio se calcula sobre el conjunto de prueba, en millones de pesos:
\[MAE = \frac{1}{n}\sum_{i=1}^{n}|y_i - \hat{y}_i|\]
mae <- function(real, pred) mean(abs(real - pred))
pred1 <- predict(mm1, newdata = test)
pred2 <- exp(predict(mm2, newdata = test)) # regreso a la escala original
tabla_mae <- data.frame(
Modelo = c("Múltiple 1 (precio)", "Múltiple 2 (log-precio)"),
MAE_millones = round(c(mae(test$precio_millon, pred1),
mae(test$precio_millon, pred2)), 2),
Precio_promedio_prueba = round(mean(test$precio_millon), 1))
tabla_mae$Error_relativo_pct <- round(100 * tabla_mae$MAE_millones / tabla_mae$Precio_promedio_prueba, 1)
tabla_mae
| Modelo | MAE_millones | Precio_promedio_prueba | Error_relativo_pct |
|---|---|---|---|
| Múltiple 1 (precio) | 71.66 | 513.9 | 13.9 |
| Múltiple 2 (log-precio) | 55.30 | 513.9 | 10.8 |
Interpretación: (en promedio, el modelo se equivoca en X millones respecto al precio real, lo que equivale a Y% del precio promedio. El modelo con menor MAE predice mejor).