Introducción

El Caney es un barrio residencial del sur de Cali, de estrato 4 en su mayoría, donde conviven conjuntos de apartamentos con casas. Esa mezcla lo vuelve un buen laboratorio para la regresión: hay suficiente variedad de tamaños, tipos de inmueble y precios para preguntarle a los datos qué hace que una vivienda cueste más que otra.

En este informe se filtra la base Datos_Vivienda (8.322 ofertas de vivienda usada en Cali) por el barrio El Caney, sin filtrar por tipo de inmueble, y se resuelven dos bloques:

  1. Modelación simple: tres modelos con un solo predictor, su interpretación, la validación de supuestos y la pertinencia de una transformación logarítmica.
  2. Modelación múltiple: dos modelos con varios predictores (uno de ellos con variable dummy), validación de supuestos y de transformaciones, selección de variables paso a paso, una predicción hipotética con predict() y el cálculo del MAE.

1. Preparación de los datos

1.1 Librerías

# Si falta alguna librería, instálala una sola vez con:
# install.packages(c("readxl", "dplyr", "ggplot2", "plotly", "lmtest", "car"))

library(readxl)   # leer el archivo de Excel
library(dplyr)    # filtrar, transformar y resumir datos
library(ggplot2)  # gráficos
library(plotly)   # gráficos interactivos
library(lmtest)   # pruebas de supuestos: Breusch-Pagan, Durbin-Watson y RESET
library(car)      # VIF (multicolinealidad) y errores estándar robustos

1.2 Carga de la base

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

datos <- read_excel("Datos_Vivienda.xlsx",
    col_types = c("text", "text", "text",
        "numeric", "numeric", "numeric",
        "numeric", "numeric", "text", "text",
        "numeric", "numeric"),
    na = c("", "NA"))   # en el Excel, los faltantes vienen escritos como "NA"

# Se fijan los nombres de las 12 columnas (en el mismo orden del archivo) para que
# el código no dependa de mayúsculas o tildes. Si tu archivo ya trae estos nombres,
# esta línea no cambia nada.
names(datos) <- c("Zona", "piso", "Estrato", "precio_millon", "Area_contruida",
                  "parqueaderos", "Banos", "Habitaciones", "Tipo", "Barrio",
                  "cordenada_longitud", "Cordenada_latitud")

# Identificador único por fila, como en el ejemplo de clase
datos$id <- 1:nrow(datos)

dim(datos)
## [1] 8319   13

1.3 Unificación del nombre del barrio

En la base, un mismo barrio aparece escrito de varias formas: con o sin artículo, en mayúsculas o minúsculas, o con un apellido de sector. Antes de filtrar conviene ver todas las grafías que contienen la palabra caney:

# Versión estandarizada del nombre: minúsculas y sin espacios sobrantes
datos$barrio_std <- tolower(trimws(datos$Barrio))

# Todas las formas en que aparece El Caney en la base
table(datos$Barrio[grepl("caney", datos$barrio_std)])
## 
##          caney caney especial       el caney       El Caney 
##             88              5            208              1

“El Caney”, “el caney” y “caney” son el mismo barrio escrito de tres maneras, y “caney especial” es un sector dentro de El Caney. Si filtráramos solo por “el caney” perderíamos casi un tercio de las ofertas. Por eso el filtro busca la palabra caney en cualquier parte del nombre y luego le asigna una sola etiqueta a todo el grupo.

datos_sub <- datos %>%
  filter(grepl("caney", barrio_std)) %>%
  mutate(Barrio  = "El Caney",
         Estrato = as.numeric(Estrato),          # viene como texto desde el Excel
         Tipo    = factor(Tipo, levels = c("Apartamento", "Casa")))

nrow(datos_sub)
## [1] 302
table(datos_sub$Tipo)
## 
## Apartamento        Casa 
##         184         118

1.4 Depuración

Una vivienda en venta con cero habitaciones o cero baños es un error de digitación, y además impide aplicar logaritmos (log(0) no existe). Esos registros se revisan y se retiran:

datos_sub %>%
  filter(Habitaciones == 0 | Banos == 0) %>%
  select(id, Tipo, precio_millon, Area_contruida, Habitaciones, Banos)
id Tipo precio_millon Area_contruida Habitaciones Banos
1382 Casa 370 280 0 3
1425 Casa 395 220 0 0
1594 Apartamento 152 60 0 2
datos_sub <- datos_sub %>% filter(Habitaciones > 0, Banos > 0)
nrow(datos_sub)
## [1] 299
# Valores faltantes por variable
colSums(is.na(datos_sub[, c("precio_millon", "Area_contruida", "Habitaciones",
                            "Banos", "parqueaderos", "Estrato", "Tipo", "piso")]))
##  precio_millon Area_contruida   Habitaciones          Banos   parqueaderos 
##              0              0              0              0             63 
##        Estrato           Tipo           piso 
##              0              0             76

Quedan 299 ofertas, muy por encima del mínimo de 30. Las variables centrales (precio, área, habitaciones, baños, estrato y tipo) están completas. Faltan datos en parqueaderos (63 registros) y en piso. El piso no se usará: no aplica a las casas y está vacío en muchos registros.

1.5 Mirada exploratoria

# Resumen por tipo de vivienda y total del barrio
resumen <- datos_sub %>%
  group_by(Tipo) %>%
  summarise(ofertas = n(),
            precio_promedio = round(mean(precio_millon), 1),
            precio_mediano  = median(precio_millon),
            area_promedio   = round(mean(Area_contruida), 1),
            habitaciones    = round(mean(Habitaciones), 1),
            banos           = round(mean(Banos), 1),
            precio_m2       = round(median(precio_millon / Area_contruida), 2))

total <- datos_sub %>%
  summarise(ofertas = n(),
            precio_promedio = round(mean(precio_millon), 1),
            precio_mediano  = median(precio_millon),
            area_promedio   = round(mean(Area_contruida), 1),
            habitaciones    = round(mean(Habitaciones), 1),
            banos           = round(mean(Banos), 1),
            precio_m2       = round(median(precio_millon / Area_contruida), 2)) %>%
  mutate(Tipo = "Total El Caney")

rbind(as.data.frame(resumen), as.data.frame(total))
Tipo ofertas precio_promedio precio_mediano area_promedio habitaciones banos precio_m2
Apartamento 183 189.7 160 69.9 2.6 2.1 2.66
Casa 116 337.1 335 190.8 4.3 3.5 1.85
Total El Caney 299 246.9 230 116.8 3.3 2.7 2.50
# Correlaciones entre el precio y las variables cuantitativas
cor_tabla <- round(cor(datos_sub[, c("precio_millon", "Area_contruida", "Habitaciones",
                                     "Banos", "parqueaderos", "Estrato")],
                       use = "complete.obs"), 2)
