Introducción

En este informe se analiza la oferta de vivienda usada publicada en OLX para el barrio Pance de Cali. El objetivo es explicar el precio de venta (en millones de pesos) a partir de las características del inmueble, mediante modelos de regresión lineal simple y múltiple, validando los supuestos del modelo, evaluando la pertinencia de transformaciones logarítmicas, seleccionando variables con stepwise y midiendo la capacidad predictiva con el MAE.

Nota sobre el filtro: la base se filtra únicamente por barrio (Pance). No se filtra por tipo de vivienda, por lo que el barrio incluye casas y apartamentos; esto permite usar el tipo de vivienda como variable dummy en la modelación múltiple.

1. Preparación de los datos

Paso 1. Cargar librerías

# Si alguna librería no está instalada: install.packages("nombre")
library(readxl)
library(dplyr)
library(ggplot2)
library(plotly)
library(knitr)
library(tidyr)
library(leaflet)
library(lmtest)   # bptest (Breusch-Pagan)

Paso 2. Cargar la base y filtrar por el barrio Pance

El archivo Datos_Vivienda.xlsx debe estar en la misma carpeta que este .Rmd.

datos <- read_excel("Datos_Vivienda.xlsx", na = c("", "NA"),
                    col_types = c("text", "text", "text",
                                  "numeric", "numeric", "numeric",
                                  "numeric", "numeric", "text", "text",
                                  "numeric", "numeric"))

# Identificador único por fila
datos <- data.frame(id = 1:nrow(datos), datos)

# Filtro SOLO por barrio: se ignoran mayúsculas/minúsculas y espacios sobrantes
datos_sub <- datos %>%
  filter(tolower(trimws(Barrio)) == "pance")

cat("Registros totales en la base:", nrow(datos), "\n")
## Registros totales en la base: 8319
cat("Registros del barrio Pance:", nrow(datos_sub), "\n")
## Registros del barrio Pance: 412
head(datos_sub)
id Zona piso Estrato precio_millon Area_contruida parqueaderos Banos Habitaciones Tipo Barrio cordenada_longitud Cordenada_latitud
1 Zona Sur 2 6 880 237 2 5 4 Casa pance -76.46300 3.43000
5 Zona Sur 2 6 1300 600 4 7 5 Casa pance -76.46438 3.43463
6 Zona Sur 3 6 513 160 2 4 4 Casa pance -76.46438 3.43463
10 Zona Sur 6 6 690 150 2 5 4 Apartamento pance -76.46478 3.42783
19 Zona Sur 3 6 550 136 2 4 4 Apartamento pance -76.46554 3.42933
33 Zona Sur 1 6 900 330 2 4 4 Casa pance -76.46809 3.41664

Criterio de mínimo 30 observaciones. Para que la regresión sea confiable, el barrio elegido debe tener al menos 30 ofertas. Se cuenta cuántas tiene cada barrio (unificando mayúsculas y espacios):

conteo_barrios <- datos %>%
  mutate(Barrio_limpio = tolower(trimws(Barrio))) %>%
  count(Barrio_limpio, name = "observaciones") %>%
  arrange(desc(observaciones))

head(conteo_barrios, 5)
Barrio_limpio observaciones
valle del lili 1009
ciudad jardin 540
pance 412
la flora 368
santa teresita 263
cat("Barrios con 30 o más observaciones:", sum(conteo_barrios$observaciones >= 30), "\n")
## Barrios con 30 o más observaciones: 59
cat("Observaciones de Pance:", conteo_barrios$observaciones[conteo_barrios$Barrio_limpio == "pance"], "\n")
## Observaciones de Pance: 412

Interpretación. Pance es el tercer barrio con más ofertas (412, después de Valle del Lili y Ciudad Jardín), muy por encima del mínimo de 30, y es uno de los 59 barrios que cumplen ese criterio. Por eso es adecuado para estimar los modelos.

str(datos_sub)
## 'data.frame':    412 obs. of  13 variables:
##  $ id                : int  1 5 6 10 19 33 35 39 44 46 ...
##  $ Zona              : chr  "Zona Sur" "Zona Sur" "Zona Sur" "Zona Sur" ...
##  $ piso              : chr  "2" "2" "3" "6" ...
##  $ Estrato           : chr  "6" "6" "6" "6" ...
##  $ precio_millon     : num  880 1300 513 690 550 900 570 510 1200 550 ...
##  $ Area_contruida    : num  237 600 160 150 136 330 137 148 211 143 ...
##  $ parqueaderos      : num  2 4 2 2 2 2 2 2 2 3 ...
##  $ Banos             : num  5 7 4 5 4 4 5 3 3 3 ...
##  $ Habitaciones      : num  4 5 4 4 4 4 4 3 3 3 ...
##  $ Tipo              : chr  "Casa" "Casa" "Casa" "Apartamento" ...
##  $ Barrio            : chr  "pance" "pance" "pance" "pance" ...
##  $ 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 ...
table(datos_sub$Tipo)
## 
## Apartamento        Casa 
##         206         206
colSums(is.na(datos_sub))
##                 id               Zona               piso            Estrato 
##                  0                  0                130                  0 
##      precio_millon     Area_contruida       parqueaderos              Banos 
##                  0                  0                 12                  0 
##       Habitaciones               Tipo             Barrio cordenada_longitud 
##                  0                  0                  0                  0 
##  Cordenada_latitud 
##                  0

Observación sobre datos faltantes: piso tiene 130 valores faltantes (78 casas y 52 apartamentos), por eso no se usa como variable explicativa. (En la base original esos faltantes venían escritos como el texto “NA”; por eso la lectura del Excel usa na = c("", "NA"), para que colSums(is.na()) los cuente bien.) parqueaderos tiene 12 faltantes; en los modelos que la usan, lm() omite esas filas automáticamente (de 412 ofertas quedan 400). Además, hay 2 registros con 0 baños y 2 con 0 habitaciones (3 filas en total, una tiene ambos en 0): pueden ser errores de digitación, y son la razón por la que no se aplica logaritmo a esas variables.

2. Análisis exploratorio

Indicadores de centralidad y variabilidad

