Introducción

María fundó C&A (Casas y Apartamentos), una agencia de bienes raíces en Cali. Ha recibido una solicitud de una empresa internacional que desea adquirir dos viviendas para ubicar a sus empleados en la ciudad. Este informe aplica técnicas de Regresión Lineal Múltiple (RLM) para predecir los precios de vivienda y apoyar la toma de decisión.

Las condiciones de las solicitudes son:

Característica Vivienda 1 Vivienda 2
Tipo Casa Apartamento
Área construida (m²) 200 300
Parqueaderos 1 3
Baños 2 3
Habitaciones 4 5
Estrato 4 o 5 5 o 6
Zona Norte Sur
Crédito pre-aprobado 350 millones COP 850 millones COP

Configuración e instalación de paquetes

library(tidyverse)
library(plotly)
library(leaflet)
library(corrplot)
library(lmtest)
library(car)
library(nortest)
library(knitr)
library(kableExtra)

# Cargar datos desde CSV (debe estar en la misma carpeta que este .Rmd)
vivienda <- read.csv("vivienda.csv")

# Convertir columnas numéricas
vivienda <- vivienda %>%
  mutate(
    preciom      = as.numeric(preciom),
    areaconst    = as.numeric(areaconst),
    estrato      = as.numeric(estrato),
    banios       = as.numeric(banios),
    habitaciones = as.numeric(habitaciones),
    parqueaderos = as.numeric(parqueaderos),
    longitud     = as.numeric(longitud),
    latitud      = as.numeric(latitud)
  )

VIVIENDA 1: Casa – Zona Norte


Paso 1: Filtrado de datos y mapa

# Filtrar: Casas en Zona Norte
base1 <- vivienda %>%
  filter(tipo == "Casa", zona == "Zona Norte")

# Primeros 3 registros
base1 %>%
  head(3) %>%
  kable(caption = "Primeros 3 registros – Casas Zona Norte") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE)
Primeros 3 registros – Casas Zona Norte
id zona piso estrato preciom areaconst parqueaderos banios habitaciones tipo barrio longitud latitud
1209 Zona Norte 2 5 320 150 2 4 6 Casa acopi -76.51341 3.47968
1592 Zona Norte 2 5 780 380 2 3 3 Casa acopi -76.51674 3.48721
4057 Zona Norte 2 6 750 445 NA 7 6 Casa acopi -76.52950 3.38527
# Tabla de verificación por tipo
table(base1$tipo) %>%
  kable(col.names = c("Tipo", "Frecuencia"),
        caption = "Verificación: solo Casas") %>%
  kable_styling(bootstrap_options = "striped", full_width = FALSE)
Verificación: solo Casas
Tipo Frecuencia
Casa 722
# Tabla de verificación por zona
table(base1$zona) %>%
  kable(col.names = c("Zona", "Frecuencia"),
        caption = "Verificación: solo Zona Norte") %>%
  kable_styling(bootstrap_options = "striped", full_width = FALSE)
Verificación: solo Zona Norte
Zona Frecuencia
Zona Norte 722
cat("Total de registros en base1:", nrow(base1), "\n")
## Total de registros en base1: 722
# Mapa interactivo con leaflet
leaflet(base1) %>%
  addTiles() %>%
  addCircleMarkers(
    lng = ~longitud,
    lat = ~latitud,
    radius = 5,
    color = "#2c7bb6",
    fillOpacity = 0.7,
    popup = ~paste0(
      "<b>Barrio:</b> ", barrio, "<br>",
      "<b>Precio:</b> $", preciom, " millones<br>",
      "<b>Área:</b> ", areaconst, " m²<br>",
      "<b>Estrato:</b> ", estrato, "<br>",
      "<b>Zona:</b> ", zona
    )
  ) %>%
  addLegend("bottomright",
            colors = "#2c7bb6",
            labels = "Casas – Zona Norte",
            title = "Base 1")

Se pueden observar algunos puntos fuera del área geográfica de la Zona Norte. Esto puede deberse a errores en la clasificación de la zona en la base de datos, o a viviendas ubicadas en límites fronterizos entre zonas. Es importante tenerlo en cuenta para interpretar los resultados con cautela.


Paso 2: Análisis exploratorio de datos

# Resumen estadístico de variables clave
base1 %>%
  select(preciom, areaconst, estrato, banios, habitaciones, parqueaderos) %>%
  summary() %>%
  kable(caption = "Resumen estadístico – Variables del modelo (base1)") %>%
  kable_styling(bootstrap_options = "striped", full_width = FALSE)
