Introducción

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).

1. Datos y filtro

# 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

Exploración rápida

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

2. Modelación simple

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.

Funciones auxiliares

# 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.

Modelo simple 1: precio ~ área construida

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).

Modelo simple 2: precio ~ número de baños

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).

Modelo simple 3: precio ~ parqueaderos

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).

Comparación de los tres modelos simples

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).

3. Modelación múltiple

3.1 Partición entrenamiento / prueba

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

3.2 Modelo múltiple 1: precio en escala original

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

Selección de variables con stepwise (AIC)

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).

Validación de supuestos

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

3.3 Modelo múltiple 2: transformación logarítmica

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

Selección con stepwise (se obliga a conservar la dummy)

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).

Validación de supuestos

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

3.4 Comparación de supuestos entre modelos

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).

4. Predicción hipotética

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).

5. Métrica de evaluación: MAE

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).