resumen <- data.frame(
  cantidad_ofertas      = nrow(datos_sub),
  promedio_precio       = mean(datos_sub$precio_millon, na.rm = TRUE),
  promedio_area         = mean(datos_sub$Area_contruida, na.rm = TRUE),
  promedio_habitaciones = mean(datos_sub$Habitaciones, na.rm = TRUE),
  promedio_banos        = mean(datos_sub$Banos, na.rm = TRUE),
  promedio_parqueaderos = mean(datos_sub$parqueaderos, na.rm = TRUE)
)
resumen
cantidad_ofertas promedio_precio promedio_area promedio_habitaciones promedio_banos promedio_parqueaderos
412 884.3883 277.6757 3.696602 4.473301 3.01
indic_precio <- data.frame(
  promedio = mean(datos_sub$precio_millon, na.rm = TRUE),
  mediana  = median(datos_sub$precio_millon, na.rm = TRUE),
  minimo   = min(datos_sub$precio_millon, na.rm = TRUE),
  maximo   = max(datos_sub$precio_millon, na.rm = TRUE),
  desv_est = sd(datos_sub$precio_millon, na.rm = TRUE)
)
indic_precio
promedio mediana minimo maximo desv_est
884.3883 780 245 1900 389.766
datos_sub %>%
  group_by(Tipo) %>%
  summarise(n = n(),
            precio_promedio = mean(precio_millon),
            precio_mediana  = median(precio_millon),
            area_promedio   = mean(Area_contruida))
Tipo n precio_promedio precio_mediana area_promedio
Apartamento 206 646.3058 600 153.3931
Casa 206 1122.4709 1125 401.9583

Histogramas

ggplotly(
  ggplot(datos_sub, aes(x = precio_millon)) +
    geom_histogram(binwidth = 100, fill = "steelblue", color = "black", alpha = 0.6) +
    labs(title = "Figura 1. Histograma del precio de las viviendas en Pance",
         x = "Precio (millones)", y = "Frecuencia") +
    theme_minimal()
)

Interpretación Figura 1. La distribución del precio es asimétrica a la derecha: la mayoría de las ofertas está por debajo de 900 millones (mediana = 780; Q1 = 590) y hay una cola de inmuebles de lujo que llega a 1.900 millones. Como el promedio (884) es mayor que la mediana (780), esos precios altos “jalan” el promedio hacia arriba. También se nota un segundo grupo de ofertas alrededor de 1.200 a 1.350 millones (casas grandes). Esta asimetría anticipa que una transformación logarítmica del precio podría ayudar a estabilizar la varianza y a acercar los residuos a la normalidad.

ggplotly(
  ggplot(datos_sub, aes(x = Area_contruida)) +
    geom_histogram(binwidth = 50, fill = "green", color = "black", alpha = 0.6) +
    labs(title = "Figura 2. Histograma del área construida en Pance",
         x = "Área construida (m²)", y = "Frecuencia") +
    theme_minimal()
)

Interpretación Figura 2. El área construida también es asimétrica a la derecha: la mediana es 199 m² (Q1 = 138 y Q3 = 350) y el promedio 278 m², más alto por la cola derecha. Hay 17 viviendas por encima de 650 m² (límite superior del boxplot del área), que son valores atípicos; la más grande tiene 1.500 m².

Boxplots

ggplotly(
  ggplot(datos_sub, aes(x = Tipo, y = precio_millon, fill = Tipo)) +
    geom_boxplot(outlier.colour = "red", outlier.size = 2) +
    labs(title = "Figura 3. Precio por tipo de vivienda en Pance",
         x = "Tipo de vivienda", y = "Precio (millones)") +
    theme_minimal() + theme(legend.position = "none")
)

Interpretación Figura 3. Las casas son bastante más caras que los apartamentos: mediana de ~1.125 millones frente a 600 (promedio 1.122 vs 646). El 50% central de las casas está entre 850 y 1.350 millones, y el de los apartamentos entre 500 y ~708 millones: las casas, además, son más dispersas. Esta diferencia justifica incluir el tipo de vivienda como variable dummy más adelante.

ggplotly(
  ggplot(datos_sub, aes(x = Tipo, y = Area_contruida, fill = Tipo)) +
    geom_boxplot(outlier.colour = "red", outlier.size = 2) +
    labs(title = "Figura 4. Área construida por tipo de vivienda en Pance",
         x = "Tipo de vivienda", y = "Área construida (m²)") +
    theme_minimal() + theme(legend.position = "none")
)

Interpretación Figura 4. Las casas son mucho más grandes que los apartamentos (mediana de ~348 m² frente a 138 m²). Los puntos rojos son atípicos dentro de cada tipo: 15 apartamentos (hasta 520 m²) y 14 casas (hasta 1.500 m²). Parte de la diferencia de precio entre casas y apartamentos se explica simplemente por el tamaño.

Relación bivariada precio - área

r_pa <- cor(datos_sub$precio_millon, datos_sub$Area_contruida, use = "complete.obs")

ggplotly(
  ggplot(datos_sub, aes(x = Area_contruida, y = precio_millon, color = Tipo)) +
    geom_point(alpha = 0.7, size = 2) +
    geom_smooth(aes(group = 1), method = "loess", color = "grey30", se = TRUE) +
    labs(title = paste0("Figura 5. Precio vs área construida (correlación = ", round(r_pa, 2), ")"),
         x = "Área construida (m²)", y = "Precio (millones)") +
    theme_minimal()
)

Interpretación Figura 5. La correlación entre precio y área es de 0.7, una relación positiva y fuerte (supera el ±0.5 recomendado): a mayor área, mayor precio. La curva suavizada muestra que la relación no es perfectamente lineal (el precio crece más lento a medida que el área aumenta) y que la dispersión crece con el tamaño, lo que sugiere explorar una transformación logarítmica. Una correlación alta no implica causalidad: la ubicación dentro del barrio, los acabados y la oferta también influyen.

Matriz de correlaciones

vars_num <- datos_sub %>% select(precio_millon, Area_contruida, parqueaderos, Banos, Habitaciones)
round(cor(vars_num, use = "pairwise.complete.obs"), 2)
##                precio_millon Area_contruida parqueaderos Banos Habitaciones
## precio_millon           1.00           0.70         0.63  0.57         0.43
## Area_contruida          0.70           1.00         0.54  0.48         0.42
## parqueaderos            0.63           0.54         1.00  0.45         0.37
## Banos                   0.57           0.48         0.45  1.00         0.64
## Habitaciones            0.43           0.42         0.37  0.64         1.00