Resumen estadístico – Variables del modelo (base1)
preciom areaconst estrato banios habitaciones parqueaderos
Min. : 89.0 Min. : 30.0 Min. :3.000 Min. : 0.000 Min. : 0.000 Min. : 1.000
1st Qu.: 261.2 1st Qu.: 140.0 1st Qu.:3.000 1st Qu.: 2.000 1st Qu.: 3.000 1st Qu.: 1.000
Median : 390.0 Median : 240.0 Median :4.000 Median : 3.000 Median : 4.000 Median : 2.000
Mean : 445.9 Mean : 264.9 Mean :4.202 Mean : 3.555 Mean : 4.507 Mean : 2.182
3rd Qu.: 550.0 3rd Qu.: 336.8 3rd Qu.:5.000 3rd Qu.: 4.000 3rd Qu.: 5.000 3rd Qu.: 3.000
Max. :1940.0 Max. :1440.0 Max. :6.000 Max. :10.000 Max. :10.000 Max. :10.000
NA NA NA NA NA NA’s :287

Precio vs Área construida

p1 <- plot_ly(base1,
              x = ~areaconst, y = ~preciom,
              type = "scatter", mode = "markers",
              marker = list(color = "#2c7bb6", opacity = 0.6),
              text = ~paste("Barrio:", barrio,
                            "<br>Precio:", preciom,
                            "<br>Área:", areaconst)) %>%
  layout(title = "Precio vs Área Construida – Casas Zona Norte",
         xaxis = list(title = "Área construida (m²)"),
         yaxis = list(title = "Precio (millones COP)"))
p1

Se observa una relación positiva entre el área construida y el precio. A mayor área, mayor precio, lo cual es lógico en el mercado inmobiliario.

Precio vs Estrato

p2 <- plot_ly(base1,
              x = ~factor(estrato), y = ~preciom,
              type = "box",
              color = ~factor(estrato),
              colors = "Blues") %>%
  layout(title = "Distribución de Precio por Estrato – Casas Zona Norte",
         xaxis = list(title = "Estrato"),
         yaxis = list(title = "Precio (millones COP)"))
p2

Los estratos más altos presentan precios medianos superiores, confirmando que el estrato socioeconómico es un determinante importante del precio.

Precio vs Número de Baños

p3 <- plot_ly(base1,
              x = ~factor(banios), y = ~preciom,
              type = "box",
              color = ~factor(banios),
              colors = "Greens") %>%
  layout(title = "Precio por Número de Baños – Casas Zona Norte",
         xaxis = list(title = "Número de baños"),
         yaxis = list(title = "Precio (millones COP)"))
p3

Las viviendas con más baños tienden a tener precios más altos, lo cual refleja mayor tamaño y lujo.

Precio vs Habitaciones

p4 <- plot_ly(base1,
              x = ~factor(habitaciones), y = ~preciom,
              type = "box",
              color = ~factor(habitaciones),
              colors = "Oranges") %>%
  layout(title = "Precio por Número de Habitaciones – Casas Zona Norte",
         xaxis = list(title = "Número de habitaciones"),
         yaxis = list(title = "Precio (millones COP)"))
p4

Mayor número de habitaciones se asocia con precios más elevados, aunque con mayor variabilidad en los rangos altos.

Matriz de correlación

vars_cor <- base1 %>%
  select(preciom, areaconst, estrato, banios, habitaciones, parqueaderos) %>%
  na.omit()

cor_matrix <- cor(vars_cor)

corrplot(cor_matrix,
         method = "color",
         type = "upper",
         tl.col = "black",
         addCoef.col = "black",
         number.cex = 0.8,
         title = "Correlación entre variables – base1",
         mar = c(0,0,1,0))

El precio presenta correlaciones positivas con todas las variables. El área construida y el estrato son las que muestran mayor correlación con el precio. Esto sugiere que son los predictores más relevantes en el modelo.


Paso 3: Estimación del modelo de regresión lineal múltiple

modelo1 <- lm(preciom ~ areaconst + estrato + habitaciones +
                parqueaderos + banios,
              data = base1)

summary(modelo1)
## 
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos + 
##     banios, data = base1)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -784.29  -77.56  -16.03   47.67  978.61 
## 
## Coefficients:
##                Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  -238.17090   44.40551  -5.364 1.34e-07 ***
## areaconst       0.67673    0.05281  12.814  < 2e-16 ***
## estrato        80.63495    9.82632   8.206 2.70e-15 ***
## habitaciones    7.64511    5.65873   1.351    0.177    
## parqueaderos   24.00598    5.86889   4.090 5.14e-05 ***
## banios         18.89938    7.48800   2.524    0.012 *  
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 155.1 on 429 degrees of freedom
##   (287 observations deleted due to missingness)
## Multiple R-squared:  0.6041, Adjusted R-squared:  0.5995 
## F-statistic: 130.9 on 5 and 429 DF,  p-value: < 2.2e-16
# Tabla de coeficientes
coef_df <- as.data.frame(summary(modelo1)$coefficients)
coef_df$Variable <- rownames(coef_df)
coef_df <- coef_df[, c("Variable", "Estimate", "Std. Error", "t value", "Pr(>|t|)")]

