Introducción

Este informe modela el precio de oferta (en millones de pesos) de los apartamentos usados del barrio La Flora, en la Zona Norte de Cali, a partir de la base Datos_Vivienda. Primero se plantean tres modelos de regresión lineal simple y luego dos modelos de regresión lineal múltiple. En cada caso se interpretan los coeficientes, se validan los supuestos del modelo (linealidad, normalidad, homocedasticidad, independencia y, en los múltiples, multicolinealidad) y se evalúa si una transformación logarítmica mejora el cumplimiento de esos supuestos. Al final se hace una predicción hipotética con predict() y se comparan los modelos múltiples con el error absoluto medio (MAE).

Preparación de los datos

Librerías

library(readxl)     # leer el archivo de Excel
library(dplyr)      # manipulación de datos
library(ggplot2)    # gráficos
library(plotly)     # convierte los gráficos de ggplot en interactivos
library(knitr)      # tablas en el informe
library(kableExtra) # formato de tablas
library(lmtest)     # pruebas de Breusch-Pagan y Durbin-Watson
library(car)        # VIF (multicolinealidad)
library(sandwich)   # errores estándar robustos
library(MASS)       # transformación de Box-Cox

Carga y filtro por barrio

# Cargar la base indicando el tipo de cada columna
datos <- read_excel("Datos_Vivienda.xlsx",
                    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)

# Normalizar el nombre del barrio (minúsculas y sin espacios sobrantes)
datos$barrio_norm <- tolower(trimws(datos$Barrio))

# Variantes de escritura del barrio La Flora que aparecen en la base
unique(datos$barrio_norm[grepl("flora", datos$barrio_norm)])
## [1] "la flora"              "floralia"              "urbanizacion la flora"
## [4] "flora industrial"      "laflora"               "norte la flora"       
## [7] "flora"

En la base el barrio aparece escrito de varias formas. Se conservan las variantes que corresponden a La Flora (la flora, laflora, urbanizacion la flora, norte la flora, flora) y se excluyen Floralia y Flora Industrial, que son barrios distintos. Además se filtra sólo el tipo Apartamento.

variantes <- c("la flora", "laflora", "urbanizacion la flora", "norte la flora", "flora")

datos_sub <- datos %>%
  filter(barrio_norm %in% variantes, Tipo == "Apartamento")

nrow(datos_sub)   # registros encontrados
## [1] 331

Limpieza

# piso y Estrato se leyeron como texto: se convierten a número ("NA" pasa a NA)
datos_sub$piso    <- suppressWarnings(as.numeric(datos_sub$piso))
datos_sub$Estrato <- as.numeric(datos_sub$Estrato)

# Registros con datos faltantes o inconsistentes
sum(is.na(datos_sub$parqueaderos))   # sin dato de parqueaderos
## [1] 7
sum(datos_sub$Habitaciones == 0)     # apartamentos con 0 habitaciones (inconsistente)
## [1] 2
sum(is.na(datos_sub$piso))           # sin dato de piso
## [1] 74
# Se eliminan los registros sin parqueadero reportado y con 0 habitaciones
datos_sub <- datos_sub %>%
  filter(!is.na(parqueaderos), Habitaciones > 0)

# Variable DUMMY: 1 si el apartamento tiene 2 o más parqueaderos, 0 si tiene uno
datos_sub$D_parq <- ifelse(datos_sub$parqueaderos >= 2, 1, 0)

nrow(datos_sub)   # registros para el análisis
## [1] 322
table(datos_sub$D_parq)
## 
##   0   1 
## 204 118

La variable piso tiene muchos faltantes; no se eliminan esos registros aquí para no perder información en los modelos simples. En los modelos múltiples que usan piso, R descarta automáticamente las filas sin ese dato (se reporta el tamaño de muestra efectivo).

Exploración rápida

datos_sub %>%
  summarise(n = n(),
            precio_prom = mean(precio_millon),
            precio_mediana = median(precio_millon),
            area_prom = mean(Area_contruida),
            banos_prom = mean(Banos),
            habitaciones_prom = mean(Habitaciones)) %>%
  kable(digits = 1, caption = "Tabla 1. Resumen de los apartamentos de La Flora") %>%
  kable_styling(full_width = FALSE)
Tabla 1. Resumen de los apartamentos de La Flora
n precio_prom precio_mediana area_prom banos_prom habitaciones_prom
322 336.5 320 99.2 2.5 2.9
# Correlaciones entre el precio y las variables candidatas
vars <- c("precio_millon", "Area_contruida", "Banos", "Habitaciones", "parqueaderos", "piso", "Estrato")
round(cor(datos_sub[, vars], use = "complete.obs"), 2)
##                precio_millon Area_contruida Banos Habitaciones parqueaderos
## precio_millon           1.00           0.66  0.60         0.15         0.53
## Area_contruida          0.66           1.00  0.62         0.41         0.53
## Banos                   0.60           0.62  1.00         0.49         0.50
## Habitaciones            0.15           0.41  0.49         1.00         0.24
## parqueaderos            0.53           0.53  0.50         0.24         1.00
## piso                    0.43           0.19  0.19         0.04         0.37
## Estrato                 0.10           0.10  0.08        -0.02         0.13
##                piso Estrato
## precio_millon  0.43    0.10
## Area_contruida 0.19    0.10
## Banos          0.19    0.08
## Habitaciones   0.04   -0.02
## parqueaderos   0.37    0.13
## piso           1.00    0.06
## Estrato        0.06    1.00
# ggplotly() convierte el gráfico en interactivo (zoom y valores al pasar el mouse)
ggplotly(
  ggplot(datos_sub, aes(x = precio_millon)) +
    geom_histogram(binwidth = 40, fill = "#2c3e50", color = "white") +
    labs(title = "Figura 1. Distribución del precio", x = "Precio (millones)", y = "Frecuencia") +
    theme_minimal()
)