El precio se relaciona más fuertemente con el área construida (0.70), seguida de parqueaderos (0.63) y baños (0.57); las tres superan el ±0.5 recomendado. Habitaciones (0.43) queda por debajo de ese umbral. Todas las relaciones con el precio son positivas, como se esperaba. Baños y habitaciones están correlacionadas entre sí (0.64), lo que puede causar problemas de multicolinealidad en los modelos múltiples.

Representación espacial

leaflet() %>%
  addTiles() %>%
  addCircleMarkers(lng = datos_sub$cordenada_longitud, lat = datos_sub$Cordenada_latitud,
                   radius = 2, color = "purple",
                   label = paste0("ID: ", datos_sub$id,
                                  ", Tipo: ", datos_sub$Tipo,
                                  ", Precio: ", datos_sub$precio_millon,
                                  ", Área: ", datos_sub$Area_contruida))

3. Modelación simple

Para validar cada modelo se usa la siguiente función, que produce los gráficos de diagnóstico y las pruebas formales de los supuestos:

  • Linealidad / media cero: gráfico Residuals vs Fitted (se espera una nube plana alrededor de cero).
  • Normalidad: gráfico Normal Q-Q y prueba de Shapiro-Wilk (H0: los residuos son normales).
  • Homocedasticidad: gráfico Scale-Location y prueba de Breusch-Pagan (H0: varianza constante).
  • Observaciones influyentes: gráfico Residuals vs Leverage.

Con un nivel de significancia de 5%, se rechaza H0 si el valor p < 0.05.

validar <- function(modelo, titulo = "") {
  op <- par(mfrow = c(2, 2))
  plot(modelo)   # Residuals vs Fitted, Normal Q-Q, Scale-Location y Residuals vs Leverage
  par(op)

  res <- data.frame(
    Prueba = c("Shapiro-Wilk (normalidad)", "Breusch-Pagan (homocedasticidad)"),
    Valor_p = c(shapiro.test(resid(modelo))$p.value,
                bptest(modelo)$p.value)
  )
  res$Decision <- ifelse(res$Valor_p < 0.05, "Se rechaza H0", "No se rechaza H0")
  res$Valor_p <- signif(res$Valor_p, 3)
  res
}

# Extrae el valor p de una prueba para usarlo en el texto
pval <- function(tabla, i) tabla$Valor_p[i]
decide <- function(p) ifelse(p < 0.05, "se rechaza", "no se rechaza")
# Valor p con formato legible para el texto (ej. 7.24e-14)
fp <- function(tabla, i) formatC(tabla$Valor_p[i], format = "g", digits = 3)

Se plantean tres modelos simples, cada uno con una variable explicativa distinta: área construida, número de parqueaderos y número de baños. Para cada uno se estima el modelo en escala original y luego una versión logarítmica, y se compara cuál cumple mejor los supuestos.

3.0 Planteamiento: relación y signos esperados

Siguiendo la metodología, antes de estimar se valida que las variables estén relacionadas con el precio (matriz de correlaciones de la sección 2: área 0.70, parqueaderos 0.63 y baños 0.57, todas por encima de ±0.5) y se plantea el signo esperado de cada coeficiente:

Variable Signo esperado Razón
Área construida + más metros, mayor precio
Parqueaderos + en Pance los parqueaderos son un atributo valorado
Baños + más comodidad, mayor precio
Habitaciones + más espacio, mayor precio
Casa (dummy) + las casas suelen ser más grandes y más caras que los apartamentos

Después de estimar cada modelo se compara el signo obtenido con el esperado y se verifica la significancia del coeficiente (valor p < 0.05).

3.1 Modelo simple 1: precio ~ área construida

a) Modelo en escala original

ms1 <- lm(precio_millon ~ Area_contruida, data = datos_sub)
summary(ms1)
## 
## Call:
## lm(formula = precio_millon ~ Area_contruida, data = datos_sub)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -1034.71  -166.49   -59.46   103.04   938.22 
## 
## Coefficients:
##                 Estimate Std. Error t value Pr(>|t|)    
## (Intercept)    509.48364   23.19634   21.96   <2e-16 ***
## Area_contruida   1.35015    0.06748   20.01   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 277.6 on 410 degrees of freedom
## Multiple R-squared:  0.4941, Adjusted R-squared:  0.4928 
## F-statistic: 400.4 on 1 and 410 DF,  p-value: < 2.2e-16
v1 <- validar(ms1, "(MS1)")

v1
Prueba Valor_p Decision
Shapiro-Wilk (normalidad) 0 Se rechaza H0
BP Breusch-Pagan (homocedasticidad) 0 Se rechaza H0

Interpretación. La ecuación estimada es: precio = 509.48 + 1.35 · área. Por cada metro cuadrado adicional de área construida, el precio esperado aumenta en 1.35 millones de pesos. El R² es 0.494: el área explica aproximadamente ese porcentaje de la variabilidad del precio. El coeficiente es significativo (valor p = 1.22e-62) y su signo positivo coincide con el esperado.

Validación de supuestos. Normalidad: se rechaza H0 (valor p = 7.89e-15); homocedasticidad: se rechaza H0 (valor p = 1.02e-24). En los gráficos, los residuos contra los ajustados no son planos: se abren como un abanico (la dispersión crece con el valor ajustado) y el QQ-plot se separa de la recta en las colas. El modelo en escala original no cumple normalidad ni varianza constante, lo que motiva probar una transformación.

b) Transformación logarítmica (log-log)

ms1_log <- lm(log(precio_millon) ~ log(Area_contruida), data = datos_sub)
summary(ms1_log)
## 
## Call:
## lm(formula = log(precio_millon) ~ log(Area_contruida), data = datos_sub)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -0.82274 -0.15856  0.01006  0.13280  0.62845 
## 
## Coefficients:
##                     Estimate Std. Error t value Pr(>|t|)    
## (Intercept)          3.34073    0.10044   33.26   <2e-16 ***
## log(Area_contruida)  0.61698    0.01838   33.57   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.2228 on 410 degrees of freedom
## Multiple R-squared:  0.7332, Adjusted R-squared:  0.7326 
## F-statistic:  1127 on 1 and 410 DF,  p-value: < 2.2e-16
v1l <- validar(ms1_log, "(MS1 log-log)")