coef_df %>%
  kable(digits = 4,
        caption = "Coeficientes del Modelo 1 – Casas Zona Norte",
        col.names = c("Variable", "Estimado", "Error Estándar",
                      "Estadístico t", "Valor p")) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Coeficientes del Modelo 1 – Casas Zona Norte
Variable Estimado Error Estándar Estadístico t Valor p
(Intercept) (Intercept) -238.1709 44.4055 -5.3635 0.0000
areaconst areaconst 0.6767 0.0528 12.8140 0.0000
estrato estrato 80.6349 9.8263 8.2060 0.0000
habitaciones habitaciones 7.6451 5.6587 1.3510 0.1774
parqueaderos parqueaderos 24.0060 5.8689 4.0904 0.0001
banios banios 18.8994 7.4880 2.5240 0.0120

Interpretación de coeficientes:

  • Intercepto: Valor base del precio cuando todas las variables son cero (referencia matemática, no interpretable directamente en este contexto).
  • areaconst: Por cada metro cuadrado adicional de área construida, el precio aumenta en promedio β₁ millones de pesos, manteniendo las demás variables constantes. Lógico: más área = más precio.
  • estrato: Por cada unidad que sube el estrato, el precio aumenta en β₂ millones. Lógico: estratos altos tienen propiedades más costosas.
  • habitaciones: Por cada habitación adicional, el precio varía en β₃ millones. Resultado esperado positivo.
  • parqueaderos: Cada parqueadero adicional agrega β₄ millones al precio. Razonable en mercado urbano.
  • banios: Cada baño adicional incrementa el precio en β₅ millones. Refleja mayor tamaño y confort.

Solo se interpretan como estadísticamente significativos los coeficientes con p-valor < 0.05.

r2 <- summary(modelo1)$r.squared
r2_adj <- summary(modelo1)$adj.r.squared
cat("R² =", round(r2, 4), "\n")
## R² = 0.6041
cat("R² ajustado =", round(r2_adj, 4), "\n")
## R² ajustado = 0.5995

Interpretación del R²: El modelo explica aproximadamente el 60.4% de la variabilidad en el precio de las casas de la Zona Norte. Un R² cercano a 1 indica mejor ajuste. Para mejorarlo se podrían incluir variables adicionales como antigüedad, estado de la vivienda, distancia a zonas comerciales, entre otras.


Paso 4: Validación de supuestos

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

par(mfrow = c(1, 1))

Normalidad de residuales

residuales1 <- residuals(modelo1)

# Test de Lilliefors (Kolmogorov-Smirnov)
lillie_test <- lillie.test(residuales1)
cat("Test de Lilliefors:\n")
## Test de Lilliefors:
cat("  Estadístico D =", round(lillie_test$statistic, 4), "\n")
##   Estadístico D = 0.1325
cat("  p-valor =", round(lillie_test$p.value, 4), "\n")
##   p-valor = 0
# QQ-plot interactivo
qq_data <- data.frame(
  teoricos = qqnorm(residuales1, plot.it = FALSE)$x,
  muestra  = qqnorm(residuales1, plot.it = FALSE)$y
)

plot_ly(qq_data, x = ~teoricos, y = ~muestra,
        type = "scatter", mode = "markers",
        marker = list(color = "#2c7bb6", size = 5)) %>%
  add_lines(x = ~teoricos, y = ~teoricos,
            line = list(color = "red", dash = "dash")) %>%
  layout(title = "QQ-Plot de Residuales – Modelo 1",
         xaxis = list(title = "Cuantiles teóricos"),
         yaxis = list(title = "Cuantiles muestrales"))

Si el p-valor del test de Lilliefors es mayor a 0.05, no se rechaza la normalidad de los residuales. En caso contrario, se sugiere aplicar transformaciones (logarítmica, Box-Cox) a la variable respuesta.

Homocedasticidad (varianza constante)