El precio se concentra entre 250 y 400 millones y tiene una cola larga hacia la derecha (pocos apartamentos de más de 600 millones). Esa asimetría anticipa posibles problemas de normalidad y de varianza no constante, y es la razón principal para probar transformaciones logarítmicas.

Funciones de apoyo para validar supuestos

Para no repetir el mismo código en cada modelo, se define una función que aplica las pruebas formales y otra que dibuja los gráficos de diagnóstico.

# Pruebas formales de supuestos sobre los residuos de un modelo lm
validar <- function(modelo) {
  sw <- shapiro.test(residuals(modelo))   # H0: residuos normales
  bp <- bptest(modelo)                    # H0: varianza constante (homocedasticidad)
  dw <- dwtest(modelo)                    # H0: no autocorrelación
  data.frame(
    Supuesto = c("Normalidad (Shapiro-Wilk)",
                 "Homocedasticidad (Breusch-Pagan)",
                 "Independencia (Durbin-Watson)"),
    Estadistico = round(c(sw$statistic, bp$statistic, dw$statistic), 3),
    p_valor = signif(c(sw$p.value, bp$p.value, dw$p.value), 3),
    Conclusion = ifelse(c(sw$p.value, bp$p.value, dw$p.value) < 0.05,
                        "Se rechaza H0 (no cumple)", "No se rechaza H0 (cumple)")
  )
}

# Gráficos de diagnóstico interactivos (2 x 2):
# residuos vs ajustados, QQ normal, escala-ubicación y residuos vs leverage.
# Al pasar el mouse sobre un punto se ve el id del apartamento, útil para ubicar atípicos.
diagnostico <- function(modelo) {
  base <- eval(modelo$call$data)                        # datos con que se estimó el modelo
  d <- data.frame(
    id        = base$id[as.integer(names(residuals(modelo)))],
    ajustados = fitted(modelo),
    residuos  = residuals(modelo),
    estand    = rstandard(modelo),                      # residuos estandarizados
    leverage  = hatvalues(modelo),
    cook      = cooks.distance(modelo)
  )
  d$teorico <- qqnorm(d$estand, plot.it = FALSE)$x      # cuantiles teóricos normales

  g1 <- ggplot(d, aes(ajustados, residuos, text = paste("id:", id))) +
    geom_point(color = "#2c3e50", alpha = 0.6) +
    geom_hline(yintercept = 0, linetype = "dashed", color = "#e74c3c") +
    labs(x = "Valores ajustados", y = "Residuos") + theme_minimal()

  g2 <- ggplot(d, aes(teorico, estand, text = paste("id:", id))) +
    geom_point(color = "#2c3e50", alpha = 0.6) +
    geom_abline(slope = 1, intercept = 0, linetype = "dashed", color = "#e74c3c") +
    labs(x = "Cuantiles teóricos", y = "Residuos estandarizados") + theme_minimal()

  g3 <- ggplot(d, aes(ajustados, sqrt(abs(estand)), text = paste("id:", id))) +
    geom_point(color = "#2c3e50", alpha = 0.6) +
    labs(x = "Valores ajustados", y = "√|Residuos estandarizados|") + theme_minimal()

  g4 <- ggplot(d, aes(leverage, estand, text = paste("id:", id, "<br>Cook:", round(cook, 3)))) +
    geom_point(color = "#2c3e50", alpha = 0.6) +
    geom_hline(yintercept = 0, linetype = "dashed", color = "#e74c3c") +
    labs(x = "Leverage", y = "Residuos estandarizados") + theme_minimal()

  titulos <- c("Residuos vs ajustados", "QQ normal", "Escala-ubicación", "Residuos vs leverage")
  subplot(ggplotly(g1, tooltip = c("text", "x", "y")), ggplotly(g2, tooltip = c("text", "x", "y")),
          ggplotly(g3, tooltip = c("text", "x", "y")), ggplotly(g4, tooltip = c("text", "x", "y")),
          nrows = 2, margin = c(0.06, 0.06, 0.09, 0.09), titleX = TRUE, titleY = TRUE) %>%
    layout(showlegend = FALSE,
           annotations = lapply(1:4, function(i) list(
             text = paste0("<b>", titulos[i], "</b>"), showarrow = FALSE,
             xref = "paper", yref = "paper",
             x = ifelse(i %% 2 == 1, 0.22, 0.78), y = ifelse(i <= 2, 1.01, 0.42),
             xanchor = "center", yanchor = "bottom")))
}

Nota sobre Durbin-Watson: esta prueba detecta autocorrelación según el orden de las filas. Como los datos son de corte transversal (ofertas sin un orden temporal), un rechazo puede reflejar sólo cómo está ordenada la base (por ejemplo, por ubicación) y no un problema serio del modelo. Se reporta, pero se interpreta con cautela.

1. Modelación simple

Se plantean tres modelos simples con el precio como variable dependiente y, como explicativas, el área construida, el número de baños y el número de habitaciones. Para cada uno se estima el modelo en niveles, se validan sus supuestos y luego se estima la versión con logaritmos para ver si los supuestos mejoran.

Modelo simple 1: precio ~ área construida

Modelo poblacional

\[Precio_i = \beta_0 + \beta_1 \, Area_i + u_i\]

Estimación

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 
## -192.566  -42.439    1.843   50.623  142.237 
## 
## Coefficients:
##                Estimate Std. Error t value            Pr(>|t|)    
## (Intercept)    131.4656    14.1738   9.275 <0.0000000000000002 ***
## Area_contruida   2.0669     0.1381  14.962 <0.0000000000000002 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 64.83 on 320 degrees of freedom
## Multiple R-squared:  0.4116, Adjusted R-squared:  0.4098 
## F-statistic: 223.9 on 1 and 320 DF,  p-value: < 0.00000000000000022
ggplotly(
  ggplot(datos_sub, aes(x = Area_contruida, y = precio_millon)) +
    geom_point(aes(text = paste("id:", id)), color = "#2c3e50", alpha = 0.6) +
    geom_smooth(method = "lm", color = "#e74c3c", se = TRUE) +
    labs(title = "Figura 2. Precio vs área construida", x = "Área (m²)", y = "Precio (millones)") +
    theme_minimal()
)