cor_tabla
##                precio_millon Area_contruida Habitaciones Banos parqueaderos
## precio_millon           1.00           0.82         0.69  0.71         0.50
## Area_contruida          0.82           1.00         0.66  0.70         0.57
## Habitaciones            0.69           0.66         1.00  0.75         0.40
## Banos                   0.71           0.70         0.75  1.00         0.41
## parqueaderos            0.50           0.57         0.40  0.41         1.00
## Estrato                 0.23           0.07         0.09  0.15         0.00
##                Estrato
## precio_millon     0.23
## Area_contruida    0.07
## Habitaciones      0.09
## Banos             0.15
## parqueaderos      0.00
## Estrato           1.00
ggplotly(
  ggplot(datos_sub, aes(x = precio_millon)) +
    geom_histogram(binwidth = 40, fill = "#2a78d6", color = "white", alpha = 0.9) +
    labs(title = "Figura 1. Distribución del precio de la vivienda en El Caney",
         x = "Precio (millones de pesos)", y = "Número de ofertas") +
    theme_minimal()
)
ggplotly(
  ggplot(datos_sub, aes(x = Tipo, y = precio_millon, fill = Tipo)) +
    geom_boxplot(alpha = 0.8, outlier.colour = "black") +
    scale_fill_manual(values = c("Apartamento" = "#2a78d6", "Casa" = "#eb6834")) +
    labs(title = "Figura 2. Precio según el tipo de vivienda",
         x = "", y = "Precio (millones de pesos)") +
    theme_minimal()
)
ggplotly(
  ggplot(datos_sub, aes(x = Area_contruida, y = precio_millon, color = Tipo)) +
    geom_point(alpha = 0.7, size = 2) +
    geom_smooth(method = "lm", se = FALSE, linewidth = 0.8) +
    scale_color_manual(values = c("Apartamento" = "#2a78d6", "Casa" = "#eb6834")) +
    labs(title = paste("Figura 3. Precio vs. área construida  |  r =",
                       round(cor(datos_sub$precio_millon, datos_sub$Area_contruida), 2)),
         x = "Área construida (m²)", y = "Precio (millones de pesos)") +
    theme_minimal()
)

Lo que muestran los datos. El mercado de El Caney tiene dos caras. Los apartamentos son la mayoría (183 ofertas), pequeños (unos 70 m² en promedio) y con un precio mediano de 160 millones. Las casas son menos (116), casi tres veces más grandes, y su precio mediano sube a 335 millones.

El histograma tiene una cola larga a la derecha: la mitad de las ofertas cuesta menos de 230 millones, pero la cola se estira hasta 790 millones. Esa asimetría anticipa que el logaritmo del precio puede ser útil más adelante.

La Figura 3 muestra la relación más fuerte de la base. A mayor área, mayor precio, con una correlación de 0.82. Sin embargo, la nube se abre como un abanico a medida que crece el área, lo que es una primera señal de heterocedasticidad. Hay además un dato curioso: el metro cuadrado de casa sale más barato que el de apartamento (1.85 frente a 2.66 millones por m²). Las casas del barrio son grandes, y el mercado no paga cada metro adicional al mismo ritmo.

1.6 Funciones de apoyo para validar supuestos

Cada modelo se va a evaluar con las mismas cuatro pruebas formales, además de los cuatro gráficos de diagnóstico de plot(). Para no repetir código se definen tres funciones:

Supuesto Prueba Hipótesis nula (H0) Se cumple si
Normalidad de los residuos Shapiro-Wilk Los residuos son normales p > 0.05
Varianza constante Breusch-Pagan Homocedasticidad p > 0.05
Independencia Durbin-Watson No hay autocorrelación p > 0.05
Forma funcional RESET de Ramsey El modelo lineal está bien especificado p > 0.05

Una advertencia sobre Durbin-Watson: esta prueba fue pensada para datos ordenados en el tiempo. Aquí las filas siguen el orden en que vienen en el archivo, que no tiene sentido temporal, así que su resultado depende de cómo esté ordenada la base. Un p-valor bajo indica que ofertas contiguas en el archivo se parecen entre sí (por ejemplo, porque se publicaron o cargaron juntas), no que exista una dinámica en el tiempo. Se reporta, pero con esa cautela.

# 0) Formato de números y p-valores (para las tablas y para el texto)
num  <- function(x, d = 2) formatC(as.numeric(x), format = "f", digits = d)
pct  <- function(b, d = 1) formatC((exp(as.numeric(b)) - 1) * 100, format = "f", digits = d)
sgn  <- function(x, d = 3) ifelse(x < 0, paste("-", num(abs(x), d)), paste("+", num(x, d)))
pnum <- function(p) {
  if (is.character(p)) return(p)
  ifelse(p < 0.001, "< 0.001", formatC(as.numeric(p), format = "f", digits = 3))
}
pval <- function(p) { s <- pnum(p); ifelse(grepl("<", s), s, paste("=", s)) }

# 1) Las cuatro pruebas de supuestos en una sola tabla
validar_supuestos <- function(modelo) {
  pruebas <- list(shapiro.test(residuals(modelo)),
                  bptest(modelo),
                  dwtest(modelo),
                  resettest(modelo, power = 2:3))
  p   <- sapply(pruebas, function(x) x$p.value)
  est <- sapply(pruebas, function(x) unname(x$statistic))
  data.frame(Supuesto    = c("Normalidad de los residuos", "Varianza constante",
                             "Independencia de los errores", "Forma funcional lineal"),
             Prueba      = c("Shapiro-Wilk", "Breusch-Pagan", "Durbin-Watson", "RESET de Ramsey"),
             Estadistico = round(est, 3),
             p_valor     = pnum(p),
             Resultado   = ifelse(p > 0.05, "Se cumple", "No se cumple"))
}

# 2) MAE en la escala original (millones), aunque el modelo esté en logaritmos.
#    Así se pueden comparar modelos con y sin log en la misma unidad.
mae <- function(modelo) {
  y_log <- grepl("^log\\(", deparse(formula(modelo)[[2]]))
  real  <- model.response(model.frame(modelo))
  pred  <- fitted(modelo)
  if (y_log) { real <- exp(real); pred <- exp(pred) }
  mean(abs(real - pred))
}

# 3) Tabla comparativa de varios modelos
comparar_modelos <- function(modelos) {
  filas <- lapply(names(modelos), function(nombre) {
    m <- modelos[[nombre]]
    s <- validar_supuestos(m)
    data.frame(Modelo         = nombre,
               R2_ajustado    = round(summary(m)$adj.r.squared, 3),
               MAE            = round(mae(m), 1),
               p_Shapiro      = s$p_valor[1],
               p_BreuschPagan = s$p_valor[2],
               p_DurbinWatson = s$p_valor[3],
               p_RESET        = s$p_valor[4],
               Cook_max       = round(max(cooks.distance(m)), 3))
  })
  tabla <- do.call(rbind, filas)
  rownames(tabla) <- NULL
  tabla
}

# 4) Las cuatro especificaciones (con y sin logaritmo) de un modelo simple
comparar_log <- function(variable, datos) {
  formulas <- c("Lin-Lin" = paste0("precio_millon ~ ", variable),
                "Log-Lin" = paste0("log(precio_millon) ~ ", variable),
                "Lin-Log" = paste0("precio_millon ~ log(", variable, ")"),
                "Log-Log" = paste0("log(precio_millon) ~ log(", variable, ")"))
  modelos <- lapply(formulas, function(f) lm(as.formula(f), data = datos))
  comparar_modelos(modelos)
}

En las tablas comparativas, Lin-Lin es precio ~ x, Log-Lin es log(precio) ~ x, Lin-Log es precio ~ log(x) y Log-Log es log(precio) ~ log(x). El R² ajustado solo es comparable entre modelos con la misma variable dependiente. Por eso el criterio para comparar escalas distintas es el MAE en millones, junto con el cumplimiento de los supuestos.

2. Modelación simple

Se eligen los tres predictores con mayor correlación con el precio: el área construida (r = 0.82), el número de baños (r = 0.71) y el número de habitaciones (r = 0.69).

2.1 Modelo simple 1: precio ~ área construida

Estimación

mod1 <- lm(precio_millon ~ Area_contruida, data = datos_sub)
summary(mod1)
## 
## Call:
## lm(formula = precio_millon ~ Area_contruida, data = datos_sub)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -140.16  -38.69  -18.69   30.20  229.28 
## 
## Coefficients:
##                 Estimate Std. Error t value Pr(>|t|)    
## (Intercept)    127.19777    5.89249   21.59   <2e-16 ***
## Area_contruida   1.02489    0.04129   24.82   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 58.52 on 297 degrees of freedom
## Multiple R-squared:  0.6747, Adjusted R-squared:  0.6736 
## F-statistic: 616.1 on 1 and 297 DF,  p-value: < 2.2e-16
b1 <- coef(mod1)