bp_test <- bptest(modelo1)
cat("Test de Breusch-Pagan:\n")
## Test de Breusch-Pagan:
cat("  Estadístico BP =", round(bp_test$statistic, 4), "\n")
##   Estadístico BP = 80.2808
cat("  p-valor =", round(bp_test$p.value, 4), "\n")
##   p-valor = 0

Si p-valor > 0.05 no hay evidencia de heterocedasticidad. En caso de detectarla, se recomienda usar errores estándar robustos o transformar la variable dependiente.

Independencia de residuales

dw_test <- dwtest(modelo1)
cat("Test de Durbin-Watson:\n")
## Test de Durbin-Watson:
cat("  DW =", round(dw_test$statistic, 4), "\n")
##   DW = 1.7615
cat("  p-valor =", round(dw_test$p.value, 4), "\n")
##   p-valor = 0.0055

Valores de DW cercanos a 2 indican independencia de residuales. Valores muy alejados sugieren autocorrelación, más común en datos de series de tiempo que en datos de corte transversal como este.

Multicolinealidad

vif_values <- vif(modelo1)
vif_df <- data.frame(Variable = names(vif_values), VIF = round(vif_values, 3))

vif_df %>%
  kable(caption = "Factor de Inflación de Varianza (VIF) – Modelo 1") %>%
  kable_styling(bootstrap_options = "striped", full_width = FALSE)
Factor de Inflación de Varianza (VIF) – Modelo 1
Variable VIF
areaconst areaconst 1.461
estrato estrato 1.308
habitaciones habitaciones 1.721
parqueaderos parqueaderos 1.226
banios banios 1.967

VIF > 10 indica multicolinealidad problemática. Si se detecta, se recomienda eliminar variables redundantes, usar Ridge Regression o aplicar PCA.


Paso 5: Predicción para Vivienda 1

# Características de la vivienda solicitada
nueva_v1 <- data.frame(
  areaconst    = 200,
  estrato      = 4,      # estrato 4 (dentro del rango 4-5 solicitado)
  habitaciones = 4,
  parqueaderos = 1,
  banios       = 2
)

pred_v1 <- predict(modelo1, newdata = nueva_v1, interval = "prediction",
                   level = 0.95)

cat("=== Predicción para Vivienda 1 ===\n")
## === Predicción para Vivienda 1 ===
cat("Precio estimado:       $", round(pred_v1[1], 1), "millones\n")
## Precio estimado:       $ 312.1 millones
cat("Límite inferior (95%): $", round(pred_v1[2], 1), "millones\n")
## Límite inferior (95%): $ 6.2 millones
cat("Límite superior (95%): $", round(pred_v1[3], 1), "millones\n")
## Límite superior (95%): $ 618 millones
cat("\nCrédito pre-aprobado: $350 millones\n")
## 
## Crédito pre-aprobado: $350 millones
if (pred_v1[1] <= 350) {
  cat("✔ La vivienda estimada está DENTRO del presupuesto.\n")
} else {
  cat("✘ La vivienda estimada SUPERA el presupuesto.\n")
}
## ✔ La vivienda estimada está DENTRO del presupuesto.

El intervalo de predicción al 95% indica el rango de precios plausibles para una casa con estas características en la Zona Norte. La comparación con el crédito pre-aprobado de 350 millones orienta la búsqueda de opciones reales.


Paso 6: Ofertas potenciales para Vivienda 1 (≤ 350 millones)

# Filtrar ofertas que cumplan condiciones y estén dentro del presupuesto
ofertas_v1 <- base1 %>%
  filter(
    preciom <= 350,
    estrato %in% c(4, 5),
    habitaciones >= 3,
    banios >= 2,
    parqueaderos >= 1
  ) %>%
  arrange(abs(areaconst - 200)) %>%   # ordenar por cercanía al área solicitada
  head(10)

ofertas_v1 %>%
  select(barrio, preciom, areaconst, estrato,
         habitaciones, banios, parqueaderos, zona) %>%
  kable(caption = "Top 10 ofertas potenciales – Vivienda 1") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Top 10 ofertas potenciales – Vivienda 1
barrio preciom areaconst estrato habitaciones banios parqueaderos zona
el bosque 350 200 5 4 3 3 Zona Norte
la flora 320 200 5 4 4 2 Zona Norte
la merced 320 200 4 4 4 2 Zona Norte
el bosque 335 202 5 5 4 1 Zona Norte
el bosque 350 203 5 5 2 2 Zona Norte
vipasa 340 203 5 4 3 2 Zona Norte
vipasa 300 205 5 6 5 2 Zona Norte
la flora 350 190 5 3 3 1 Zona Norte
urbanización la merced 320 210 5 5 3 2 Zona Norte
la merced 350 216 5 4 2 2 Zona Norte
# Tomar las primeras 5 para el mapa
top5_v1 <- ofertas_v1 %>% head(5)