Interpretación

La ecuación estimada es Precio = 131.47 + 2.07 × Área.

  • Pendiente: por cada metro cuadrado adicional de área construida, el precio esperado del apartamento aumenta en 2.07 millones de pesos, en promedio. El coeficiente es altamente significativo (p < 0.001).
  • Intercepto: no tiene una interpretación económica directa (no existen apartamentos de 0 m²); sólo ubica la recta.
  • Ajuste: el R² es 0.412, es decir, el área explica cerca del 41% de la variación del precio en La Flora. Es la variable individual con mayor poder explicativo.

Validación de supuestos

diagnostico(m1)
kable(validar(m1), caption = "Supuestos – Modelo 1 (niveles)") %>% kable_styling(full_width = FALSE)
Supuestos – Modelo 1 (niveles)
Supuesto Estadistico p_valor Conclusion
W Normalidad (Shapiro-Wilk) 0.988 0.00965 Se rechaza H0 (no cumple)
BP Homocedasticidad (Breusch-Pagan) 64.454 0.00000 Se rechaza H0 (no cumple)
DW Independencia (Durbin-Watson) 1.682 0.00207 Se rechaza H0 (no cumple)

En niveles el modelo no cumple homocedasticidad: la prueba de Breusch-Pagan rechaza con un p-valor muy pequeño y el gráfico de residuos vs ajustados muestra un embudo (los errores crecen con el precio). Tampoco cumple normalidad al 5%, aunque el QQ-plot muestra que la desviación se concentra en las colas (apartamentos atípicamente grandes o caros). Durbin-Watson rechaza la independencia, pero, como se explicó, en datos de corte transversal esto refleja el orden de la base (las ofertas aparecen ordenadas aproximadamente por longitud geográfica) más que una autocorrelación real.

Transformación logarítmica (log-log)

\[\ln(Precio_i) = \beta_0 + \beta_1 \ln(Area_i) + u_i\]

m1_log <- lm(log(precio_millon) ~ log(Area_contruida), data = datos_sub)
summary(m1_log)
## 
## Call:
## lm(formula = log(precio_millon) ~ log(Area_contruida), data = datos_sub)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -0.72422 -0.12006  0.02698  0.14731  0.35621 
## 
## Coefficients:
##                     Estimate Std. Error t value            Pr(>|t|)    
## (Intercept)          2.52083    0.23263   10.84 <0.0000000000000002 ***
## log(Area_contruida)  0.71460    0.05083   14.06 <0.0000000000000002 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.1969 on 320 degrees of freedom
## Multiple R-squared:  0.3818, Adjusted R-squared:  0.3799 
## F-statistic: 197.7 on 1 and 320 DF,  p-value: < 0.00000000000000022
diagnostico(m1_log)
kable(validar(m1_log), caption = "Supuestos – Modelo 1 (log-log)") %>% kable_styling(full_width = FALSE)
Supuestos – Modelo 1 (log-log)
Supuesto Estadistico p_valor Conclusion
W Normalidad (Shapiro-Wilk) 0.952 0.00000 Se rechaza H0 (no cumple)
BP Homocedasticidad (Breusch-Pagan) 3.338 0.06770 No se rechaza H0 (cumple)
DW Independencia (Durbin-Watson) 1.710 0.00445 Se rechaza H0 (no cumple)

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 0.71% en el precio. Como es menor que 1, el precio crece menos que proporcionalmente con el área (el precio por m² baja en los apartamentos más grandes).

¿Es pertinente la transformación? Parcialmente. El logaritmo corrige la heterocedasticidad (Breusch-Pagan ya no rechaza al 5%), pero empeora la normalidad de los residuos (el p-valor de Shapiro-Wilk baja aún más), porque el logaritmo abre la cola de los apartamentos pequeños y baratos. El R² baja ligeramente (0.382). Si el objetivo es hacer inferencia sobre la pendiente, la versión log-log es preferible por la varianza constante; con un tamaño de muestra de más de 300 datos, la falta de normalidad tiene menos consecuencias gracias al teorema del límite central.

Modelo simple 2: precio ~ baños

Modelo poblacional

\[Precio_i = \beta_0 + \beta_1 \, Banos_i + u_i\]

Estimación

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 
## -205.878  -43.175   -1.222   42.216  310.434 
## 
## Coefficients:
##             Estimate Std. Error t value            Pr(>|t|)    
## (Intercept)  164.815     14.298   11.53 <0.0000000000000002 ***
## Banos         68.688      5.506   12.47 <0.0000000000000002 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 69.32 on 320 degrees of freedom
## Multiple R-squared:  0.3272, Adjusted R-squared:  0.3251 
## F-statistic: 155.6 on 1 and 320 DF,  p-value: < 0.00000000000000022
ggplotly(
  ggplot(datos_sub, aes(x = factor(Banos), y = precio_millon)) +
    geom_boxplot(fill = "#95a5a6") +
    labs(title = "Figura 3. Precio según número de baños", x = "Baños", y = "Precio (millones)") +
    theme_minimal()
)

Interpretación

La ecuación estimada es Precio = 164.81 + 68.69 × Baños.

  • Cada baño adicional se asocia con un aumento promedio de 68.69 millones en el precio. El efecto es significativo (p < 0.001).
  • El R² es 0.327: el número de baños explica cerca del 33% de la variación del precio. Es un buen predictor porque los baños funcionan como una señal del tamaño y del nivel de acabados del apartamento.

Validación de supuestos

diagnostico(m2)
kable(validar(m2), caption = "Supuestos – Modelo 2 (niveles)") %>% kable_styling(full_width = FALSE)
Supuestos – Modelo 2 (niveles)
Supuesto Estadistico p_valor Conclusion
W Normalidad (Shapiro-Wilk) 0.988 0.01120 Se rechaza H0 (no cumple)
BP Homocedasticidad (Breusch-Pagan) 9.198 0.00242 Se rechaza H0 (no cumple)
DW Independencia (Durbin-Watson) 1.840 0.07500 No se rechaza H0 (cumple)