v1l
Prueba Valor_p Decision
Shapiro-Wilk (normalidad) 0.000824 Se rechaza H0
BP Breusch-Pagan (homocedasticidad) 0.000000 Se rechaza H0

Interpretación. En el modelo log-log el coeficiente es una elasticidad: un aumento de 1% en el área construida se asocia con un aumento de aproximadamente 0.617% en el precio. El R² pasa de 0.494 (escala original) a 0.733.

Pertinencia de la transformación. Con el log-log, Breusch-Pagan pasa de p = 1.02e-24 a p = 2.69e-13 y Shapiro-Wilk de p = 7.89e-15 a p = 0.000824: ambos valores p suben (mejoran) y el R² pasa de 0.494 a 0.733, pero las dos pruebas siguen rechazando H0. Conclusión: la transformación sí es pertinente (mejora claramente el ajuste y los supuestos) aunque no los resuelve del todo; falta incluir más variables, lo que se hace en la modelación múltiple. Nota: el R² de modelos con variable respuesta distinta (precio vs log precio) no es directamente comparable; por eso la comparación se apoya en los supuestos y en el error de predicción (MAE, sección 4.6).

Se comparan además las cuatro formas funcionales posibles para el área (lin-lin, log-lin, lin-log y log-log):

ms1_loglin <- lm(log(precio_millon) ~ Area_contruida, data = datos_sub)
ms1_linlog <- lm(precio_millon ~ log(Area_contruida), data = datos_sub)
formas <- list("Lin-lin" = ms1, "Log-lin" = ms1_loglin, "Lin-log" = ms1_linlog, "Log-log" = ms1_log)
data.frame(
  Forma = names(formas),
  R2 = round(sapply(formas, function(m) summary(m)$r.squared), 3),
  p_Shapiro = signif(sapply(formas, function(m) shapiro.test(resid(m))$p.value), 3),
  p_BreuschPagan = signif(sapply(formas, function(m) bptest(m)$p.value), 3),
  row.names = NULL
)
Forma R2 p_Shapiro p_BreuschPagan
Lin-lin 0.494 0.00e+00 0
Log-lin 0.491 4.41e-05 0
Lin-log 0.674 0.00e+00 0
Log-log 0.733 8.24e-04 0

Interpretación. Transformar solo la variable respuesta (log-lin) casi no cambia el ajuste (R² 0.491 vs 0.494). Transformar el área (lin-log) sube el R² a 0.674, y transformar ambas variables (log-log) es la mejor opción: R² = 0.733 y los valores p más altos en Shapiro-Wilk y Breusch-Pagan entre las cuatro formas. Por eso la forma log-log es la elegida.

3.2 Modelo simple 2: precio ~ parqueaderos

a) Modelo en escala original

ms2 <- lm(precio_millon ~ parqueaderos, data = datos_sub)
summary(ms2)
## 
## Call:
## lm(formula = precio_millon ~ parqueaderos, data = datos_sub)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -862.69 -190.31  -80.31  151.60 1189.69 
## 
## Coefficients:
##              Estimate Std. Error t value Pr(>|t|)    
## (Intercept)    372.21      34.73   10.72   <2e-16 ***
## parqueaderos   169.05      10.39   16.27   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 302.5 on 398 degrees of freedom
##   (12 observations deleted due to missingness)
## Multiple R-squared:  0.3996, Adjusted R-squared:  0.398 
## F-statistic: 264.8 on 1 and 398 DF,  p-value: < 2.2e-16
v2 <- validar(ms2, "(MS2)")

v2
Prueba Valor_p Decision
Shapiro-Wilk (normalidad) 0.00000 Se rechaza H0
BP Breusch-Pagan (homocedasticidad) 0.00742 Se rechaza H0

Interpretación. Cada parqueadero adicional se asocia con un aumento promedio de 169.05 millones en el precio. El modelo explica el 40% de la variabilidad del precio. En los supuestos: normalidad se rechaza H0 (p = 7.24e-14), homocedasticidad se rechaza H0 (p = 0.00742). El coeficiente es significativo (valor p = 5.19e-46) y su signo positivo coincide con el esperado.

b) Transformación logarítmica

parqueaderos toma valores desde 1, por lo que se puede aplicar log a ambas variables (log-log).

ms2_log <- lm(log(precio_millon) ~ log(parqueaderos), data = datos_sub)
summary(ms2_log)
## 
## Call:
## lm(formula = log(precio_millon) ~ log(parqueaderos), data = datos_sub)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -0.9805 -0.1949 -0.0382  0.1903  1.2209 
## 
## Coefficients:
##                   Estimate Std. Error t value Pr(>|t|)    
## (Intercept)        6.02334    0.04176  144.23   <2e-16 ***
## log(parqueaderos)  0.66135    0.03825   17.29   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.3258 on 398 degrees of freedom
##   (12 observations deleted due to missingness)
## Multiple R-squared:  0.4289, Adjusted R-squared:  0.4275 
## F-statistic:   299 on 1 and 398 DF,  p-value: < 2.2e-16
v2l <- validar(ms2_log, "(MS2 log-log)")

v2l
Prueba Valor_p Decision
Shapiro-Wilk (normalidad) 2.40e-06 Se rechaza H0
BP Breusch-Pagan (homocedasticidad) 9.61e-04 Se rechaza H0

Interpretación. Un aumento de 1% en el número de parqueaderos se asocia con un cambio de aproximadamente 0.661% en el precio (elasticidad). El R² pasa de 0.4 a 0.429. Breusch-Pagan pasa de p = 0.00742 a p = 0.000961 (el valor p baja, es decir, la homocedasticidad no mejora; ambos modelos la rechazan) y Shapiro-Wilk pasa de p = 7.24e-14 a p = 2.36e-06 (mejora, pero sigue rechazando). Conclusión: la transformación no es claramente pertinente para parqueaderos: mejora un poco la normalidad y el R², pero no corrige la varianza no constante. Además, el número de parqueaderos es una variable discreta con pocos valores, por lo que los residuos siempre muestran “escalones”.

3.3 Modelo simple 3: precio ~ baños

a) Modelo en escala original