Interpretación

La ecuación estimada es precio = 127.20 + 1.025 × área.

En El Caney, cada metro cuadrado adicional se asocia, en promedio, con 1.02 millones más en el precio de oferta. Ampliar una vivienda en 10 m² equivale, según el modelo, a unos 10.2 millones. El intercepto (127.2 millones) no describe una vivienda real, porque nadie vende un inmueble de 0 m². Funciona más bien como el piso de precio que pone la ubicación antes de contar los metros.

Ambos coeficientes son altamente significativos (p < 0.001). El R² de 0.675 dice que el área sola explica cerca del 67 % de la variación del precio, una cifra alta para un solo predictor.

Validación de supuestos

par(mfrow = c(2, 2))
plot(mod1)

par(mfrow = c(1, 1))

sup1 <- validar_supuestos(mod1)
sup1
Supuesto Prueba Estadistico p_valor Resultado
Normalidad de los residuos Shapiro-Wilk 0.930 < 0.001 No se cumple
Varianza constante Breusch-Pagan 11.133 < 0.001 No se cumple
Independencia de los errores Durbin-Watson 1.517 < 0.001 No se cumple
Forma funcional lineal RESET de Ramsey 58.274 < 0.001 No se cumple

Los gráficos confirman lo que insinuaba la nube de puntos. En Residuals vs Fitted, los residuos dibujan un arco: el modelo sobreestima las viviendas más pequeñas y las más grandes y subestima las intermedias. Una recta no describe bien la relación. Scale-Location tiene una línea roja que sube: la varianza no es constante, y Breusch-Pagan lo confirma (p < 0.001). El Q-Q plot se despega de la diagonal en la cola superior, por las casas caras que el modelo subestima, y Shapiro-Wilk rechaza la normalidad (p < 0.001). RESET (p < 0.001) avisa que una recta no es la mejor forma funcional. Ningún punto supera la distancia de Cook de 1 en Residuals vs Leverage, así que no hay una observación que por sí sola arrastre la recta.

¿Conviene una transformación logarítmica?

cmp1 <- comparar_log("Area_contruida", datos_sub)
cmp1
Modelo R2_ajustado MAE p_Shapiro p_BreuschPagan p_DurbinWatson p_RESET Cook_max
Lin-Lin 0.674 46.2 < 0.001 < 0.001 < 0.001 < 0.001 0.425
Log-Lin 0.601 53.3 < 0.001 0.529 < 0.001 < 0.001 0.233
Lin-Log 0.754 34.4 < 0.001 < 0.001 0.021 < 0.001 0.514
Log-Log 0.755 38.8 < 0.001 0.462 < 0.001 < 0.001 0.045

La tabla da una lección clara. Pasar el precio a logaritmo (Log-Lin y Log-Log) corrige la heterocedasticidad: el p-valor de Breusch-Pagan pasa de < 0.001 a 0.462 en el Log-Log. Pasar el área a logaritmo (Lin-Log y Log-Log) mejora el ajuste, porque capta que el precio crece más despacio en las viviendas grandes. El Lin-Log tiene el MAE más bajo, pero conserva la varianza no constante. El Log-Log reúne lo mejor de ambos: el R² ajustado más alto (0.755), varianza constante y un MAE de 38.8 millones, frente a 46.2 del modelo lineal.

mod1_log <- lm(log(precio_millon) ~ log(Area_contruida), data = datos_sub)
summary(mod1_log)
## 
## Call:
## lm(formula = log(precio_millon) ~ log(Area_contruida), data = datos_sub)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -0.65825 -0.12179 -0.03633  0.11585  0.77326 
## 
## Coefficients:
##                     Estimate Std. Error t value Pr(>|t|)    
## (Intercept)          2.61150    0.09368   27.88   <2e-16 ***
## log(Area_contruida)  0.61514    0.02031   30.29   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.2002 on 297 degrees of freedom
## Multiple R-squared:  0.7554, Adjusted R-squared:  0.7546 
## F-statistic: 917.5 on 1 and 297 DF,  p-value: < 2.2e-16
par(mfrow = c(2, 2))
plot(mod1_log)

par(mfrow = c(1, 1))

sup1_log <- validar_supuestos(mod1_log)
sup1_log
Supuesto Prueba Estadistico p_valor Resultado
Normalidad de los residuos Shapiro-Wilk 0.955 < 0.001 No se cumple
Varianza constante Breusch-Pagan 0.542 0.462 Se cumple
Independencia de los errores Durbin-Watson 1.592 < 0.001 No se cumple
Forma funcional lineal RESET de Ramsey 63.575 < 0.001 No se cumple
b1l <- coef(mod1_log)

Interpretación del modelo Log-Log. El coeficiente 0.615 es una elasticidad: si el área construida aumenta 1 %, el precio aumenta en promedio 0.62 %. Con un aumento del 10 % en el área, el precio sube cerca de 6.0 %. Como la elasticidad es menor que 1, el precio crece menos que proporcionalmente con el tamaño. Duplicar el área no duplica el precio: lo multiplica por 2^0.62 ≈ 1.53.

En los diagnósticos, el abanico desaparece y la línea de Scale-Location se aplana. Los residuos siguen sin ser perfectamente normales (Shapiro p < 0.001) y RESET aún detecta algo de curvatura. El arco que persiste en Residuals vs Fitted tiene una explicación: el barrio mezcla dos mercados, apartamentos y casas, que una sola variable no puede separar. Esa es la motivación de la dummy de la sección 3. Con 299 observaciones, el teorema del límite central respalda la inferencia sobre los coeficientes. Conclusión: la transformación logarítmica es pertinente y el Log-Log es el modelo simple preferido para el área.

2.2 Modelo simple 2: precio ~ baños

Estimación

mod2 <- lm(precio_millon ~ Banos, data = datos_sub)
summary(mod2)
## 
## Call:
## lm(formula = precio_millon ~ Banos, data = datos_sub)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -124.50  -50.67  -25.89   37.72  296.90 
## 
## Coefficients:
##             Estimate Std. Error t value Pr(>|t|)    
## (Intercept)   51.455     11.294   4.556 7.63e-06 ***
## Banos         73.608      3.972  18.534  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 69.87 on 297 degrees of freedom
## Multiple R-squared:  0.5363, Adjusted R-squared:  0.5347 
## F-statistic: 343.5 on 1 and 297 DF,  p-value: < 2.2e-16
b2 <- coef(mod2)

Interpretación

La ecuación es precio = 51.46 + 73.61 × baños. Cada baño adicional se asocia con 73.6 millones más en el precio. El efecto no es literal, porque instalar un baño no valoriza una casa en esa cifra. El número de baños funciona como un termómetro del tamaño y la categoría de la vivienda: un inmueble con cuatro baños suele ser una casa amplia, no un apartamento pequeño. El coeficiente es significativo (p < 0.001) y el modelo explica el 54 % de la variación del precio (R² = 0.536).

Validación de supuestos

par(mfrow = c(2, 2))
plot(mod2)

par(mfrow = c(1, 1))

sup2 <- validar_supuestos(mod2)
sup2
Supuesto Prueba Estadistico p_valor Resultado
Normalidad de los residuos Shapiro-Wilk 0.894 < 0.001 No se cumple
Varianza constante Breusch-Pagan 0.756 0.385 Se cumple
Independencia de los errores Durbin-Watson 1.586 < 0.001 No se cumple
Forma funcional lineal RESET de Ramsey 0.966 0.382 Se cumple