# Paleta de colores por precio
pal1 <- colorNumeric(palette = "Blues", domain = top5_v1$preciom)

leaflet(top5_v1) %>%
  addTiles() %>%
  addCircleMarkers(
    lng = ~longitud,
    lat = ~latitud,
    radius = 8,
    color = ~pal1(preciom),
    fillOpacity = 0.9,
    popup = ~paste0(
      "<b>Barrio:</b> ", barrio, "<br>",
      "<b>Precio:</b> $", preciom, " millones<br>",
      "<b>Área:</b> ", areaconst, " m²<br>",
      "<b>Estrato:</b> ", estrato, "<br>",
      "<b>Habitaciones:</b> ", habitaciones, "<br>",
      "<b>Baños:</b> ", banios, "<br>",
      "<b>Parqueaderos:</b> ", parqueaderos
    ),
    label = ~paste0("$", preciom, "M – ", barrio)
  ) %>%
  addLegend("bottomright",
            pal = pal1,
            values = ~preciom,
            title = "Precio (millones)",
            labFormat = labelFormat(prefix = "$")) %>%
  addLegend("topright",
            colors = "#2c7bb6",
            labels = "Ofertas potenciales V1",
            title = "Vivienda 1 (≤$350M)")

Análisis de ofertas: Se presentan 5 propiedades que cumplen con los requisitos de la solicitud (casa, Zona Norte, estrato 4-5, mínimo 3 habitaciones, 2 baños, 1 parqueadero) y se encuentran dentro del presupuesto pre-aprobado de 350 millones. Se recomienda a María priorizar las que más se acercan a las 4 habitaciones y 200 m² solicitados.

# VIVIENDA 2: Apartamento – Zona Sur

Paso 1: Filtrado de datos y mapa

# Filtrar: Apartamentos en Zona Sur
base2 <- vivienda %>%
  filter(tipo == "Apartamento", zona == "Zona Sur")

# Primeros 3 registros
base2 %>%
  head(3) %>%
  kable(caption = "Primeros 3 registros – Apartamentos Zona Sur") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE)
Primeros 3 registros – Apartamentos Zona Sur
id zona piso estrato preciom areaconst parqueaderos banios habitaciones tipo barrio longitud latitud
5098 Zona Sur 5 4 290 96 1 2 3 Apartamento acopi -76.53464 3.44987
698 Zona Sur 2 3 78 40 1 1 2 Apartamento aguablanca -76.50100 3.40000
8199 Zona Sur NA 6 875 194 2 5 3 Apartamento aguacatal -76.55700 3.45900
table(base2$tipo) %>%
  kable(col.names = c("Tipo", "Frecuencia"),
        caption = "Verificación: solo Apartamentos") %>%
  kable_styling(bootstrap_options = "striped", full_width = FALSE)
Verificación: solo Apartamentos
Tipo Frecuencia
Apartamento 2787
table(base2$zona) %>%
  kable(col.names = c("Zona", "Frecuencia"),
        caption = "Verificación: solo Zona Sur") %>%
  kable_styling(bootstrap_options = "striped", full_width = FALSE)
Verificación: solo Zona Sur
Zona Frecuencia
Zona Sur 2787
cat("Total de registros en base2:", nrow(base2), "\n")
## Total de registros en base2: 2787
leaflet(base2) %>%
  addTiles() %>%
  addCircleMarkers(
    lng = ~longitud,
    lat = ~latitud,
    radius = 5,
    color = "#d7191c",
    fillOpacity = 0.7,
    popup = ~paste0(
      "<b>Barrio:</b> ", barrio, "<br>",
      "<b>Precio:</b> $", preciom, " millones<br>",
      "<b>Área:</b> ", areaconst, " m²<br>",
      "<b>Estrato:</b> ", estrato, "<br>",
      "<b>Zona:</b> ", zona
    )
  ) %>%
  addLegend("bottomright",
            colors = "#d7191c",
            labels = "Apartamentos – Zona Sur",
            title = "Base 2")

Al igual que en la base anterior, pueden aparecer puntos geográficamente fuera de la Zona Sur, posiblemente por errores de clasificación o por viviendas en zonas limítrofes entre sectores de la ciudad.


Paso 2: Análisis exploratorio de datos

base2 %>%
  select(preciom, areaconst, estrato, banios, habitaciones, parqueaderos) %>%
  summary() %>%
  kable(caption = "Resumen estadístico – Variables del modelo (base2)") %>%
  kable_styling(bootstrap_options = "striped", full_width = FALSE)