ms3 <- lm(precio_millon ~ Banos, data = datos_sub)
summary(ms3)
## 
## Call:
## lm(formula = precio_millon ~ Banos, data = datos_sub)
## 
## Residuals:
##    Min     1Q Median     3Q    Max 
## -613.1 -218.6  -90.0  138.5 1127.7 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept)    72.32      60.27    1.20    0.231    
## Banos         181.54      13.00   13.96   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 321.2 on 410 degrees of freedom
## Multiple R-squared:  0.3223, Adjusted R-squared:  0.3207 
## F-statistic:   195 on 1 and 410 DF,  p-value: < 2.2e-16
v3 <- validar(ms3, "(MS3)")

v3
Prueba Valor_p Decision
Shapiro-Wilk (normalidad) 0.000 Se rechaza H0
BP Breusch-Pagan (homocedasticidad) 0.567 No se rechaza H0

Interpretación. Cada baño adicional se asocia con un aumento promedio de 181.54 millones en el precio, con un R² de 0.322. Supuestos: normalidad se rechaza H0 (p = 2.08e-14), homocedasticidad no se rechaza H0 (p = 0.567). El coeficiente es significativo (valor p = 1.57e-36) y su signo positivo coincide con el esperado.

b) Transformación logarítmica

La base tiene registros con 0 baños, y log(0) no está definido; por eso solo se transforma la variable respuesta (modelo log-lineal: log(precio) ~ baños).

ms3_log <- lm(log(precio_millon) ~ Banos, data = datos_sub)
summary(ms3_log)
## 
## Call:
## lm(formula = log(precio_millon) ~ Banos, data = datos_sub)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -0.82930 -0.24398 -0.04321  0.21452  1.38347 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  5.70660    0.06329   90.17   <2e-16 ***
## Banos        0.22032    0.01365   16.14   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.3374 on 410 degrees of freedom
## Multiple R-squared:  0.3885, Adjusted R-squared:  0.387 
## F-statistic: 260.5 on 1 and 410 DF,  p-value: < 2.2e-16
v3l <- validar(ms3_log, "(MS3 log-lineal)")

v3l
Prueba Valor_p Decision
Shapiro-Wilk (normalidad) 0.000000 Se rechaza H0
BP Breusch-Pagan (homocedasticidad) 0.000542 Se rechaza H0

Interpretación. En un modelo log-lineal, cada baño adicional se asocia con un aumento porcentual aproximado de 24.6% en el precio. El R² pasa de 0.322 a 0.388. Pero Breusch-Pagan pasa de p = 0.567 a p = 0.000542: el modelo en escala original sí cumplía homocedasticidad y el log-lineal deja de cumplirla. Shapiro-Wilk mejora (de p = 2.08e-14 a p = 2.8e-08) pero sigue rechazando. Conclusión: para baños la transformación no es pertinente por supuestos: solo mejora el R² y la normalidad parcialmente, y empeora la varianza constante. Si se prioriza cumplir los supuestos, es preferible el modelo en escala original.

3.4 Resumen de los modelos simples

data.frame(
  Modelo = c("MS1: precio ~ área", "MS1 log-log", "MS2: precio ~ parqueaderos", "MS2 log-log",
             "MS3: precio ~ baños", "MS3 log-lineal"),
  R2 = round(c(summary(ms1)$r.squared, summary(ms1_log)$r.squared,
               summary(ms2)$r.squared, summary(ms2_log)$r.squared,
               summary(ms3)$r.squared, summary(ms3_log)$r.squared), 3),
  p_Shapiro = c(pval(v1,1), pval(v1l,1), pval(v2,1), pval(v2l,1), pval(v3,1), pval(v3l,1)),
  p_BreuschPagan = c(pval(v1,2), pval(v1l,2), pval(v2,2), pval(v2l,2), pval(v3,2), pval(v3l,2))
)
Modelo R2 p_Shapiro p_BreuschPagan
MS1: precio ~ área 0.494 0.00e+00 0.000000
MS1 log-log 0.733 8.24e-04 0.000000
MS2: precio ~ parqueaderos 0.400 0.00e+00 0.007420
MS2 log-log 0.429 2.40e-06 0.000961
MS3: precio ~ baños 0.322 0.00e+00 0.567000
MS3 log-lineal 0.388 0.00e+00 0.000542

Conclusión de la modelación simple. El área construida es la variable que más explica el precio (R² de 0.49 en escala original y 0.73 en log-log), seguida de parqueaderos (0.40) y baños (0.32). La transformación logarítmica es pertinente solo para el área, donde mejora el ajuste y los valores p de los supuestos. Para parqueaderos no corrige la heterocedasticidad y para baños la empeora (el modelo original sí la cumplía). En ningún modelo simple se cumple la normalidad de los residuos (con n ≈ 400 es esperable que Shapiro-Wilk sea estricto). Los modelos simples dejan una parte importante de la variabilidad sin explicar, lo que justifica pasar a modelos múltiples.

4. Modelación múltiple

4.1 Variable dummy

Se crea una variable dummy para el tipo de vivienda: casa = 1 si es casa y 0 si es apartamento (categoría de referencia).

datos_sub <- datos_sub %>%
  mutate(casa = ifelse(Tipo == "Casa", 1, 0))

table(datos_sub$Tipo, datos_sub$casa)
##              
##                 0   1
##   Apartamento 206   0
##   Casa          0 206
# Base para la modelación múltiple: casos completos en las variables a usar
datos_mod <- datos_sub %>%
  select(id, precio_millon, Area_contruida, Banos, Habitaciones, parqueaderos, casa) %>%
  na.omit()

cat("Observaciones usadas en la modelación múltiple:", nrow(datos_mod), "\n")
## Observaciones usadas en la modelación múltiple: 400

4.2 Modelo múltiple 1 (escala original)

precio = β0 + β1·área + β2·baños + β3·habitaciones + β4·parqueaderos + β5·casa + ε

mm1 <- lm(precio_millon ~ Area_contruida + Banos + Habitaciones + parqueaderos + casa,
          data = datos_mod)