Como los baños toman pocos valores (1, 2, 3…), los gráficos de residuos forman columnas verticales, algo esperable con un predictor discreto. Este modelo sale mejor librado que el del área: Breusch-Pagan no rechaza la varianza constante (p = 0.385) y RESET tampoco rechaza la forma lineal (p = 0.382). La normalidad sí falla (p < 0.001), por las mismas casas caras de la cola superior del Q-Q plot.

¿Conviene una transformación logarítmica?

cmp2 <- comparar_log("Banos", datos_sub)
cmp2
Modelo R2_ajustado MAE p_Shapiro p_BreuschPagan p_DurbinWatson p_RESET Cook_max
Lin-Lin 0.535 55.9 < 0.001 0.385 < 0.001 0.382 0.386
Log-Lin 0.500 58.1 < 0.001 0.005 < 0.001 < 0.001 0.258
Lin-Log 0.524 55.7 < 0.001 0.861 < 0.001 0.010 0.334
Log-Log 0.518 54.4 < 0.001 < 0.001 < 0.001 0.066 0.042
# Se estima la versión Log-Log para revisar sus supuestos
mod2_log <- lm(log(precio_millon) ~ log(Banos), data = datos_sub)
sup2_log <- validar_supuestos(mod2_log)
sup2_log
Supuesto Prueba Estadistico p_valor Resultado
Normalidad de los residuos Shapiro-Wilk 0.951 < 0.001 No se cumple
Varianza constante Breusch-Pagan 19.655 < 0.001 No se cumple
Independencia de los errores Durbin-Watson 1.551 < 0.001 No se cumple
Forma funcional lineal RESET de Ramsey 2.745 0.066 Se cumple

Aquí el logaritmo no ayuda, y vale la pena notarlo porque no es una receta universal. El modelo lineal tiene el R² ajustado más alto (0.535) y cumple homocedasticidad y forma funcional. Las versiones con logaritmo en el precio rompen la varianza constante (Breusch-Pagan del Log-Log: p < 0.001), y el Log-Lin además empeora RESET. El Lin-Log conserva la varianza constante, pero rompe la forma funcional (RESET p = 0.010). La ganancia en MAE del Log-Log es mínima (54.4 frente a 55.9 millones) y no compensa perder un supuesto. Conclusión: para los baños se mantiene el modelo lineal sin transformar.

2.3 Modelo simple 3: precio ~ habitaciones

Estimación

mod3 <- lm(precio_millon ~ Habitaciones, data = datos_sub)
summary(mod3)
## 
## Call:
## lm(formula = precio_millon ~ Habitaciones, data = datos_sub)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -154.02  -47.90  -17.02   40.73  376.61 
## 
## Coefficients:
##              Estimate Std. Error t value Pr(>|t|)    
## (Intercept)    45.149     12.264   3.681 0.000275 ***
## Habitaciones   61.374      3.509  17.492  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 72.01 on 297 degrees of freedom
## Multiple R-squared:  0.5074, Adjusted R-squared:  0.5058 
## F-statistic:   306 on 1 and 297 DF,  p-value: < 2.2e-16
b3 <- coef(mod3)

Interpretación

La ecuación es precio = 45.15 + 61.37 × habitaciones. Cada habitación adicional se asocia con 61.4 millones más. Igual que con los baños, las habitaciones resumen el tamaño general del inmueble. El efecto es significativo (p < 0.001) y el modelo explica el 51 % de la variación (R² = 0.507), el menor de los tres modelos simples.

Validación de supuestos

par(mfrow = c(2, 2))
plot(mod3)

par(mfrow = c(1, 1))

sup3 <- validar_supuestos(mod3)
sup3
Supuesto Prueba Estadistico p_valor Resultado
Normalidad de los residuos Shapiro-Wilk 0.939 < 0.001 No se cumple
Varianza constante Breusch-Pagan 13.028 < 0.001 No se cumple
Independencia de los errores Durbin-Watson 1.766 0.021 No se cumple
Forma funcional lineal RESET de Ramsey 9.155 < 0.001 No se cumple

Este modelo incumple casi todo. La varianza crece con el número de habitaciones (Breusch-Pagan p < 0.001), los residuos no son normales (p < 0.001) y RESET rechaza la forma lineal (p < 0.001). Pasar de 2 a 3 habitaciones no vale lo mismo que pasar de 6 a 7, y una recta no puede capturar ese rendimiento decreciente.

¿Conviene una transformación logarítmica?

cmp3 <- comparar_log("Habitaciones", datos_sub)
cmp3
Modelo R2_ajustado MAE p_Shapiro p_BreuschPagan p_DurbinWatson p_RESET Cook_max
Lin-Lin 0.506 55.0 < 0.001 < 0.001 0.021 < 0.001 0.297
Log-Lin 0.504 59.6 < 0.001 0.121 < 0.001 < 0.001 0.134
Lin-Log 0.532 51.1 < 0.001 0.002 0.073 0.277 0.272
Log-Log 0.559 53.0 0.003 0.315 0.016 0.023 0.049
mod3_log <- lm(log(precio_millon) ~ log(Habitaciones), data = datos_sub)
summary(mod3_log)
## 
## Call:
## lm(formula = log(precio_millon) ~ log(Habitaciones), data = datos_sub)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -0.57451 -0.17588 -0.01412  0.17720  0.71366 
## 
## Coefficients:
##                   Estimate Std. Error t value Pr(>|t|)    
## (Intercept)        4.39082    0.05544   79.20   <2e-16 ***
## log(Habitaciones)  0.91457    0.04697   19.47   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.2683 on 297 degrees of freedom
## Multiple R-squared:  0.5607, Adjusted R-squared:  0.5593 
## F-statistic: 379.1 on 1 and 297 DF,  p-value: < 2.2e-16
par(mfrow = c(2, 2))
plot(mod3_log)

par(mfrow = c(1, 1))

sup3_log <- validar_supuestos(mod3_log)
sup3_log
Supuesto Prueba Estadistico p_valor Resultado
Normalidad de los residuos Shapiro-Wilk 0.985 0.003 No se cumple
Varianza constante Breusch-Pagan 1.009 0.315 Se cumple
Independencia de los errores Durbin-Watson 1.754 0.016 No se cumple
Forma funcional lineal RESET de Ramsey 3.837 0.023 No se cumple
b3l <- coef(mod3_log)

El Log-Log es la mejor especificación. Tiene el R² ajustado más alto (0.559 frente a 0.506), recupera la varianza constante (Breusch-Pagan p = 0.315) y acerca mucho más los residuos a la normalidad (Shapiro p = 0.003, frente a < 0.001 en el lineal). RESET todavía marca algo de curvatura (p = 0.023), aunque bastante menos que en la versión lineal.

Interpretación del modelo Log-Log. La elasticidad es 0.915: un 1 % más de habitaciones se asocia con un 0.91 % más de precio. Como las habitaciones son enteras, es más útil traducirlo a un caso concreto. Pasar de 3 a 4 habitaciones (un aumento del 33 %) eleva el precio esperado en torno a 30.1 %. Pasar de 6 a 7 (un aumento del 17 %) lo eleva solo un 15.1 %. Ese es el rendimiento decreciente que la recta no podía ver. Conclusión: la transformación logarítmica es pertinente.

2.4 Resumen de la modelación simple