Resumen estadístico – Variables del modelo (base2)
preciom areaconst estrato banios habitaciones parqueaderos
Min. : 75.0 Min. : 40.00 Min. :3.00 Min. :0.000 Min. :0.000 Min. : 1.000
1st Qu.: 175.0 1st Qu.: 65.00 1st Qu.:4.00 1st Qu.:2.000 1st Qu.:3.000 1st Qu.: 1.000
Median : 245.0 Median : 85.00 Median :5.00 Median :2.000 Median :3.000 Median : 1.000
Mean : 297.3 Mean : 97.47 Mean :4.63 Mean :2.488 Mean :2.966 Mean : 1.415
3rd Qu.: 335.0 3rd Qu.:110.00 3rd Qu.:5.00 3rd Qu.:3.000 3rd Qu.:3.000 3rd Qu.: 2.000
Max. :1750.0 Max. :932.00 Max. :6.00 Max. :8.000 Max. :6.000 Max. :10.000
NA NA NA NA NA NA’s :406

Precio vs Área construida

p1b <- plot_ly(base2,
               x = ~areaconst, y = ~preciom,
               type = "scatter", mode = "markers",
               marker = list(color = "#d7191c", opacity = 0.6),
               text = ~paste("Barrio:", barrio,
                             "<br>Precio:", preciom,
                             "<br>Área:", areaconst)) %>%
  layout(title = "Precio vs Área Construida – Apartamentos Zona Sur",
         xaxis = list(title = "Área construida (m²)"),
         yaxis = list(title = "Precio (millones COP)"))
p1b

Precio vs Estrato

p2b <- plot_ly(base2,
               x = ~factor(estrato), y = ~preciom,
               type = "box",
               color = ~factor(estrato),
               colors = "Reds") %>%
  layout(title = "Distribución de Precio por Estrato – Apartamentos Zona Sur",
         xaxis = list(title = "Estrato"),
         yaxis = list(title = "Precio (millones COP)"))
p2b

Precio vs Número de Baños

p3b <- plot_ly(base2,
               x = ~factor(banios), y = ~preciom,
               type = "box",
               color = ~factor(banios),
               colors = "Purples") %>%
  layout(title = "Precio por Número de Baños – Apartamentos Zona Sur",
         xaxis = list(title = "Número de baños"),
         yaxis = list(title = "Precio (millones COP)"))
p3b

Precio vs Habitaciones

p4b <- plot_ly(base2,
               x = ~factor(habitaciones), y = ~preciom,
               type = "box",
               color = ~factor(habitaciones),
               colors = "Oranges") %>%
  layout(title = "Precio por Número de Habitaciones – Apartamentos Zona Sur",
         xaxis = list(title = "Número de habitaciones"),
         yaxis = list(title = "Precio (millones COP)"))
p4b

Matriz de correlación

vars_cor2 <- base2 %>%
  select(preciom, areaconst, estrato, banios, habitaciones, parqueaderos) %>%
  na.omit()

cor_matrix2 <- cor(vars_cor2)

corrplot(cor_matrix2,
         method = "color",
         type = "upper",
         tl.col = "black",
         addCoef.col = "black",
         number.cex = 0.8,
         title = "Correlación entre variables – base2",
         mar = c(0,0,1,0))

En los apartamentos de la Zona Sur se esperan correlaciones similares a la base1. El área y el estrato suelen dominar como predictores del precio. Nótese que en estratos altos (5-6) la variabilidad de precios es mayor.


Paso 3: Estimación del modelo de regresión lineal múltiple

modelo2 <- lm(preciom ~ areaconst + estrato + habitaciones +
                parqueaderos + banios,
              data = base2)

summary(modelo2)
## 
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos + 
##     banios, data = base2)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -1092.02   -42.28    -1.33    40.58   926.56 
## 
## Coefficients:
##                Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  -261.62501   15.63220 -16.736  < 2e-16 ***
## areaconst       1.28505    0.05403  23.785  < 2e-16 ***
## estrato        60.89709    3.08408  19.746  < 2e-16 ***
## habitaciones  -24.83693    3.89229  -6.381 2.11e-10 ***
## parqueaderos   72.91468    3.95797  18.422  < 2e-16 ***
## banios         50.69675    3.39637  14.927  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 98.02 on 2375 degrees of freedom
##   (406 observations deleted due to missingness)
## Multiple R-squared:  0.7485, Adjusted R-squared:  0.748 
## F-statistic:  1414 on 5 and 2375 DF,  p-value: < 2.2e-16
coef_df2 <- as.data.frame(summary(modelo2)$coefficients)
coef_df2$Variable <- rownames(coef_df2)
coef_df2 <- coef_df2[, c("Variable", "Estimate", "Std. Error", "t value", "Pr(>|t|)")]