En niveles se rechaza la homocedasticidad (la dispersión del precio crece con el número de baños, visible en la Figura 3) y también la normalidad al 5%. Durbin-Watson no rechaza al 5%.

Transformación logarítmica (log-lin)

Como Banos es una variable de conteo con pocos valores, se transforma sólo la variable dependiente:

\[\ln(Precio_i) = \beta_0 + \beta_1 \, Banos_i + u_i\]

m2_log <- lm(log(precio_millon) ~ Banos, data = datos_sub)
summary(m2_log)
## 
## Call:
## lm(formula = log(precio_millon) ~ Banos, data = datos_sub)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -0.77711 -0.13154  0.01121  0.14301  0.58030 
## 
## Coefficients:
##             Estimate Std. Error t value            Pr(>|t|)    
## (Intercept)  5.31161    0.04364  121.71 <0.0000000000000002 ***
## Banos        0.19048    0.01681   11.33 <0.0000000000000002 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.2116 on 320 degrees of freedom
## Multiple R-squared:  0.2864, Adjusted R-squared:  0.2842 
## F-statistic: 128.4 on 1 and 320 DF,  p-value: < 0.00000000000000022
diagnostico(m2_log)
kable(validar(m2_log), caption = "Supuestos – Modelo 2 (log-lin)") %>% kable_styling(full_width = FALSE)
Supuestos – Modelo 2 (log-lin)
Supuesto Estadistico p_valor Conclusion
W Normalidad (Shapiro-Wilk) 0.981 0.000269 Se rechaza H0 (no cumple)
BP Homocedasticidad (Breusch-Pagan) 0.141 0.708000 No se rechaza H0 (cumple)
DW Independencia (Durbin-Watson) 1.844 0.080100 No se rechaza H0 (cumple)

Interpretación: cada baño adicional se asocia con un aumento aproximado de 19% en el precio (de forma exacta, 21%, calculado como 100·(e^β − 1)).

¿Es pertinente la transformación? Sí, para la varianza: el logaritmo resuelve por completo la heterocedasticidad (Breusch-Pagan con p-valor alto). La normalidad sigue sin cumplirse e incluso empeora un poco, de nuevo por los apartamentos más baratos. En balance, el modelo log-lin es más confiable para inferencia que el de niveles.

Modelo simple 3: precio ~ habitaciones

Modelo poblacional

\[Precio_i = \beta_0 + \beta_1 \, Habitaciones_i + u_i\]

Estimación

m3 <- lm(precio_millon ~ Habitaciones, data = datos_sub)
summary(m3)
## 
## Call:
## lm(formula = precio_millon ~ Habitaciones, data = datos_sub)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -187.78  -54.21  -17.78   62.22  388.66 
## 
## Coefficients:
##              Estimate Std. Error t value             Pr(>|t|)    
## (Intercept)   267.085     26.661  10.018 < 0.0000000000000002 ***
## Habitaciones   23.564      8.907   2.646              0.00856 ** 
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 83.6 on 320 degrees of freedom
## Multiple R-squared:  0.0214, Adjusted R-squared:  0.01835 
## F-statistic: 6.999 on 1 and 320 DF,  p-value: 0.008558

Interpretación

La ecuación estimada es Precio = 267.09 + 23.56 × Habitaciones.

  • Cada habitación adicional se asocia con 23.56 millones más en el precio. El coeficiente es significativo al 1% (p = 0.0086), pero el efecto es pequeño.
  • El R² es sólo 0.021: el número de habitaciones casi no explica el precio por sí solo. La mayoría de los apartamentos de La Flora tienen 3 habitaciones, así que la variable casi no varía, y un apartamento con más habitaciones no necesariamente es más grande (las habitaciones pueden ser más pequeñas). Este resultado anticipa que, en el modelo múltiple, una vez se controle por el área, el efecto de las habitaciones puede cambiar de signo.

Validación de supuestos

diagnostico(m3)
kable(validar(m3), caption = "Supuestos – Modelo 3 (niveles)") %>% kable_styling(full_width = FALSE)
Supuestos – Modelo 3 (niveles)
Supuesto Estadistico p_valor Conclusion
W Normalidad (Shapiro-Wilk) 0.976 0.0000289 Se rechaza H0 (no cumple)
BP Homocedasticidad (Breusch-Pagan) 7.401 0.0065200 Se rechaza H0 (no cumple)
DW Independencia (Durbin-Watson) 1.855 0.0939000 No se rechaza H0 (cumple)

En niveles se rechazan normalidad y homocedasticidad.

Transformación logarítmica (log-lin)

\[\ln(Precio_i) = \beta_0 + \beta_1 \, Habitaciones_i + u_i\]

m3_log <- lm(log(precio_millon) ~ Habitaciones, data = datos_sub)
summary(m3_log)
## 
## Call:
## lm(formula = log(precio_millon) ~ Habitaciones, data = datos_sub)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -0.78022 -0.15323 -0.02254  0.20061  0.77158 
## 
## Coefficients:
##              Estimate Std. Error t value            Pr(>|t|)    
## (Intercept)   5.61795    0.07930  70.847 <0.0000000000000002 ***
## Habitaciones  0.05764    0.02649   2.176              0.0303 *  
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.2487 on 320 degrees of freedom
## Multiple R-squared:  0.01458,    Adjusted R-squared:  0.0115 
## F-statistic: 4.733 on 1 and 320 DF,  p-value: 0.03032
diagnostico(m3_log)
kable(validar(m3_log), caption = "Supuestos – Modelo 3 (log-lin)") %>% kable_styling(full_width = FALSE)
Supuestos – Modelo 3 (log-lin)
Supuesto Estadistico p_valor Conclusion
W Normalidad (Shapiro-Wilk) 0.992 0.1040 No se rechaza H0 (cumple)
BP Homocedasticidad (Breusch-Pagan) 3.168 0.0751 No se rechaza H0 (cumple)
DW Independencia (Durbin-Watson) 1.846 0.0808 No se rechaza H0 (cumple)