data.frame(
  Modelo         = c("1. Área construida", "2. Baños", "3. Habitaciones"),
  Especificacion = c("Log-Log", "Lin-Lin", "Log-Log"),
  R2_ajustado    = round(c(summary(mod1_log)$adj.r.squared, summary(mod2)$adj.r.squared,
                           summary(mod3_log)$adj.r.squared), 3),
  MAE_millones   = round(c(mae(mod1_log), mae(mod2), mae(mod3_log)), 1)
)
Modelo Especificacion R2_ajustado MAE_millones
1. Área construida Log-Log 0.755 38.8
2. Baños Lin-Lin 0.535 55.9
3. Habitaciones Log-Log 0.559 53.0

El área construida es, por mucho, el mejor predictor individual del precio en El Caney: explica más varianza y se equivoca menos. Los tres modelos simples dejan una lección de método. El logaritmo fue la solución para el área y las habitaciones, que tienen rendimientos decrecientes y varianza creciente, pero empeoró el modelo de los baños, donde la relación ya era lineal. La transformación se valida con los datos; no se aplica por costumbre.

3. Modelación múltiple

3.1 Base común para los modelos múltiples

Para que los modelos múltiples y la selección paso a paso sean comparables, todos deben estimarse sobre las mismas filas. Se construye una base con las variables candidatas y se eliminan los registros incompletos (los faltantes vienen casi todos de parqueaderos):

datos_mod <- datos_sub %>%
  select(precio_millon, Area_contruida, Habitaciones, Banos, parqueaderos, Estrato, Tipo) %>%
  na.omit()

nrow(datos_mod)
## [1] 236
table(datos_mod$Tipo)
## 
## Apartamento        Casa 
##         130         106

Se trabaja con 236 viviendas completas.

3.2 Modelo múltiple 1: tamaño y distribución

Este primer modelo usa solo variables cuantitativas que describen el inmueble: área, habitaciones, baños y parqueaderos.

Estimación

mm1 <- lm(precio_millon ~ Area_contruida + Habitaciones + Banos + parqueaderos, data = datos_mod)
summary(mm1)
## 
## Call:
## lm(formula = precio_millon ~ Area_contruida + Habitaciones + 
##     Banos + parqueaderos, data = datos_mod)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -133.22  -37.26  -13.15   33.95  229.26 
## 
## Coefficients:
##                Estimate Std. Error t value Pr(>|t|)    
## (Intercept)    69.98498   13.30580   5.260 3.29e-07 ***
## Area_contruida  0.66957    0.06663  10.049  < 2e-16 ***
## Habitaciones   14.88324    4.78087   3.113  0.00209 ** 
## Banos          16.76691    5.59143   2.999  0.00301 ** 
## parqueaderos    8.81134    8.29081   1.063  0.28899    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 55 on 231 degrees of freedom
## Multiple R-squared:  0.7233, Adjusted R-squared:  0.7185 
## F-statistic:   151 on 4 and 231 DF,  p-value: < 2.2e-16
c1 <- coef(mm1)
p1 <- summary(mm1)$coefficients[, 4]

Interpretación

En un modelo múltiple, cada coeficiente se lee manteniendo constantes las demás variables:

  • Área construida: con igual número de habitaciones, baños y parqueaderos, cada m² adicional suma 0.67 millones. Es menos que los 1.02 del modelo simple, porque parte de lo que antes se atribuía al área lo explican ahora habitaciones y baños.
  • Habitaciones: con la misma área, una habitación adicional suma 14.9 millones (p = 0.002).
  • Baños: cada baño adicional suma 16.8 millones (p = 0.003).
  • Parqueaderos: el coeficiente (8.8) no es significativo (p = 0.289). Una vez se conoce el tamaño de la vivienda, el número de parqueaderos no aporta información adicional sobre el precio.

El modelo explica el 71.9 % de la variación del precio (R² ajustado), más que cualquiera de los modelos simples lineales.

Validación de supuestos

par(mfrow = c(2, 2))
plot(mm1)

par(mfrow = c(1, 1))

sup_mm1 <- validar_supuestos(mm1)
sup_mm1
Supuesto Prueba Estadistico p_valor Resultado
Normalidad de los residuos Shapiro-Wilk 0.944 < 0.001 No se cumple
Varianza constante Breusch-Pagan 16.574 0.002 No se cumple
Independencia de los errores Durbin-Watson 1.674 0.006 No se cumple
Forma funcional lineal RESET de Ramsey 26.809 < 0.001 No se cumple
# Multicolinealidad: un VIF mayor a 5 sería preocupante
vif(mm1)
## Area_contruida   Habitaciones          Banos   parqueaderos 
##       2.589264       2.521327       2.787338       1.471493

El VIF de todas las variables está por debajo de 5, así que, aunque área, baños y habitaciones están correlacionadas, la multicolinealidad no infla los errores estándar de manera grave. Los supuestos sobre el error, en cambio, fallan: hay heterocedasticidad (Breusch-Pagan p = 0.002), no normalidad (p < 0.001) y problemas de forma funcional (RESET p < 0.001). El patrón es el mismo del modelo simple del área.

Validación de la transformación logarítmica

Siguiendo lo aprendido en la modelación simple, se toma el logaritmo del precio y del área. Habitaciones, baños y parqueaderos se dejan en niveles, porque son conteos pequeños.

mm1_log <- lm(log(precio_millon) ~ log(Area_contruida) + Habitaciones + Banos + parqueaderos,
              data = datos_mod)
summary(mm1_log)
## 
## Call:
## lm(formula = log(precio_millon) ~ log(Area_contruida) + Habitaciones + 
##     Banos + parqueaderos, data = datos_mod)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -0.57829 -0.13154 -0.04265  0.11960  0.63988 
## 
## Coefficients:
##                     Estimate Std. Error t value Pr(>|t|)    
## (Intercept)         2.987844   0.123767  24.141   <2e-16 ***
## log(Area_contruida) 0.487720   0.035969  13.559   <2e-16 ***
## Habitaciones        0.039083   0.017419   2.244   0.0258 *  
## Banos               0.033818   0.019793   1.709   0.0889 .  
## parqueaderos        0.005475   0.028758   0.190   0.8492    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.1958 on 231 degrees of freedom
## Multiple R-squared:  0.7612, Adjusted R-squared:  0.7571 
## F-statistic: 184.1 on 4 and 231 DF,  p-value: < 2.2e-16
par(mfrow = c(2, 2))
plot(mm1_log)

par(mfrow = c(1, 1))

validar_supuestos(mm1_log)
Supuesto Prueba Estadistico p_valor Resultado
Normalidad de los residuos Shapiro-Wilk 0.971 < 0.001 No se cumple
Varianza constante Breusch-Pagan 2.576 0.631 Se cumple
Independencia de los errores Durbin-Watson 1.677 0.006 No se cumple
Forma funcional lineal RESET de Ramsey 55.886 < 0.001 No se cumple
comparar_modelos(list("MM1 lineal" = mm1, "MM1 log" = mm1_log))
Modelo R2_ajustado MAE p_Shapiro p_BreuschPagan p_DurbinWatson p_RESET Cook_max
MM1 lineal 0.719 42.9 < 0.001 0.002 0.006 < 0.001 0.266
MM1 log 0.757 41.1 < 0.001 0.631 0.006 < 0.001 0.162
c1l <- coef(mm1_log)
p1l <- summary(mm1_log)$coefficients[, 4]

La versión logarítmica gana en todos los frentes. Sube el R² ajustado, baja el MAE y la heterocedasticidad desaparece (Breusch-Pagan pasa de 0.002 a 0.631). La distancia de Cook máxima también baja, señal de que las casas grandes pesan menos sobre la estimación.

Interpretación (versión log). Con habitaciones, baños y parqueaderos constantes, un 10 % más de área se asocia con un 4.8 % más de precio. Cada habitación adicional suma un 4.0 % al precio (p = 0.026). El baño adicional (3.4 %, p = 0.089) y los parqueaderos (p = 0.849) pierden significancia al 5 %: en logaritmos, el área absorbe casi toda la información de tamaño.