coef_df2 %>%
  kable(digits = 4,
        caption = "Coeficientes del Modelo 2 – Apartamentos Zona Sur",
        col.names = c("Variable", "Estimado", "Error Estándar",
                      "Estadístico t", "Valor p")) %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Coeficientes del Modelo 2 – Apartamentos Zona Sur
Variable Estimado Error Estándar Estadístico t Valor p
(Intercept) (Intercept) -261.6250 15.6322 -16.7363 0
areaconst areaconst 1.2850 0.0540 23.7853 0
estrato estrato 60.8971 3.0841 19.7457 0
habitaciones habitaciones -24.8369 3.8923 -6.3811 0
parqueaderos parqueaderos 72.9147 3.9580 18.4223 0
banios banios 50.6967 3.3964 14.9267 0
r2b <- summary(modelo2)$r.squared
r2b_adj <- summary(modelo2)$adj.r.squared
cat("R² =", round(r2b, 4), "\n")
## R² = 0.7485
cat("R² ajustado =", round(r2b_adj, 4), "\n")
## R² ajustado = 0.748

El modelo para apartamentos de la Zona Sur explica el 74.9% de la variabilidad en el precio. Se interpretan solo los coeficientes con p-valor < 0.05 como estadísticamente significativos.


Paso 4: Validación de supuestos

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

par(mfrow = c(1, 1))

Normalidad

residuales2 <- residuals(modelo2)
lillie_test2 <- lillie.test(residuales2)
cat("Test de Lilliefors:\n")
## Test de Lilliefors:
cat("  Estadístico D =", round(lillie_test2$statistic, 4), "\n")
##   Estadístico D = 0.1244
cat("  p-valor =", round(lillie_test2$p.value, 4), "\n")
##   p-valor = 0
qq_data2 <- data.frame(
  teoricos = qqnorm(residuales2, plot.it = FALSE)$x,
  muestra  = qqnorm(residuales2, plot.it = FALSE)$y
)

plot_ly(qq_data2, x = ~teoricos, y = ~muestra,
        type = "scatter", mode = "markers",
        marker = list(color = "#d7191c", size = 5)) %>%
  add_lines(x = ~teoricos, y = ~teoricos,
            line = list(color = "red", dash = "dash")) %>%
  layout(title = "QQ-Plot de Residuales – Modelo 2",
         xaxis = list(title = "Cuantiles teóricos"),
         yaxis = list(title = "Cuantiles muestrales"))

Homocedasticidad

bp_test2 <- bptest(modelo2)
cat("Test de Breusch-Pagan:\n")
## Test de Breusch-Pagan:
cat("  Estadístico BP =", round(bp_test2$statistic, 4), "\n")
##   Estadístico BP = 754.8051
cat("  p-valor =", round(bp_test2$p.value, 4), "\n")
##   p-valor = 0

Independencia

dw_test2 <- dwtest(modelo2)
cat("Test de Durbin-Watson:\n")
## Test de Durbin-Watson:
cat("  DW =", round(dw_test2$statistic, 4), "\n")
##   DW = 1.5333
cat("  p-valor =", round(dw_test2$p.value, 4), "\n")
##   p-valor = 0

Multicolinealidad

vif_values2 <- vif(modelo2)
vif_df2 <- data.frame(Variable = names(vif_values2), VIF = round(vif_values2, 3))

vif_df2 %>%
  kable(caption = "Factor de Inflación de Varianza (VIF) – Modelo 2") %>%
  kable_styling(bootstrap_options = "striped", full_width = FALSE)
Factor de Inflación de Varianza (VIF) – Modelo 2
Variable VIF
areaconst areaconst 2.067
estrato estrato 1.545
habitaciones habitaciones 1.429
parqueaderos parqueaderos 1.738
banios banios 2.529

Paso 5: Predicción para Vivienda 2

nueva_v2 <- data.frame(
  areaconst    = 300,
  estrato      = 5,      # estrato 5 (dentro del rango 5-6 solicitado)
  habitaciones = 5,
  parqueaderos = 3,
  banios       = 3
)

pred_v2 <- predict(modelo2, newdata = nueva_v2, interval = "prediction",
                   level = 0.95)