Interpretación: cada habitación adicional se asocia con un precio 5.8% mayor, en promedio (significativo al 5%).

¿Es pertinente la transformación? Sí: en la versión log-lin se cumplen normalidad, homocedasticidad e independencia al 5%. Es el único modelo simple en el que la transformación corrige todos los supuestos. Sin embargo, el poder explicativo sigue siendo muy bajo, por lo que, aunque es un modelo estadísticamente “bien comportado”, es un mal modelo para predecir el precio.

Comparación de los modelos simples

comp_simple <- data.frame(
  Modelo = c("1. Precio ~ Área", "1. ln(Precio) ~ ln(Área)",
             "2. Precio ~ Baños", "2. ln(Precio) ~ Baños",
             "3. Precio ~ Habitaciones", "3. ln(Precio) ~ Habitaciones"),
  R2 = sapply(list(m1, m1_log, m2, m2_log, m3, m3_log), function(m) summary(m)$r.squared),
  p_Shapiro = sapply(list(m1, m1_log, m2, m2_log, m3, m3_log), function(m) shapiro.test(residuals(m))$p.value),
  p_BreuschPagan = sapply(list(m1, m1_log, m2, m2_log, m3, m3_log), function(m) bptest(m)$p.value)
)
kable(comp_simple, digits = 4, caption = "Tabla 2. Resumen de modelos simples") %>% kable_styling(full_width = FALSE)
Tabla 2. Resumen de modelos simples
Modelo R2 p_Shapiro p_BreuschPagan
  1. Precio ~ Área
0.4116 0.0096 0.0000
  1. ln(Precio) ~ ln(Área)
0.3818 0.0000 0.0677
  1. Precio ~ Baños
0.3272 0.0112 0.0024
  1. ln(Precio) ~ Baños
0.2864 0.0003 0.7077
  1. Precio ~ Habitaciones
0.0214 0.0000 0.0065
  1. ln(Precio) ~ Habitaciones
0.0146 0.1045 0.0751

El área construida es el mejor predictor individual del precio, seguido por el número de baños. En los tres casos la transformación logarítmica ayuda sobre todo con la homocedasticidad, que es el supuesto más afectado en niveles. Ningún modelo simple supera un R² de 0.42, lo que justifica pasar a modelos múltiples.

2. Modelación múltiple

Base para los modelos múltiples

Los modelos múltiples incluyen piso, que tiene datos faltantes. Para que todos los modelos (y el proceso step) se estimen sobre las mismas observaciones, se trabaja con los registros completos.

datos_m <- datos_sub %>%
  filter(!is.na(piso)) %>%
  dplyr::select(id, precio_millon, Area_contruida, Banos, Habitaciones, piso, Estrato, parqueaderos, D_parq)

nrow(datos_m)
## [1] 250

Selección de variables con Step by Step

Se parte de un modelo completo con todas las variables candidatas, incluida la dummy de parqueaderos, y se aplica step() en ambas direcciones (agregar y quitar variables), que elige el modelo con menor criterio de información de Akaike (AIC).

modelo_completo <- lm(precio_millon ~ Area_contruida + Banos + Habitaciones + piso + Estrato + D_parq,
                      data = datos_m)
summary(modelo_completo)
## 
## Call:
## lm(formula = precio_millon ~ Area_contruida + Banos + Habitaciones + 
##     piso + Estrato + D_parq, data = datos_m)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -157.161  -29.498   -0.437   33.397  133.821 
## 
## Coefficients:
##                Estimate Std. Error t value             Pr(>|t|)    
## (Intercept)    142.6983    57.4289   2.485               0.0136 *  
## Area_contruida   1.8376     0.2118   8.676 0.000000000000000598 ***
## Banos           39.1106     6.3188   6.190 0.000000002547821623 ***
## Habitaciones   -36.2946     6.8393  -5.307 0.000000251302615797 ***
## piso             5.6111     0.9228   6.081 0.000000004611287319 ***
## Estrato         -3.5456    10.8174  -0.328               0.7434    
## D_parq          13.7817     8.3624   1.648               0.1006    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 50.2 on 243 degrees of freedom
## Multiple R-squared:  0.628,  Adjusted R-squared:  0.6188 
## F-statistic: 68.38 on 6 and 243 DF,  p-value: < 0.00000000000000022
seleccion <- step(modelo_completo, direction = "both", trace = 1)
## Start:  AIC=1964.9
## precio_millon ~ Area_contruida + Banos + Habitaciones + piso + 
##     Estrato + D_parq
## 
##                  Df Sum of Sq    RSS    AIC
## - Estrato         1       271 612622 1963.0
## <none>                        612352 1964.9
## - D_parq          1      6845 619196 1965.7
## - Habitaciones    1     70967 683319 1990.3
## - piso            1     93172 705523 1998.3
## - Banos           1     96540 708892 1999.5
## - Area_contruida  1    189696 802047 2030.4
## 
## Step:  AIC=1963.01
## precio_millon ~ Area_contruida + Banos + Habitaciones + piso + 
##     D_parq
## 
##                  Df Sum of Sq    RSS    AIC
## <none>                        612622 1963.0
## - D_parq          1      6674 619297 1963.7
## + Estrato         1       271 612352 1964.9
## - Habitaciones    1     70719 683341 1988.3
## - piso            1     93094 705717 1996.4
## - Banos           1     96325 708947 1997.5
## - Area_contruida  1    189438 802061 2028.4

El procedimiento elimina la variable Estrato (casi todos los apartamentos de La Flora son estrato 5, así que no aporta variación) y conserva el área, los baños, las habitaciones, el piso y la dummy de parqueaderos. Ese modelo seleccionado es el Modelo múltiple 1.

Modelo múltiple 1 (niveles, con variable DUMMY)

Modelo poblacional