3.3 Modelo múltiple 2: con variables dummy

El segundo modelo cambia de enfoque. Además del tamaño (área, baños y parqueaderos), incorpora qué se vende y dónde en términos socioeconómicos:

  • Tipo es una variable cualitativa que R convierte en una dummy: TipoCasa = 1 si el inmueble es casa y 0 si es apartamento. La categoría base es Apartamento.
  • Estrato entra como variable numérica (3 a 6) y mide el salto de precio por cada nivel.
# Codificación de la dummy: Apartamento es la base (fila de ceros)
contrasts(datos_mod$Tipo)
##             Casa
## Apartamento    0
## Casa           1

Estimación

mm2 <- lm(precio_millon ~ Area_contruida + Banos + parqueaderos + Tipo + Estrato, data = datos_mod)
summary(mm2)
## 
## Call:
## lm(formula = precio_millon ~ Area_contruida + Banos + parqueaderos + 
##     Tipo + Estrato, data = datos_mod)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -114.50  -34.56  -12.38   33.85  263.39 
## 
## Coefficients:
##                 Estimate Std. Error t value Pr(>|t|)    
## (Intercept)    -61.60094   32.93421  -1.870 0.062695 .  
## Area_contruida   0.62564    0.06791   9.213  < 2e-16 ***
## Banos           16.33529    4.93152   3.312 0.001074 ** 
## parqueaderos    13.93999    7.95923   1.751 0.081206 .  
## TipoCasa        37.83930   10.48183   3.610 0.000376 ***
## Estrato         39.95840    7.80675   5.118  6.5e-07 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 52.58 on 230 degrees of freedom
## Multiple R-squared:  0.7482, Adjusted R-squared:  0.7427 
## F-statistic: 136.7 on 5 and 230 DF,  p-value: < 2.2e-16
c2 <- coef(mm2)
p2 <- summary(mm2)$coefficients[, 4]

Interpretación

  • TipoCasa (dummy): con igual área, baños, parqueaderos y estrato, una casa se ofrece en promedio 37.8 millones más cara que un apartamento (p < 0.001). Comparadas metro a metro, las casas parecían más baratas. Ese resultado cambia de signo al controlar por el tamaño: a igualdad de condiciones, la casa tiene una prima, probablemente por el lote, la privacidad y la ausencia de cuota de administración.
  • Estrato: cada nivel adicional suma 40.0 millones (p < 0.001). Aunque casi todo El Caney es estrato 4, las ofertas de estrato 5 se cotizan claramente por encima.
  • Área construida: cada m² suma 0.63 millones, con las demás variables constantes.
  • Baños: cada baño suma 16.3 millones (p = 0.001).
  • Parqueaderos: 13.9 millones, significativo solo al 10 % (p = 0.081).
  • El intercepto es negativo, pero carece de interpretación práctica: correspondería a un apartamento de 0 m², sin baños y de estrato 0.

El modelo explica el 74.3 % de la variación del precio (R² ajustado), más que el modelo múltiple 1, aunque no incluye las habitaciones. El tipo de inmueble y el estrato aportan información que el tamaño no contiene.

Validación de supuestos

par(mfrow = c(2, 2))
plot(mm2)

par(mfrow = c(1, 1))

sup_mm2 <- validar_supuestos(mm2)
sup_mm2
Supuesto Prueba Estadistico p_valor Resultado
Normalidad de los residuos Shapiro-Wilk 0.937 < 0.001 No se cumple
Varianza constante Breusch-Pagan 11.346 0.045 No se cumple
Independencia de los errores Durbin-Watson 1.793 0.053 Se cumple
Forma funcional lineal RESET de Ramsey 18.027 < 0.001 No se cumple
vif(mm2)
## Area_contruida          Banos   parqueaderos           Tipo        Estrato 
##       2.942175       2.371985       1.483589       2.320447       1.069346

La multicolinealidad es baja (VIF < 5). La heterocedasticidad es más leve que en el modelo 1, pero todavía rechaza al 5 % (Breusch-Pagan p = 0.045). La normalidad y la forma funcional siguen fallando.

Validación de la transformación logarítmica

mm2_log <- lm(log(precio_millon) ~ log(Area_contruida) + Banos + parqueaderos + Tipo + Estrato,
              data = datos_mod)
summary(mm2_log)
## 
## Call:
## lm(formula = log(precio_millon) ~ log(Area_contruida) + Banos + 
##     parqueaderos + Tipo + Estrato, data = datos_mod)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -0.5147 -0.1304 -0.0244  0.1250  0.6293 
## 
## Coefficients:
##                     Estimate Std. Error t value Pr(>|t|)    
## (Intercept)          2.54680    0.16937  15.036  < 2e-16 ***
## log(Area_contruida)  0.45759    0.04071  11.241  < 2e-16 ***
## Banos                0.04058    0.01716   2.365   0.0189 *  
## parqueaderos         0.02769    0.02788   0.993   0.3216    
## TipoCasa             0.07505    0.04229   1.774   0.0773 .  
## Estrato              0.15297    0.02847   5.374 1.89e-07 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.187 on 230 degrees of freedom
## Multiple R-squared:  0.7833, Adjusted R-squared:  0.7786 
## F-statistic: 166.3 on 5 and 230 DF,  p-value: < 2.2e-16
par(mfrow = c(2, 2))
plot(mm2_log)

par(mfrow = c(1, 1))

validar_supuestos(mm2_log)
Supuesto Prueba Estadistico p_valor Resultado
Normalidad de los residuos Shapiro-Wilk 0.976 < 0.001 No se cumple
Varianza constante Breusch-Pagan 2.369 0.796 Se cumple
Independencia de los errores Durbin-Watson 1.839 0.102 Se cumple
Forma funcional lineal RESET de Ramsey 37.118 < 0.001 No se cumple
comparar_modelos(list("MM2 lineal" = mm2, "MM2 log" = mm2_log))
Modelo R2_ajustado MAE p_Shapiro p_BreuschPagan p_DurbinWatson p_RESET Cook_max
MM2 lineal 0.743 40.4 < 0.001 0.045 0.053 < 0.001 0.434
MM2 log 0.779 38.7 < 0.001 0.796 0.102 < 0.001 0.171
c2l <- coef(mm2_log)
p2l <- summary(mm2_log)$coefficients[, 4]

Otra vez la versión en logaritmos es superior. Tiene un R² ajustado de 0.779, el MAE más bajo hasta ahora (38.7 millones) y varianza constante (Breusch-Pagan p = 0.796).

Interpretación (versión log). En un modelo con el precio en logaritmo, el coeficiente de una dummy se traduce en porcentaje con (e^b − 1) × 100:

  • TipoCasa: una casa vale un 7.8 % más que un apartamento comparable (p = 0.077). La prima se reduce y queda en el límite de la significancia, porque el logaritmo del área ya recoge buena parte de la diferencia entre ambos tipos.
  • Estrato: cada nivel adicional eleva el precio un 16.5 % (p < 0.001), el efecto más nítido del modelo después del área.
  • Área: un 10 % más de área equivale a un 4.5 % más de precio.
  • Baños: cada baño suma un 4.1 % (p = 0.019). Los parqueaderos no son significativos (p = 0.322).

3.4 Selección de variables paso a paso (stepwise)

Los dos modelos anteriores se armaron con criterio propio. La selección paso a paso deja que los datos decidan. Se parte del modelo completo (todas las variables candidatas, en la especificación logarítmica validada) y en cada paso se quita o se agrega la variable que más reduce el AIC (criterio de información de Akaike). El proceso se detiene cuando ningún cambio mejora el AIC.