summary(mm1)
## 
## Call:
## lm(formula = precio_millon ~ Area_contruida + Banos + Habitaciones + 
##     parqueaderos + casa, data = datos_mod)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -803.81 -125.30  -38.79   70.71  943.20 
## 
## Coefficients:
##                Estimate Std. Error t value Pr(>|t|)    
## (Intercept)    120.6271    59.2991   2.034  0.04260 *  
## Area_contruida   0.6929     0.0794   8.727  < 2e-16 ***
## Banos           83.5910    14.3011   5.845 1.06e-08 ***
## Habitaciones   -20.6435    18.3494  -1.125  0.26126    
## parqueaderos    70.9328    10.1308   7.002 1.10e-11 ***
## casa           109.2961    33.6395   3.249  0.00126 ** 
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 235.2 on 394 degrees of freedom
## Multiple R-squared:  0.6405, Adjusted R-squared:  0.636 
## F-statistic: 140.4 on 5 and 394 DF,  p-value: < 2.2e-16

Interpretación (manteniendo constantes las demás variables):

  • Área construida: cada m² adicional se asocia con un aumento de 0.693 millones en el precio.
  • Baños: cada baño adicional se asocia con 83.59 millones más.
  • Habitaciones: cada habitación adicional se asocia con un cambio de -20.64 millones, pero no es significativo (valor p = 0.261): una vez se controla por área y baños, las habitaciones no aportan información adicional. El signo negativo es contrario al esperado y se explica por su correlación con los baños (0.64).
  • Parqueaderos: cada parqueadero adicional se asocia con 70.93 millones más.
  • Dummy casa: a igual área, baños, habitaciones y parqueaderos, una casa cuesta en promedio 109.3 millones más que un apartamento (la categoría de referencia). Es significativa (valor p = 0.0013).
  • R² ajustado = 0.636.

Con un nivel de 5%, son significativos el área, los baños, los parqueaderos y la dummy casa; habitaciones no lo es. Los signos coinciden con los esperados, salvo habitaciones (negativo).

Validación de supuestos MM1

vm1 <- validar(mm1, "(MM1)")

vm1
Prueba Valor_p Decision
Shapiro-Wilk (normalidad) 0e+00 Se rechaza H0
BP Breusch-Pagan (homocedasticidad) 3e-07 Se rechaza H0

Interpretación. Normalidad: se rechaza H0 (p = 9.61e-15). Homocedasticidad: se rechaza H0 (p = 3.35e-07). Es decir, MM1 no cumple normalidad ni varianza constante.

4.3 Modelo múltiple 2 (transformación logarítmica)

Dada la asimetría del precio y la forma de la relación con el área, se estima el modelo con log(precio) y log(área). Las demás variables (baños, habitaciones, parqueaderos y dummy) se dejan en su escala original, porque baños y habitaciones tienen valores en cero.

log(precio) = β0 + β1·log(área) + β2·baños + β3·habitaciones + β4·parqueaderos + β5·casa + ε

mm2 <- lm(log(precio_millon) ~ log(Area_contruida) + Banos + Habitaciones + parqueaderos + casa,
          data = datos_mod)
summary(mm2)
## 
## Call:
## lm(formula = log(precio_millon) ~ log(Area_contruida) + Banos + 
##     Habitaciones + parqueaderos + casa, data = datos_mod)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -0.78503 -0.14089 -0.01421  0.11732  0.65937 
## 
## Coefficients:
##                      Estimate Std. Error t value Pr(>|t|)    
## (Intercept)          3.629274   0.140160  25.894  < 2e-16 ***
## log(Area_contruida)  0.498682   0.031300  15.932  < 2e-16 ***
## Banos                0.080083   0.012979   6.170 1.69e-09 ***
## Habitaciones        -0.032145   0.016206  -1.983    0.048 *  
## parqueaderos         0.042455   0.009167   4.631 4.94e-06 ***
## casa                -0.035660   0.032571  -1.095    0.274    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.2074 on 394 degrees of freedom
## Multiple R-squared:  0.771,  Adjusted R-squared:  0.7681 
## F-statistic: 265.3 on 5 and 394 DF,  p-value: < 2.2e-16

Interpretación:

  • log(área): un aumento de 1% en el área se asocia con un aumento de aproximadamente 0.499% en el precio (elasticidad).
  • Baños: cada baño adicional se asocia con un cambio de 8.3% en el precio.
  • Habitaciones: cambio de -3.2% por habitación adicional; es apenas significativo (p = 0.048) y con signo contrario al esperado (efecto de la correlación con baños).
  • Parqueaderos: cambio de 4.3% por parqueadero adicional.
  • Dummy casa: una casa tiene un precio -3.5% distinto al de un apartamento comparable, pero no es significativa (valor p = 0.27). En el modelo logarítmico, el área ya recoge casi toda la diferencia de precio entre casas y apartamentos.
  • R² ajustado = 0.768.

Con un nivel de 5%, son significativos el log del área, los baños y los parqueaderos; habitaciones apenas lo es (p ≈ 0.048) y con signo contrario al esperado; la dummy casa no es significativa.

Validación de supuestos MM2

vm2 <- validar(mm2, "(MM2)")

vm2
Prueba Valor_p Decision
Shapiro-Wilk (normalidad) 4.2e-06 Se rechaza H0
BP Breusch-Pagan (homocedasticidad) 0.0e+00 Se rechaza H0

Interpretación. Normalidad: se rechaza H0 (p = 4.21e-06); homocedasticidad: se rechaza H0 (p = 1.07e-12).

Pertinencia de la transformación logarítmica

data.frame(
  Modelo = c("MM1 (escala original)", "MM2 (log)"),
  p_Shapiro = c(pval(vm1, 1), pval(vm2, 1)),
  p_BreuschPagan = c(pval(vm1, 2), pval(vm2, 2)),
  R2_ajustado = round(c(summary(mm1)$adj.r.squared, summary(mm2)$adj.r.squared), 3)
)
Modelo p_Shapiro p_BreuschPagan R2_ajustado
MM1 (escala original) 0.0e+00 3e-07 0.636
MM2 (log) 4.2e-06 0e+00 0.768

Interpretación. Al pasar a logaritmos, el R² ajustado sube de 0.636 a 0.768 y la normalidad mejora (Shapiro-Wilk pasa de p = 9.61e-15 a p = 4.21e-06), aunque sigue rechazándose. En cambio, el valor p de Breusch-Pagan baja (de 3.35e-07 a 1.07e-12): con ese criterio la transformación no corrige la varianza no constante, y ambos modelos la incumplen. Conclusión: la transformación es pertinente por el mejor ajuste, la mejor normalidad y el menor error de predicción (MAE, sección 4.6), pero no resuelve la heterocedasticidad.