\[Precio_i = \beta_0 + \beta_1 Area_i + \beta_2 Banos_i + \beta_3 Habitaciones_i + \beta_4 Piso_i + \beta_5 D\_parq_i + u_i\]

donde \(D\_parq_i = 1\) si el apartamento tiene 2 o más parqueaderos y 0 si tiene uno.

Estimación

mm1 <- lm(precio_millon ~ Area_contruida + Banos + Habitaciones + piso + D_parq, data = datos_m)
summary(mm1)
## 
## Call:
## lm(formula = precio_millon ~ Area_contruida + Banos + Habitaciones + 
##     piso + D_parq, data = datos_m)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -157.260  -29.817   -0.382   33.112  133.629 
## 
## Coefficients:
##                Estimate Std. Error t value             Pr(>|t|)    
## (Intercept)    125.2226    21.2975   5.880 0.000000013447839984 ***
## Area_contruida   1.8344     0.2112   8.686 0.000000000000000549 ***
## Banos           39.0504     6.3046   6.194 0.000000002473451924 ***
## Habitaciones   -36.1167     6.8052  -5.307 0.000000250016693855 ***
## piso             5.6086     0.9211   6.089 0.000000004378526281 ***
## D_parq          13.5676     8.3216   1.630                0.104    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 50.11 on 244 degrees of freedom
## Multiple R-squared:  0.6279, Adjusted R-squared:  0.6202 
## F-statistic: 82.33 on 5 and 244 DF,  p-value: < 0.00000000000000022

Interpretación

Manteniendo todo lo demás constante (ceteris paribus):

  • Área: cada m² adicional aumenta el precio esperado en 1.83 millones (p < 0.001).
  • Baños: cada baño adicional aumenta el precio en 39.05 millones (p < 0.001).
  • Habitaciones: cada habitación adicional reduce el precio en 36.12 millones (p < 0.001). El signo cambia respecto al modelo simple porque ahora se compara a dos apartamentos con la misma área: si tienen el mismo tamaño, el que tiene más habitaciones las tiene más pequeñas, y el mercado valora menos esa distribución. Es un caso clásico de sesgo por variable omitida en el modelo simple.
  • Piso: cada piso adicional de altura aumenta el precio en 5.61 millones (p < 0.001), consistente con una prima por vista e iluminación.
  • Dummy de parqueaderos: un apartamento con 2 o más parqueaderos cuesta, en promedio, 13.57 millones más que uno con un solo parqueadero, con las demás características iguales. Sin embargo, el efecto no es significativo al 5% ni al 10% (p = 0.104): una vez se controla por área y baños, el segundo parqueadero no agrega un valor diferenciado claro (los apartamentos con dos parqueaderos ya son los más grandes). El step la conserva porque mejora levemente el AIC.
  • Ajuste: R² = 0.628 y R² ajustado = 0.62. El modelo explica cerca del 63% de la variación del precio, bastante más que cualquier modelo simple.

Validación de supuestos

diagnostico(mm1)
kable(validar(mm1), caption = "Supuestos – Modelo múltiple 1") %>% kable_styling(full_width = FALSE)
Supuestos – Modelo múltiple 1
Supuesto Estadistico p_valor Conclusion
W Normalidad (Shapiro-Wilk) 0.996 0.81700 No se rechaza H0 (cumple)
BP Homocedasticidad (Breusch-Pagan) 24.299 0.00019 Se rechaza H0 (no cumple)
DW Independencia (Durbin-Watson) 1.938 0.30400 No se rechaza H0 (cumple)
# Multicolinealidad: VIF > 5 (o > 10) indicaría un problema
vif(mm1)
## Area_contruida          Banos   Habitaciones           piso         D_parq 
##       1.863310       1.970917       1.366785       1.160396       1.637304
  • Normalidad: se cumple (Shapiro-Wilk no rechaza). Al incluir varias variables, los residuos dejan de tener colas largas.
  • Homocedasticidad: no se cumple (Breusch-Pagan rechaza). La varianza del error crece con el precio.
  • Independencia: se cumple (Durbin-Watson no rechaza).
  • Multicolinealidad: todos los VIF son menores que 2, así que no hay problema.

Como hay heterocedasticidad, los coeficientes siguen siendo insesgados, pero los errores estándar usuales no son confiables. Se recalculan con errores estándar robustos (HC1):

coeftest(mm1, vcov = vcovHC(mm1, type = "HC1"))
## 
## t test of coefficients:
## 
##                 Estimate Std. Error t value       Pr(>|t|)    
## (Intercept)    125.22256   24.25290  5.1632 0.000000503843 ***
## Area_contruida   1.83444    0.28037  6.5429 0.000000000352 ***
## Banos           39.05036    6.72273  5.8087 0.000000019546 ***
## Habitaciones   -36.11667    7.00489 -5.1559 0.000000521818 ***
## piso             5.60855    0.88586  6.3312 0.000000001158 ***
## D_parq          13.56760    9.74653  1.3920         0.1652    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Con errores robustos las conclusiones se mantienen: área, baños, habitaciones y piso siguen siendo significativos, y la dummy de parqueaderos sigue sin serlo.

Validación de la transformación logarítmica

Se busca primero qué transformación de la variable dependiente sugiere el método de Box-Cox (λ = 0 equivale a logaritmo; λ = 1, a no transformar):

bc <- boxcox(mm1, lambda = seq(-1, 2, 0.05), plotit = FALSE)   # log-verosimilitud para cada λ
lambda_opt <- bc$x[which.max(bc$y)]
lambda_opt
## [1] 0.65
ggplotly(
  ggplot(data.frame(lambda = bc$x, logLik = bc$y), aes(lambda, logLik)) +
    geom_line(color = "#2c3e50") +
    geom_vline(xintercept = lambda_opt, linetype = "dashed", color = "#e74c3c") +
    geom_vline(xintercept = c(0, 1), linetype = "dotted", color = "gray50") +
    labs(title = "Figura. Box-Cox: log-verosimilitud según λ (0 = log, 1 = niveles)",
         x = "λ", y = "Log-verosimilitud") +
    theme_minimal()
)