modelo_completo <- lm(log(precio_millon) ~ log(Area_contruida) + Habitaciones + Banos +
                        parqueaderos + Tipo + Estrato, data = datos_mod)

modelo_step <- step(modelo_completo, direction = "both", trace = 1)
## Start:  AIC=-789.53
## log(precio_millon) ~ log(Area_contruida) + Habitaciones + Banos + 
##     parqueaderos + Tipo + Estrato
## 
##                       Df Sum of Sq     RSS     AIC
## - parqueaderos         1    0.0284  7.8668 -790.68
## - Banos                1    0.0332  7.8716 -790.53
## <none>                              7.8384 -789.53
## - Tipo                 1    0.0746  7.9131 -789.30
## - Habitaciones         1    0.2020  8.0404 -785.53
## - Estrato              1    1.0207  8.8592 -762.64
## - log(Area_contruida)  1    3.8858 11.7242 -696.51
## 
## Step:  AIC=-790.68
## log(precio_millon) ~ log(Area_contruida) + Habitaciones + Banos + 
##     Tipo + Estrato
## 
##                       Df Sum of Sq     RSS     AIC
## - Banos                1    0.0374  7.9042 -791.56
## - Tipo                 1    0.0627  7.9295 -790.80
## <none>                              7.8668 -790.68
## + parqueaderos         1    0.0284  7.8384 -789.53
## - Habitaciones         1    0.2081  8.0749 -786.52
## - Estrato              1    0.9937  8.8605 -764.61
## - log(Area_contruida)  1    4.8156 12.6824 -679.97
## 
## Step:  AIC=-791.56
## log(precio_millon) ~ log(Area_contruida) + Habitaciones + Tipo + 
##     Estrato
## 
##                       Df Sum of Sq     RSS     AIC
## <none>                              7.9042 -791.56
## - Tipo                 1    0.0866  7.9908 -790.99
## + Banos                1    0.0374  7.8668 -790.68
## + parqueaderos         1    0.0326  7.8716 -790.53
## - Habitaciones         1    0.3837  8.2880 -782.37
## - Estrato              1    1.0672  8.9715 -763.67
## - log(Area_contruida)  1    5.2437 13.1480 -673.47
# Resumen de los pasos realizados
modelo_step$anova
Step Df Deviance Resid. Df Resid. Dev AIC
NA NA 229 7.838436 -789.5310
- parqueaderos 1 0.0283702 230 7.866806 -790.6784
- Banos 1 0.0374345 231 7.904241 -791.5580
# Comprobación: la selección hacia adelante, partiendo del modelo vacío
modelo_nulo <- lm(log(precio_millon) ~ 1, data = datos_mod)
modelo_fwd  <- step(modelo_nulo, scope = formula(modelo_completo), direction = "forward", trace = 0)
formula(modelo_fwd)
## log(precio_millon) ~ log(Area_contruida) + Estrato + Habitaciones + 
##     Tipo
summary(modelo_step)
## 
## Call:
## lm(formula = log(precio_millon) ~ log(Area_contruida) + Habitaciones + 
##     Tipo + Estrato, data = datos_mod)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -0.53771 -0.13173 -0.02293  0.11974  0.65873 
## 
## Coefficients:
##                     Estimate Std. Error t value Pr(>|t|)    
## (Intercept)          2.51096    0.16293  15.412  < 2e-16 ***
## log(Area_contruida)  0.46045    0.03720  12.379  < 2e-16 ***
## Habitaciones         0.04906    0.01465   3.349 0.000948 ***
## TipoCasa             0.06500    0.04087   1.590 0.113093    
## Estrato              0.15446    0.02766   5.585 6.55e-08 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.185 on 231 degrees of freedom
## Multiple R-squared:  0.787,  Adjusted R-squared:  0.7833 
## F-statistic: 213.3 on 4 and 231 DF,  p-value: < 2.2e-16
cs <- coef(modelo_step)
ps <- summary(modelo_step)$coefficients[, 4]

Qué decidió el algoritmo. En el primer paso salió parqueaderos, la variable que menos aportaba (ya era no significativa en todos los modelos). En el segundo salió baños, cuya información queda cubierta por el área y las habitaciones. El AIC bajó de -789.5 a -791.6. La selección hacia adelante, que parte del modelo vacío, llega al mismo conjunto de variables, lo que da confianza en el resultado.

El modelo final es:

log(precio) = 2.511 + 0.460 · log(área) + 0.049 · habitaciones + 0.065 · Casa + 0.154 · estrato

  • Un 10 % más de área se asocia con un 4.5 % más de precio.
  • Cada habitación adicional suma un 5.0 % (p < 0.001).
  • Cada nivel de estrato suma un 16.7 % (p < 0.001).
  • La dummy Casa (6.7 %) se conserva aunque su p-valor es 0.113. El AIC es menos exigente que el umbral de 0.05: retiene una variable si mejora el balance entre ajuste y complejidad, aunque su efecto individual no sea concluyente.

Validación de supuestos del modelo seleccionado

par(mfrow = c(2, 2))
plot(modelo_step)

par(mfrow = c(1, 1))

sup_step <- validar_supuestos(modelo_step)
sup_step
Supuesto Prueba Estadistico p_valor Resultado
Normalidad de los residuos Shapiro-Wilk 0.976 < 0.001 No se cumple
Varianza constante Breusch-Pagan 2.797 0.592 Se cumple
Independencia de los errores Durbin-Watson 1.831 0.092 Se cumple
Forma funcional lineal RESET de Ramsey 27.936 < 0.001 No se cumple
vif(modelo_step)
## log(Area_contruida)        Habitaciones                Tipo             Estrato 
##            3.194630            2.092598            2.849919            1.084467
# Errores estándar robustos (HC3): una red de seguridad ante la falta de normalidad
coeftest(modelo_step, vcov. = hccm(modelo_step, type = "hc3"))
## 
## t test of coefficients:
## 
##                     Estimate Std. Error t value  Pr(>|t|)    
## (Intercept)         2.510964   0.192059 13.0739 < 2.2e-16 ***
## log(Area_contruida) 0.460453   0.051395  8.9591 < 2.2e-16 ***
## Habitaciones        0.049059   0.017899  2.7409  0.006607 ** 
## TipoCasa            0.064998   0.052301  1.2428  0.215210    
## Estrato             0.154465   0.029812  5.1812 4.801e-07 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

El modelo seleccionado mantiene la varianza constante (Breusch-Pagan p = 0.592) y un VIF bajo. La normalidad (p < 0.001) y la forma funcional (RESET p < 0.001) siguen sin cumplirse del todo, una limitación común en datos de precios. Por eso se recalculan los errores estándar de forma robusta (HC3). Las conclusiones no cambian: área, habitaciones y estrato siguen siendo significativos, y la dummy Casa sigue sin serlo.

3.5 Predicción hipotética

¿Cuánto debería pedirse por dos viviendas típicas de El Caney? Se usa el modelo seleccionado con predict(). Como el modelo está en logaritmos, la predicción se devuelve a millones con exp(). Así se obtiene el precio mediano esperado, que es justo la cantidad que minimiza el error absoluto (MAE).

nuevas <- data.frame(
  Tipo           = factor(c("Apartamento", "Casa"), levels = levels(datos_mod$Tipo)),
  Area_contruida = c(80, 160),
  Habitaciones   = c(3, 4),
  Banos          = c(2, 3),
  parqueaderos   = c(1, 2),
  Estrato        = c(4, 4))

pred_conf <- exp(predict(modelo_step, newdata = nuevas, interval = "confidence", level = 0.95))
pred_pred <- exp(predict(modelo_step, newdata = nuevas, interval = "prediction", level = 0.95))