4.4 Selección de variables con Step by Step

Se usa step() con criterio AIC y dirección both (adelante y atrás), partiendo del modelo completo.

cat("===== MM1 (escala original) =====\n")
## ===== MM1 (escala original) =====
mm1_step <- step(mm1, direction = "both", trace = 1)
## Start:  AIC=4374.39
## precio_millon ~ Area_contruida + Banos + Habitaciones + parqueaderos + 
##     casa
## 
##                  Df Sum of Sq      RSS    AIC
## - Habitaciones    1     70031 21870320 4373.7
## <none>                        21800289 4374.4
## - casa            1    584085 22384374 4383.0
## - Banos           1   1890370 23690660 4405.7
## - parqueaderos    1   2712526 24512816 4419.3
## - Area_contruida  1   4214055 26014344 4443.1
## 
## Step:  AIC=4373.67
## precio_millon ~ Area_contruida + Banos + parqueaderos + casa
## 
##                  Df Sum of Sq      RSS    AIC
## <none>                        21870320 4373.7
## + Habitaciones    1     70031 21800289 4374.4
## - casa            1    516535 22386856 4381.0
## - Banos           1   2044119 23914439 4407.4
## - parqueaderos    1   2718521 24588841 4418.5
## - Area_contruida  1   4227455 26097775 4442.4
summary(mm1_step)
## 
## Call:
## lm(formula = precio_millon ~ Area_contruida + Banos + parqueaderos + 
##     casa, data = datos_mod)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -801.55 -126.06  -43.43   64.26  948.42 
## 
## Coefficients:
##                Estimate Std. Error t value Pr(>|t|)    
## (Intercept)    84.59948   49.92517   1.695  0.09095 .  
## Area_contruida  0.69396    0.07942   8.738  < 2e-16 ***
## Banos          75.67502   12.45456   6.076 2.90e-09 ***
## parqueaderos   71.00954   10.13396   7.007 1.06e-11 ***
## casa           98.55709   32.26761   3.054  0.00241 ** 
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 235.3 on 395 degrees of freedom
## Multiple R-squared:  0.6394, Adjusted R-squared:  0.6357 
## F-statistic: 175.1 on 4 and 395 DF,  p-value: < 2.2e-16
cat("===== MM2 (logarítmico) =====\n")
## ===== MM2 (logarítmico) =====
mm2_step <- step(mm2, direction = "both", trace = 1)
## Start:  AIC=-1252.7
## log(precio_millon) ~ log(Area_contruida) + Banos + Habitaciones + 
##     parqueaderos + casa
## 
##                       Df Sum of Sq    RSS     AIC
## - casa                 1    0.0515 16.992 -1253.5
## <none>                             16.941 -1252.7
## - Habitaciones         1    0.1692 17.110 -1250.7
## - parqueaderos         1    0.9223 17.863 -1233.5
## - Banos                1    1.6370 18.578 -1217.8
## - log(Area_contruida)  1   10.9139 27.855 -1055.8
## 
## Step:  AIC=-1253.48
## log(precio_millon) ~ log(Area_contruida) + Banos + Habitaciones + 
##     parqueaderos
## 
##                       Df Sum of Sq    RSS     AIC
## <none>                             16.992 -1253.5
## + casa                 1    0.0515 16.941 -1252.7
## - Habitaciones         1    0.2236 17.216 -1250.2
## - parqueaderos         1    0.8956 17.888 -1234.9
## - Banos                1    1.6469 18.639 -1218.5
## - log(Area_contruida)  1   14.2102 31.202 -1012.4
summary(mm2_step)
## 
## Call:
## lm(formula = log(precio_millon) ~ log(Area_contruida) + Banos + 
##     Habitaciones + parqueaderos, data = datos_mod)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -0.77249 -0.14816 -0.01079  0.11296  0.67897 
## 
## Coefficients:
##                      Estimate Std. Error t value Pr(>|t|)    
## (Intercept)          3.726836   0.108213  34.440  < 2e-16 ***
## log(Area_contruida)  0.480307   0.026427  18.175  < 2e-16 ***
## Banos                0.080314   0.012980   6.187 1.53e-09 ***
## Habitaciones        -0.036051   0.015813  -2.280   0.0231 *  
## parqueaderos         0.041728   0.009145   4.563 6.74e-06 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.2074 on 395 degrees of freedom
## Multiple R-squared:  0.7703, Adjusted R-squared:  0.768 
## F-statistic: 331.2 on 4 and 395 DF,  p-value: < 2.2e-16
formula(mm1_step)
## precio_millon ~ Area_contruida + Banos + parqueaderos + casa
formula(mm2_step)
## log(precio_millon) ~ log(Area_contruida) + Banos + Habitaciones + 
##     parqueaderos
data.frame(
  Modelo = c("MM1 completo", "MM1 step", "MM2 completo", "MM2 step"),
  AIC = round(c(AIC(mm1), AIC(mm1_step), AIC(mm2), AIC(mm2_step)), 1),
  R2_ajustado = round(c(summary(mm1)$adj.r.squared, summary(mm1_step)$adj.r.squared,
                        summary(mm2)$adj.r.squared, summary(mm2_step)$adj.r.squared), 3)
)
Modelo AIC R2_ajustado
MM1 completo 5511.5 0.636
MM1 step 5510.8 0.636
MM2 completo -115.5 0.768
MM2 step -116.3 0.768

Interpretación. El procedimiento elimina las variables que no mejoran el AIC. En el modelo en escala original (MM1) conserva Area_contruida, Banos, parqueaderos, casa: elimina Habitaciones y mantiene la dummy casa. En el modelo logarítmico (MM2) conserva log(Area_contruida), Banos, Habitaciones, parqueaderos: elimina la dummy casa y mantiene Habitaciones. La mejora del AIC es pequeña en ambos casos (el AIC baja menos de 1 punto), es decir, los modelos completos y los reducidos son casi equivalentes; el step sirve sobre todo para simplificar. Los AIC de MM1 y MM2 no son comparables entre sí porque tienen distinta variable respuesta; sí lo son los AIC dentro de cada modelo (completo vs. step).