El λ óptimo es 0.65, entre el logaritmo (0) y los niveles (1). Esto indica que transformar puede ayudar, pero el logaritmo no es necesariamente la mejor opción. Para comprobarlo se estima el modelo múltiple 2 en logaritmos.

Modelo múltiple 2 (logarítmico)

Se aplica de nuevo step(), partiendo del modelo completo con el precio y el área en logaritmos:

modelo_completo_log <- lm(log(precio_millon) ~ log(Area_contruida) + Banos + Habitaciones + piso + Estrato + D_parq,
                          data = datos_m)
seleccion_log <- step(modelo_completo_log, direction = "both", trace = 0)
formula(seleccion_log)
## log(precio_millon) ~ log(Area_contruida) + Banos + Habitaciones + 
##     piso

En la especificación logarítmica el step elimina tanto el estrato como la dummy de parqueaderos.

Modelo poblacional

\[\ln(Precio_i) = \beta_0 + \beta_1 \ln(Area_i) + \beta_2 Banos_i + \beta_3 Habitaciones_i + \beta_4 Piso_i + u_i\]

Estimación

mm2 <- lm(log(precio_millon) ~ log(Area_contruida) + Banos + Habitaciones + piso, data = datos_m)
summary(mm2)
## 
## Call:
## lm(formula = log(precio_millon) ~ log(Area_contruida) + Banos + 
##     Habitaciones + piso, data = datos_m)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -0.61057 -0.08883  0.00184  0.10842  0.38029 
## 
## Coefficients:
##                      Estimate Std. Error t value             Pr(>|t|)    
## (Intercept)          3.037839   0.256292  11.853 < 0.0000000000000002 ***
## log(Area_contruida)  0.585374   0.062620   9.348 < 0.0000000000000002 ***
## Banos                0.117771   0.018813   6.260       0.000000001710 ***
## Habitaciones        -0.112574   0.021052  -5.348       0.000000204272 ***
## piso                 0.018840   0.002717   6.934       0.000000000036 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.1549 on 245 degrees of freedom
## Multiple R-squared:  0.6033, Adjusted R-squared:  0.5968 
## F-statistic: 93.15 on 4 and 245 DF,  p-value: < 0.00000000000000022

Interpretación

Ceteris paribus:

  • ln(Área): un aumento de 1% en el área se asocia con un aumento de 0.585% en el precio (elasticidad menor que 1).
  • Baños: cada baño adicional aumenta el precio en aproximadamente 11.8%.
  • Habitaciones: cada habitación adicional, con la misma área, reduce el precio en aproximadamente 11.3%.
  • Piso: cada piso adicional aumenta el precio en aproximadamente 1.9%.
  • Todas las variables son significativas al 1%. El R² es 0.603; no es directamente comparable con el del modelo 1 porque la variable dependiente está en otra escala (por eso la comparación se hace con el MAE más adelante).

Validación de supuestos

diagnostico(mm2)
kable(validar(mm2), caption = "Supuestos – Modelo múltiple 2 (log)") %>% kable_styling(full_width = FALSE)
Supuestos – Modelo múltiple 2 (log)
Supuesto Estadistico p_valor Conclusion
W Normalidad (Shapiro-Wilk) 0.988 0.03560 Se rechaza H0 (no cumple)
BP Homocedasticidad (Breusch-Pagan) 17.552 0.00151 Se rechaza H0 (no cumple)
DW Independencia (Durbin-Watson) 1.931 0.28600 No se rechaza H0 (cumple)
vif(mm2)
## log(Area_contruida)               Banos        Habitaciones                piso 
##            1.676325            1.835317            1.367786            1.055836

¿Es pertinente la transformación logarítmica en el modelo múltiple?

comp_mult <- data.frame(
  Modelo = c("Múltiple 1 (niveles)", "Múltiple 2 (log)"),
  n = c(nobs(mm1), nobs(mm2)),
  R2_ajustado = c(summary(mm1)$adj.r.squared, summary(mm2)$adj.r.squared),
  p_Shapiro = c(shapiro.test(residuals(mm1))$p.value, shapiro.test(residuals(mm2))$p.value),
  p_BreuschPagan = c(bptest(mm1)$p.value, bptest(mm2)$p.value),
  p_DurbinWatson = c(dwtest(mm1)$p.value, dwtest(mm2)$p.value)
)
kable(comp_mult, digits = 4, caption = "Tabla 3. Supuestos de los modelos múltiples") %>% kable_styling(full_width = FALSE)
Tabla 3. Supuestos de los modelos múltiples
Modelo n R2_ajustado p_Shapiro p_BreuschPagan p_DurbinWatson
Múltiple 1 (niveles) 250 0.6202 0.8167 0.0002 0.3037
Múltiple 2 (log) 250 0.5968 0.0356 0.0015 0.2858

En el modelo múltiple la transformación logarítmica no mejora los supuestos: la heterocedasticidad persiste en ambos modelos, y el logaritmo además hace que se pierda la normalidad de los residuos, que sí se cumplía en niveles (Shapiro-Wilk rechaza al 5% en el modelo log). Esto coincide con el resultado de Box-Cox (λ ≈ 0.7, más cerca de 1 que de 0). Por eso, en el modelo múltiple se prefiere la especificación en niveles con errores estándar robustos. El modelo log sigue siendo útil por su interpretación en porcentajes y elasticidades.

3. Predicción hipotética

Se predice el precio de un apartamento hipotético en La Flora con estas características: 100 m², 2 baños, 3 habitaciones, piso 5 y 2 parqueaderos (D_parq = 1).

nuevo <- data.frame(Area_contruida = 100, Banos = 2, Habitaciones = 3, piso = 5, D_parq = 1)