data.frame(nuevas,
           Precio_estimado = round(pred_pred[, "fit"], 1),
           IC95_media_inf  = round(pred_conf[, "lwr"], 1),
           IC95_media_sup  = round(pred_conf[, "upr"], 1),
           IP95_inf        = round(pred_pred[, "lwr"], 1),
           IP95_sup        = round(pred_pred[, "upr"], 1))
Tipo Area_contruida Habitaciones Banos parqueaderos Estrato Precio_estimado IC95_media_inf IC95_media_sup IP95_inf IP95_sup
Apartamento 80 3 2 1 4 199.1 192.2 206.2 138.0 287.1
Casa 160 4 3 2 4 307.0 296.0 318.5 212.9 442.9

Interpretación. Para un apartamento de 80 m², 3 habitaciones, estrato 4, el modelo estima un precio de 199 millones. Para una casa de 160 m², 4 habitaciones, estrato 4, la estimación sube a 307 millones.

Los dos intervalos responden preguntas distintas. El intervalo de confianza (192 a 206 millones para el apartamento) es estrecho porque estima el precio promedio de todos los apartamentos con esas características. El intervalo de predicción (138 a 287 millones) es mucho más ancho porque se refiere a una oferta concreta, que además depende de los acabados, la vista, el conjunto y la urgencia del vendedor. Un aviso que pida más de 287 millones por ese apartamento estaría por encima de lo que sugiere el mercado del barrio.

Baños y parqueaderos se incluyen en nuevas por completitud, pero el modelo seleccionado no los usa.

3.6 Métricas de evaluación: MAE

El MAE (error absoluto medio) mide, en millones de pesos, cuánto se equivoca el modelo en promedio. Se calcula de dos formas: sobre los mismos datos con que se estimó cada modelo (dentro de muestra), y con validación cruzada de 10 pliegues (fuera de muestra). En la validación cruzada, la base se divide en 10 partes. Cada parte se usa una vez como prueba mientras el modelo se estima con las otras nueve, de modo que cada vivienda se predice con un modelo que nunca la vio. Es la prueba honesta, y es más estable que un único corte entre entrenamiento y prueba.

# Referencia: el mejor modelo simple, reestimado en la misma base
ref_simple <- lm(log(precio_millon) ~ log(Area_contruida), data = datos_mod)

modelos_finales <- list("Simple Log-Log (área)" = ref_simple,
                        "MM1 lineal"            = mm1,
                        "MM1 log"               = mm1_log,
                        "MM2 lineal (dummy)"    = mm2,
                        "MM2 log (dummy)"       = mm2_log,
                        "Stepwise (log)"        = modelo_step)

# MAE fuera de muestra con validación cruzada de 10 pliegues
set.seed(123)
k <- 10
pliegues <- sample(rep(1:k, length.out = nrow(datos_mod)))

mae_cv <- function(modelo) {
  errores <- c()
  for (i in 1:k) {
    entrenamiento <- datos_mod[pliegues != i, ]
    prueba        <- datos_mod[pliegues == i, ]
    m    <- lm(formula(modelo), data = entrenamiento)
    pred <- predict(m, newdata = prueba)
    if (grepl("^log\\(", deparse(formula(modelo)[[2]]))) pred <- exp(pred)
    errores <- c(errores, abs(prueba$precio_millon - pred))
  }
  mean(errores)
}

tabla_mae <- data.frame(
  Modelo            = names(modelos_finales),
  R2_ajustado       = sapply(modelos_finales, function(m) round(summary(m)$adj.r.squared, 3)),
  MAE_dentro        = sapply(modelos_finales, function(m) round(mae(m), 1)),
  MAE_val_cruzada   = sapply(modelos_finales, function(m) round(mae_cv(m), 1)))
tabla_mae$MAE_relativo_pct <- round(100 * sapply(modelos_finales, mae) / mean(datos_mod$precio_millon), 1)
rownames(tabla_mae) <- NULL
tabla_mae
Modelo R2_ajustado MAE_dentro MAE_val_cruzada MAE_relativo_pct
Simple Log-Log (área) 0.744 40.9 41.2 15.5
MM1 lineal 0.719 42.9 44.0 16.2
MM1 log 0.757 41.1 42.3 15.6
MM2 lineal (dummy) 0.743 40.4 41.8 15.3
MM2 log (dummy) 0.779 38.7 40.3 14.6
Stepwise (log) 0.783 38.9 39.8 14.7
ggplotly(
  ggplot(data.frame(real = datos_mod$precio_millon, estimado = exp(fitted(modelo_step)),
                    Tipo = datos_mod$Tipo),
         aes(x = estimado, y = real, color = Tipo)) +
    geom_point(alpha = 0.7, size = 2) +
    geom_abline(slope = 1, intercept = 0, linetype = "dashed", color = "grey40") +
    scale_color_manual(values = c("Apartamento" = "#2a78d6", "Casa" = "#eb6834")) +
    labs(title = "Figura 4. Precio real vs. estimado (modelo stepwise)",
         x = "Precio estimado (millones)", y = "Precio real (millones)") +
    theme_minimal()
)

Interpretación. El modelo stepwise se equivoca en promedio 38.9 millones por vivienda, alrededor del 14.7 % del precio medio del barrio (264.1 millones). El mejor modelo simple fallaba por 40.9 millones, así que añadir habitaciones, tipo y estrato recorta el error en 2.0 millones por vivienda. La mejora es modesta porque el área ya hacía casi todo el trabajo, pero el R² ajustado sube de 0.744 a 0.783 y el modelo gana en interpretación.

En validación cruzada, el MAE del modelo stepwise es de 39.8 millones, apenas por encima del valor dentro de muestra, así que el modelo no está sobreajustado. Además, es el más bajo de la tabla: fuera de muestra, el stepwise es el que mejor predice. Los modelos con dummy (MM2 y stepwise) le ganan al MM1, sobre todo fuera de muestra: saber si es casa o apartamento y en qué estrato está ayuda a predecir mejor que sumar más medidas de tamaño. Entre el MM2 log y el stepwise la diferencia de MAE es mínima, pero el stepwise usa una variable menos, y ante dos modelos casi igual de precisos conviene el más simple.

En la Figura 4, los puntos se alinean alrededor de la diagonal y se abren en los precios altos. Las casas más caras son las más difíciles de predecir.

4. Conclusiones

  1. El tamaño manda, pero con rendimientos decrecientes. El área construida es el mejor predictor individual del precio en El Caney. La relación se describe mejor en logaritmos: cada 10 % de área adicional se traduce en cerca de un 4.5 % más de precio en el modelo final. Los metros extra valen menos en las viviendas grandes.
  2. El logaritmo se valida, no se asume. Corrigió la heterocedasticidad del área, de las habitaciones y de los modelos múltiples, pero empeoró el modelo de los baños, que ya era lineal.
  3. La dummy cuenta una historia que el promedio esconde. Metro a metro, la casa parece más barata. Al comparar viviendas del mismo tamaño y estrato, la diferencia se invierte o se diluye. El estrato resultó ser el segundo factor más importante: cada nivel adicional eleva el precio en torno a un 17 %.
  4. El modelo final (log del área, habitaciones, tipo y estrato) explica cerca del 78 % de la variación del precio y se equivoca, en promedio, en unos 39 millones dentro de muestra y 40 en validación cruzada.
  5. Limitaciones. Los residuos no son del todo normales y RESET sugiere que queda estructura sin capturar, por ejemplo un efecto del área distinto para casas y apartamentos (una interacción Tipo:Area) o variables ausentes como la antigüedad, los acabados o el conjunto cerrado. Los errores robustos protegen las conclusiones actuales; incorporar esas variables o interacciones es el siguiente paso para mejorar el modelo.