4.5 Predicción hipotética (predict)

Se predice el precio de una vivienda hipotética en Pance: 250 m² de área construida, 4 baños, 4 habitaciones, 3 parqueaderos, considerando los dos tipos de vivienda.

nuevas <- data.frame(
  Area_contruida = c(250, 250),
  Banos          = c(4, 4),
  Habitaciones   = c(4, 4),
  parqueaderos   = c(3, 3),
  casa           = c(1, 0)
)
nuevas$Tipo <- c("Casa", "Apartamento")

# Modelo 1 (escala original): la predicción ya está en millones
p1 <- predict(mm1_step, newdata = nuevas, interval = "prediction", level = 0.95)

# Modelo 2 (log): se devuelve a millones con exp()
p2 <- predict(mm2_step, newdata = nuevas, interval = "prediction", level = 0.95)

tabla_pred <- data.frame(
  Tipo = nuevas$Tipo,
  MM1_step_ajuste = round(p1[, "fit"], 1),
  MM1_step_inf = round(p1[, "lwr"], 1),
  MM1_step_sup = round(p1[, "upr"], 1),
  MM2_step_ajuste = round(exp(p2[, "fit"]), 1),
  MM2_step_inf = round(exp(p2[, "lwr"]), 1),
  MM2_step_sup = round(exp(p2[, "upr"]), 1)
)
tabla_pred
Tipo MM1_step_ajuste MM1_step_inf MM1_step_sup MM2_step_ajuste MM2_step_inf MM2_step_sup
Casa 872.4 407.6 1337.2 797.2 529.7 1199.7
Apartamento 773.8 309.7 1237.9 797.2 529.7 1199.7

Interpretación. Para una vivienda de 250 m², 4 baños, 4 habitaciones y 3 parqueaderos: el modelo MM1 step (que conserva la dummy) predice 872.4 millones si es casa y 773.8 millones si es apartamento (la diferencia es el coeficiente de la dummy), con intervalos de predicción al 95% de 407.6 a 1337.2 (casa) y de 309.7 a 1237.9 (apartamento). El modelo MM2 step predice 797.2 millones (intervalo de 529.7 a 1199.7), igual para casa y apartamento, porque el step eliminó la dummy de ese modelo y predict ignora la columna casa. Los intervalos de predicción son amplios (unos 930 millones de ancho en MM1), reflejando que el error típico del modelo es de unos 235 millones: sirven para el rango de una oferta individual, no del precio promedio. Los del modelo logarítmico son asimétricos (más amplios hacia arriba), coherente con la naturaleza del precio. Los valores centrales (entre ~770 y ~870 millones) son razonables frente a la mediana del barrio (780 millones).

4.6 Métricas de evaluación: MAE

El error absoluto medio (MAE) es el promedio de los errores en valor absoluto, en las mismas unidades del precio (millones de pesos):

\[MAE = \frac{1}{n}\sum_{i=1}^{n} |y_i - \hat{y}_i|\]

Se calcula dentro de la muestra con los modelos finales del step (la predicción del modelo logarítmico se devuelve a millones con exp()).

mae <- function(real, pred) mean(abs(real - pred))

# (a) Dentro de la muestra (se devuelve el modelo log a millones)
mae_tabla <- data.frame(
  Modelo = c("MM1 step (escala original)", "MM2 step (log)"),
  MAE_muestra = round(c(
    mae(datos_mod$precio_millon, predict(mm1_step, datos_mod)),
    mae(datos_mod$precio_millon, exp(predict(mm2_step, datos_mod)))
  ), 2)
)
mae_tabla
Modelo MAE_muestra
MM1 step (escala original) 164.39
MM2 step (log) 154.96

Interpretación del MAE. El MAE indica que, en promedio, la predicción se desvía del precio real en la cantidad de millones de pesos de la tabla: 164.39 millones en MM1 step y 154.96 millones en MM2 step. Frente al precio promedio de las ofertas usadas en los modelos (881 millones), eso es un error de ≈ 18.7% y ≈ 17.6%. El modelo con menor MAE es MM2 step (log), pero la diferencia es pequeña (9.4 millones), así que ambos predicen de forma parecida. La comparación entre MM1 y MM2 es válida porque ambos errores se miden en millones de pesos.

5. Conclusiones

  • En Pance, el área construida es el principal determinante del precio entre las variables simples (R² de 0.49 en escala original y 0.73 en log-log); parqueaderos (0.40) y baños (0.32) aportan menos, y las habitaciones aportan poco (no son significativas en MM1 y apenas lo son en MM2, con signo contrario al esperado).
  • La transformación logarítmica es pertinente para el área (mejora el ajuste y los supuestos) y para el modelo múltiple (R² ajustado de 0.64 a 0.77, mejor normalidad y menor MAE). No lo es para parqueaderos ni baños en los modelos simples, donde no mejora (o empeora) la homocedasticidad. En ningún caso se resuelve del todo la heterocedasticidad ni la normalidad, por lo que los resultados deben leerse con cautela.
  • La dummy del tipo de vivienda es significativa en el modelo en escala original (en MM1 step, una casa cuesta ≈ 99 millones más que un apartamento comparable), pero no en el modelo logarítmico, donde el área ya recoge casi toda la diferencia y el step la elimina.
  • La selección step simplifica los modelos (elimina Habitaciones en MM1 y casa en MM2), con mejoras muy pequeñas del AIC.
  • El MAE es de unos 160 millones de pesos (≈ 18% del precio promedio) y es algo menor para MM2 step (log). Los modelos no capturan factores como la ubicación exacta, el estado y los acabados, por lo que el error de predicción sigue siendo considerable.

Limitaciones: son precios de oferta (no de cierre), la muestra proviene de una sola plataforma, hay valores atípicos (viviendas muy grandes) y registros con 0 baños o 0 habitaciones que pueden ser errores de digitación.

Notas sobre la lógica del código

  • No se usó piso porque tiene 130 valores faltantes (78 casas y 52 apartamentos).
  • Banos y Habitaciones tienen 2 registros con valor 0 cada una (3 filas en total), así que a esas variables no se les aplicó logaritmo (solo al precio y al área).
  • En los modelos múltiples se usan 400 de las 412 ofertas (se omiten 12 sin dato de parqueaderos).