# Modelo múltiple 1 (niveles): predicción puntual e intervalos
pred_mm1_conf <- predict(mm1, newdata = nuevo, interval = "confidence", level = 0.95)
pred_mm1_pred <- predict(mm1, newdata = nuevo, interval = "prediction", level = 0.95)
pred_mm1_conf
##        fit      lwr      upr
## 1 320.0275 305.1217 334.9332
pred_mm1_pred
##        fit      lwr      upr
## 1 320.0275 220.2101 419.8449
# Modelo múltiple 2 (log): la predicción sale en ln(precio), se devuelve a millones con exp()
pred_mm2 <- predict(mm2, newdata = nuevo, interval = "prediction", level = 0.95)
exp(pred_mm2)
##       fit     lwr      upr
## 1 306.618 225.653 416.6335

Interpretación:

  • Según el modelo múltiple 1, el precio esperado de ese apartamento es de 320 millones. El intervalo de confianza del 95% para el precio promedio de apartamentos con esas características va de 305.1 a 334.9 millones. El intervalo de predicción para un apartamento individual es más amplio: de 220.2 a 419.8 millones.
  • El modelo logarítmico predice 306.6 millones, un valor muy cercano. Las dos especificaciones coinciden en que un apartamento típico de La Flora con esas características se ofrece alrededor de 313 millones, cerca de la mediana del barrio (320 millones).

4. Métricas de evaluación (MAE)

El error absoluto medio mide, en millones de pesos, cuánto se equivoca en promedio el modelo:

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

Para evaluar los modelos con datos que no usaron para estimarse, se dividen las observaciones en entrenamiento (80%) y prueba (20%). Los modelos se reestiman con el conjunto de entrenamiento y el MAE se calcula sobre el de prueba. En el modelo logarítmico, las predicciones se pasan a millones con exp() antes de calcular el error.

set.seed(123)   # semilla para que la partición sea reproducible
idx <- sample(1:nrow(datos_m), size = round(0.8 * nrow(datos_m)))
train <- datos_m[idx, ]
test  <- datos_m[-idx, ]

# Reestimar los modelos sólo con entrenamiento
mm1_tr <- lm(precio_millon ~ Area_contruida + Banos + Habitaciones + piso + D_parq, data = train)
mm2_tr <- lm(log(precio_millon) ~ log(Area_contruida) + Banos + Habitaciones + piso, data = train)

# Predicciones sobre el conjunto de prueba (en millones)
pred1_test <- predict(mm1_tr, newdata = test)
pred2_test <- exp(predict(mm2_tr, newdata = test))

# MAE dentro de muestra (con todos los datos) y fuera de muestra (prueba)
mae <- function(real, pred) mean(abs(real - pred))

tabla_mae <- data.frame(
  Modelo = c("Múltiple 1 (niveles)", "Múltiple 2 (log)"),
  MAE_dentro_muestra = c(mae(datos_m$precio_millon, fitted(mm1)),
                         mae(datos_m$precio_millon, exp(fitted(mm2)))),
  MAE_prueba = c(mae(test$precio_millon, pred1_test),
                 mae(test$precio_millon, pred2_test))
)
tabla_mae$MAE_prueba_pct_media <- 100 * tabla_mae$MAE_prueba / mean(test$precio_millon)

kable(tabla_mae, digits = 2, caption = "Tabla 4. MAE de los modelos múltiples (millones de pesos)") %>%
  kable_styling(full_width = FALSE)
Tabla 4. MAE de los modelos múltiples (millones de pesos)
Modelo MAE_dentro_muestra MAE_prueba MAE_prueba_pct_media
Múltiple 1 (niveles) 39.53 38.08 10.94
Múltiple 2 (log) 39.11 38.50 11.06
# Gráfico de valores reales vs predichos en el conjunto de prueba
g_pred <- data.frame(Real = rep(test$precio_millon, 2),
                     Predicho = c(pred1_test, pred2_test),
                     Modelo = rep(c("Múltiple 1 (niveles)", "Múltiple 2 (log)"), each = nrow(test))) %>%
  ggplot(aes(x = Real, y = Predicho, color = Modelo)) +
  geom_point(alpha = 0.7) +
  geom_abline(slope = 1, intercept = 0, linetype = "dashed") +
  scale_color_manual(values = c("#2c3e50", "#e67e22")) +
  labs(title = "Figura 4. Precio real vs predicho (conjunto de prueba)",
       x = "Precio real (millones)", y = "Precio predicho (millones)") +
  theme_minimal()

ggplotly(g_pred)   # clic en la leyenda para ocultar/mostrar cada modelo

Interpretación: en el conjunto de prueba, el modelo múltiple 1 se equivoca en promedio en 38.1 millones por apartamento (cerca del 10.9% del precio medio), y el modelo logarítmico en 38.5 millones. El modelo en niveles tiene el menor error fuera de muestra, aunque la diferencia entre ambos es pequeña. Los MAE dentro y fuera de muestra son parecidos, lo que indica que los modelos no están sobreajustados. En la Figura 4 los puntos se alejan más de la diagonal en los precios altos, que es justamente la heterocedasticidad detectada en la validación de supuestos.

Conclusiones

  1. En La Flora, el área construida es el principal determinante del precio de los apartamentos, seguida por el número de baños y la altura (piso). Con la misma área, más habitaciones reducen el precio, porque implican espacios más pequeños.
  2. En los modelos simples, la transformación logarítmica es útil sobre todo para corregir la heterocedasticidad. En el modelo de habitaciones logra que se cumplan todos los supuestos, pero ese modelo explica muy poco del precio.
  3. En el modelo múltiple, el logaritmo no mejora los supuestos: la especificación en niveles cumple normalidad e independencia y no tiene multicolinealidad. Su único incumplimiento, la heterocedasticidad, se corrige con errores estándar robustos.
  4. La dummy de parqueaderos tiene el signo esperado (positivo), pero no es significativa una vez se controla por el tamaño del apartamento.
  5. El modelo múltiple 1 explica cerca del 63% de la variación del precio y tiene un error de predicción promedio de unos 38 millones en datos nuevos. Es una herramienta razonable para estimar precios de referencia en el barrio, pero quedan factores sin medir (antigüedad, acabados, amenidades del conjunto) que explican el resto de la variación.