cat("=== Predicción para Vivienda 2 ===\n")
## === Predicción para Vivienda 2 ===
cat("Precio estimado:       $", round(pred_v2[1], 1), "millones\n")
## Precio estimado:       $ 675 millones
cat("Límite inferior (95%): $", round(pred_v2[2], 1), "millones\n")
## Límite inferior (95%): $ 481.5 millones
cat("Límite superior (95%): $", round(pred_v2[3], 1), "millones\n")
## Límite superior (95%): $ 868.6 millones
cat("\nCrédito pre-aprobado: $850 millones\n")
## 
## Crédito pre-aprobado: $850 millones
if (pred_v2[1] <= 850) {
  cat("✔ La vivienda estimada está DENTRO del presupuesto.\n")
} else {
  cat("✘ La vivienda estimada SUPERA el presupuesto.\n")
}
## ✔ La vivienda estimada está DENTRO del presupuesto.

Paso 6: Ofertas potenciales para Vivienda 2 (≤ 850 millones)

ofertas_v2 <- base2 %>%
  filter(
    preciom <= 850,
    estrato %in% c(5, 6),
    habitaciones >= 4,
    banios >= 3,
    parqueaderos >= 2
  ) %>%
  arrange(abs(areaconst - 300)) %>%
  head(10)

ofertas_v2 %>%
  select(barrio, preciom, areaconst, estrato,
         habitaciones, banios, parqueaderos, zona) %>%
  kable(caption = "Top 10 ofertas potenciales – Vivienda 2") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = FALSE)
Top 10 ofertas potenciales – Vivienda 2
barrio preciom areaconst estrato habitaciones banios parqueaderos zona
seminario 670 300.00 5 6 5 3 Zona Sur
cuarto de legua 410 295.55 5 4 4 2 Zona Sur
cuarto de legua 520 320.00 5 4 4 2 Zona Sur
ciudadela pasoancho 650 275.00 5 5 5 2 Zona Sur
capri 350 270.00 5 4 3 3 Zona Sur
san fernando 500 330.00 5 4 4 2 Zona Sur
san fernando viejo 485 259.00 5 4 4 2 Zona Sur
San Fernando 350 258.00 5 5 4 2 Zona Sur
seminario 530 256.00 5 5 5 3 Zona Sur
el ingenio 700 250.00 6 5 4 2 Zona Sur
top5_v2 <- ofertas_v2 %>% head(5)

pal2 <- colorNumeric(palette = "Reds", domain = top5_v2$preciom)

leaflet(top5_v2) %>%
  addTiles() %>%
  addCircleMarkers(
    lng = ~longitud,
    lat = ~latitud,
    radius = 8,
    color = ~pal2(preciom),
    fillOpacity = 0.9,
    popup = ~paste0(
      "<b>Barrio:</b> ", barrio, "<br>",
      "<b>Precio:</b> $", preciom, " millones<br>",
      "<b>Área:</b> ", areaconst, " m²<br>",
      "<b>Estrato:</b> ", estrato, "<br>",
      "<b>Habitaciones:</b> ", habitaciones, "<br>",
      "<b>Baños:</b> ", banios, "<br>",
      "<b>Parqueaderos:</b> ", parqueaderos
    ),
    label = ~paste0("$", preciom, "M – ", barrio)
  ) %>%
  addLegend("bottomright",
            pal = pal2,
            values = ~preciom,
            title = "Precio (millones)",
            labFormat = labelFormat(prefix = "$")) %>%
  addLegend("topright",
            colors = "#d7191c",
            labels = "Ofertas potenciales V2",
            title = "Vivienda 2 (≤$850M)")

Se identifican apartamentos en la Zona Sur que cumplen con los criterios de la segunda solicitud: estrato 5 o 6, mínimo 4 habitaciones, 3 baños y 2 parqueaderos, dentro del presupuesto de 850 millones. Se recomienda priorizar los más cercanos a los 300 m² y con mayor número de parqueaderos dado el tamaño de la familia.


Conclusiones

  • Los modelos de Regresión Lineal Múltiple permiten estimar el precio de vivienda en función de características observables.
  • Para Vivienda 1 (casa, Zona Norte), el modelo sugiere un precio estimado de aproximadamente 312 millones, con opciones reales dentro del presupuesto de 350 millones.
  • Para Vivienda 2 (apartamento, Zona Sur), el precio estimado es de 675 millones, y se identificaron opciones dentro del límite de 850 millones.
  • La validación de supuestos es clave para determinar la confiabilidad de las predicciones. En caso de violaciones, se recomienda transformaciones de variables o métodos de estimación alternativos.
  • Variables como el área construida y el estrato son los principales determinantes del precio en ambos segmentos.

Informe elaborado con R y RMarkdown | Modelos Estadísticos para la Toma de Decisiones