Carga y preparación de datos

library(paqueteMODELOS)
## Cargando paquete requerido: boot
## Cargando paquete requerido: broom
## Warning: package 'broom' was built under R version 4.5.3
## Cargando paquete requerido: GGally
## Warning: package 'GGally' was built under R version 4.5.3
## Cargando paquete requerido: ggplot2
## Warning: package 'ggplot2' was built under R version 4.5.3
## Cargando paquete requerido: gridExtra
## Warning: package 'gridExtra' was built under R version 4.5.3
## Cargando paquete requerido: knitr
## Cargando paquete requerido: summarytools
library(dplyr)
## Warning: package 'dplyr' was built under R version 4.5.3
## 
## Adjuntando el paquete: 'dplyr'
## The following object is masked from 'package:gridExtra':
## 
##     combine
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
library(tidyr)
library(ggplot2)
library(plotly)
## Warning: package 'plotly' was built under R version 4.5.3
## 
## Adjuntando el paquete: 'plotly'
## The following object is masked from 'package:ggplot2':
## 
##     last_plot
## The following object is masked from 'package:stats':
## 
##     filter
## The following object is masked from 'package:graphics':
## 
##     layout
library(leaflet)
## Warning: package 'leaflet' was built under R version 4.5.3
library(knitr)
library(factoextra)
## Warning: package 'factoextra' was built under R version 4.5.3
## Welcome to factoextra!
## Want to learn more? See two factoextra-related books at https://www.datanovia.com/library/principal-component-methods
data("vivienda")
str(vivienda)
## spc_tbl_ [8,322 × 13] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
##  $ id          : num [1:8322] 1147 1169 1350 5992 1212 ...
##  $ zona        : chr [1:8322] "Zona Oriente" "Zona Oriente" "Zona Oriente" "Zona Sur" ...
##  $ piso        : chr [1:8322] NA NA NA "02" ...
##  $ estrato     : num [1:8322] 3 3 3 4 5 5 4 5 5 5 ...
##  $ preciom     : num [1:8322] 250 320 350 400 260 240 220 310 320 780 ...
##  $ areaconst   : num [1:8322] 70 120 220 280 90 87 52 137 150 380 ...
##  $ parqueaderos: num [1:8322] 1 1 2 3 1 1 2 2 2 2 ...
##  $ banios      : num [1:8322] 3 2 2 5 2 3 2 3 4 3 ...
##  $ habitaciones: num [1:8322] 6 3 4 3 3 3 3 4 6 3 ...
##  $ tipo        : chr [1:8322] "Casa" "Casa" "Casa" "Casa" ...
##  $ barrio      : chr [1:8322] "20 de julio" "20 de julio" "20 de julio" "3 de julio" ...
##  $ longitud    : num [1:8322] -76.5 -76.5 -76.5 -76.5 -76.5 ...
##  $ latitud     : num [1:8322] 3.43 3.43 3.44 3.44 3.46 ...
##  - attr(*, "spec")=List of 3
##   ..$ cols   :List of 13
##   .. ..$ id          : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   .. ..$ zona        : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
##   .. ..$ piso        : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
##   .. ..$ estrato     : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   .. ..$ preciom     : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   .. ..$ areaconst   : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   .. ..$ parqueaderos: list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   .. ..$ banios      : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   .. ..$ habitaciones: list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   .. ..$ tipo        : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
##   .. ..$ barrio      : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
##   .. ..$ longitud    : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   .. ..$ latitud     : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   ..$ default: list()
##   .. ..- attr(*, "class")= chr [1:2] "collector_guess" "collector"
##   ..$ delim  : chr ";"
##   ..- attr(*, "class")= chr "col_spec"
##  - attr(*, "problems")=<externalptr>

Una vez cargada la información, se debe realizar una limpieza inicial, donde se eliminan los valores nulos, celdas vacías y demás elementos que puedan afectar el análisis del conjunto de datos.

# Ver datos faltantes por variable
cat("=== DATOS FALTANTES POR VARIABLE ===\n")
## === DATOS FALTANTES POR VARIABLE ===
colSums(is.na(vivienda))
##           id         zona         piso      estrato      preciom    areaconst 
##            3            3         2638            3            2            3 
## parqueaderos       banios habitaciones         tipo       barrio     longitud 
##         1605            3            3            3            3            3 
##      latitud 
##            3
# Porcentaje de datos faltantes
cat("\n=== PORCENTAJE DE DATOS FALTANTES ===\n")
## 
## === PORCENTAJE DE DATOS FALTANTES ===
round(colSums(is.na(vivienda))/nrow(vivienda)*100, 2)
##           id         zona         piso      estrato      preciom    areaconst 
##         0.04         0.04        31.70         0.04         0.02         0.04 
## parqueaderos       banios habitaciones         tipo       barrio     longitud 
##        19.29         0.04         0.04         0.04         0.04         0.04 
##      latitud 
##         0.04
# Número de registros antes de limpiar
cat("\nRegistros originales:", nrow(vivienda))
## 
## Registros originales: 8322
# Limpiar datos
vivienda_limpia <- vivienda %>%
  dplyr::select(estrato, preciom, areaconst, 
                parqueaderos, banios, habitaciones) %>%
  na.omit()

cat("\nRegistros después de limpiar:", nrow(vivienda_limpia))
## 
## Registros después de limpiar: 6717
# Resumen estadístico
summary(vivienda_limpia)
##     estrato        preciom         areaconst       parqueaderos   
##  Min.   :3.00   Min.   :  58.0   Min.   :  30.0   Min.   : 1.000  
##  1st Qu.:4.00   1st Qu.: 248.0   1st Qu.:  86.0   1st Qu.: 1.000  
##  Median :5.00   Median : 355.0   Median : 130.0   Median : 2.000  
##  Mean   :4.83   Mean   : 468.9   Mean   : 181.1   Mean   : 1.835  
##  3rd Qu.:6.00   3rd Qu.: 580.0   3rd Qu.: 233.0   3rd Qu.: 2.000  
##  Max.   :6.00   Max.   :1999.0   Max.   :1745.0   Max.   :10.000  
##      banios        habitaciones   
##  Min.   : 0.000   Min.   : 0.000  
##  1st Qu.: 2.000   1st Qu.: 3.000  
##  Median : 3.000   Median : 3.000  
##  Mean   : 3.255   Mean   : 3.611  
##  3rd Qu.: 4.000   3rd Qu.: 4.000  
##  Max.   :10.000   Max.   :10.000

Análisis estadístico descriptivo

El resumen estadístico de las 6 variables numéricas seleccionadas para el análisis muestra lo siguiente:

  • Estrato: las propiedades se concentran entre estratos 4 y 6 (mediana = 5, media = 4.83). El rango va de estrato 3 a 6.
  • Precio (preciom): promedio de 468.9 millones con mediana de 355 millones, evidenciando asimetría positiva. Rango entre 58 y 1,999 millones.
  • Área construida (areaconst): promedio de 181.1 m² con mediana de 130 m², también con asimetría positiva. Rango entre 30 y 1,745 m².
  • Parqueaderos: mayoría entre 1 y 2 (media = 1.835), con casos extremos de hasta 10.
  • Baños: promedio de 3.255, mediana de 3, rango de 0 a 10.
  • Habitaciones: promedio de 3.611, mediana de 3, rango de 0 a 10.

En este caso preciom y areaconst muestran asimetría positiva clara, consistente con el comportamiento típico de precios inmobiliarios donde unas pocas propiedades de lujo elevan el promedio. Esto sugiere evaluar más adelante una transformación logarítmica sobre estas variables si el modelo de regresión lo requiere.

Detección de valores atípicos

vivienda_limpia %>%
  pivot_longer(cols = everything(), names_to = "variable", values_to = "valor") %>%
  ggplot(aes(x = variable, y = valor)) +
  geom_boxplot(fill = "steelblue", alpha = 0.6) +
  facet_wrap(~variable, scales = "free") +
  theme_minimal() +
  labs(title = "Boxplots de variables numéricas", x = "", y = "Valor")

Se observan valores atípicos superiores en areaconst, preciom y parqueaderos, correspondientes a un segmento de propiedades grandes o de lujo dentro de la oferta general. Estrato no presenta outliers, lo cual es esperable por tratarse de una variable ordinal acotada.

ado que estos valores atípicos parecen corresponder a propiedades legítimas de alto valor (no errores de captura), no se recomienda eliminarlos del análisis general; sin embargo, se debe considerar su influencia sobre los supuestos del modelo de regresión (sección de validaDción, punto 4).

1.Depuración de la variable Zona

Existen registros cuya zona declarada no corresponde con su ubicación geográfica real. Para evidenciarlo, se compara la distribución de la latitud según la zona declarada:

ggplot(vivienda, aes(x = zona, y = latitud, fill = zona)) +
  geom_boxplot() +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
  labs(title = "Distribución de latitud según zona declarada",
       y = "Latitud", x = "Zona")
## Warning: Removed 3 rows containing non-finite outside the scale range
## (`stat_boxplot()`).

Se observa que Zona Norte presenta la mediana de latitud más alta 3.47, consistente con su ubicación geográfica esperada, mientras que Zona Sur presenta la mediana más baja 3.39. Sin embargo, Zona Centro, Zona Oeste y Zona Oriente se solapan considerablemente entre sí medianas entre 3.44 y 3.45, lo que dificulta diferenciarlas únicamente por latitud.

Adicionalmente, se observan varios puntos atípicos en la parte inferior de casi todas las zonas, incluyendo “Zona Norte”, que caen por debajo de 3.40 e incluso cerca de 3.35 es decir, registros etiquetados como zona norte pero con una latitud típica de zona sur. Esto confirma el problema de codificación mencionado. También aparece una categoría NA sin datos, que deberá revisarse.

leaflet(vivienda) %>%
  addProviderTiles(providers$CartoDB.Positron) %>%
  addCircleMarkers(~longitud, ~latitud,
                    color = ~colorFactor("Set1", zona)(zona),
                    radius = 3, opacity = 0.7,
                    popup = ~paste("Zona declarada:", zona))
## Warning in validateCoords(lng, lat, funcName): Data contains 3 rows with either
## missing or invalid lat/lon values and will be ignored

Se observa que los colores (zonas declaradas) están completamente mezclados en el espacio: en la parte superior (norte real de la ciudad) predominan puntos azules, pero también aparecen puntos naranja, verdes y morados en esa misma zona; en la parte inferior (sur real) predomina el naranja, pero igualmente se mezclan puntos azules, verdes y morados. No hay una separación geográfica limpia entre colores — cualquier zona declarada aparece dispersa por toda la extensión de la ciudad.

Gracias a esto descarta la posibilidad de depurar la zona norte usando únicamente la etiqueta zona o un simple corte de latitud, ya que el nivel de mezcla es alto en todas las categorías, no solo en los bordes. Es necesario usar un criterio geográfico objetivo el análisis de conglomerados (clustering) con latitud y longitud — que agrupe las viviendas según su ubicación real, independientemente de la etiqueta declarada, y así construir una definición confiable de “zona norte” para filtrar base1.

sum(is.na(vivienda$longitud))
## [1] 3
sum(is.na(vivienda$latitud))
## [1] 3

Se identificaron 3 registros con valores faltantes en longitud y latitud. Dado que representan una proporción mínima del total de la base, se excluyen únicamente para efectos del análisis de conglomerados geográfico.

# Excluir registros con NA en coordenadas y estandarizar
geo <- vivienda %>%
  filter(!is.na(longitud), !is.na(latitud))

geo_scaled <- geo %>%
  select(longitud, latitud) %>%
  scale()

1.1 Análisis de conglomerados geográfico

Para determinar el número óptimo de conglomerados se utiliza el método del codo, que nos ayuda a evaluar la variabilidad interna (WSS) para distintos valores de k

set.seed(123)
fviz_nbclust(geo_scaled, kmeans, method = "wss", k.max = 10) +
  labs(title = "Método del codo para número óptimo de clusters")

El método del codo muestra una reducción marcada de la varianza interna hasta k=4, punto a partir del cual agregar más conglomerados no mejora sustancialmente el ajuste. Por lo tanto, se seleccionan 4 conglomerados para el análisis, consistente con las principales zonas geográficas de la ciudad.

Ahora apliquemos el k-means con k=4:

set.seed(123)
kmeans_geo <- kmeans(geo_scaled, centers = 4, nstart = 25)

geo$cluster_geo <- factor(kmeans_geo$cluster)

fviz_cluster(kmeans_geo, data = geo_scaled,
             geom = "point",
             ellipse.type = "convex",
             palette = c("#00AFBB", "#E7B800", "#FC4E07", "#7CAE00"),
             ggtheme = theme_minimal()) +
  labs(title = "Clusters geográficos de viviendas")

Ahora podemos comparar estos clusters contra la zona declarada, para identificar cuál corresponde a la zona norte real:

table(geo$zona, geo$cluster_geo)
##               
##                   1    2    3    4
##   Zona Centro     2   98   16    8
##   Zona Norte    160 1028  107  625
##   Zona Oeste     17  125 1025   31
##   Zona Oriente   52   83   17  199
##   Zona Sur     3148  350 1014  214

Se observa que los registros declarados como “Zona Norte” se distribuyen principalmente entre los clusters 2 y 4 (1,028 y 625 registros respectivamente), mientras que “Zona Sur” se concentra fuertemente en el cluster 1 (3,148 registros). El cluster 3 agrupa mayoritariamente “Zona Oeste” y una cantidad importante de “Zona Sur”, lo que sugiere que corresponde a una zona occidental/suroccidental de la ciudad.

Llama la atención que la “Zona Norte” quedó dividida entre dos clusters distintos (2 y 4), lo que podría indicar que el norte real de la ciudad se subdivide geográficamente en dos grupos, o que uno de estos dos clusters corresponde en realidad a otra zona colindante mal codificada como norte.

Se debe confirmar entonces cuál cluster corresponde geográficamente al norte real, necesitamos ver la latitud promedio de cada cluster (a mayor latitud, más al norte):

geo %>%
  group_by(cluster_geo) %>%
  summarise(
    lat_prom = mean(latitud),
    lon_prom = mean(longitud),
    n = n()
  )
## # A tibble: 4 × 4
##   cluster_geo lat_prom lon_prom     n
##   <fct>          <dbl>    <dbl> <int>
## 1 1               3.38    -76.5  3379
## 2 2               3.46    -76.5  1684
## 3 3               3.43    -76.5  2179
## 4 4               3.46    -76.5  1077
  • Cluster 1 (lat. prom. = 3.375): latitud más baja, corresponde entonces a la zona sur real de la ciudad — coincide con que este cluster concentraba la mayoría de los registros de “Zona Sur” en la tabla de contingencia.

  • Cluster 3 (lat. prom. = 3.428, lon. prom. = -76.547, la más occidental): corresponde a la zona oeste, coincidiendo con el predominio de “Zona Oeste” observado antes en el gráfico.

  • Clusters 2 y 4 (lat. prom. = 3.462 y 3.460 respectivamente, las más altas): corresponden a la zona norte real de la ciudad. La diferencia entre ambos está en la longitud (cluster 2 más occidental, cluster 4 más oriental), lo que sugiere que el norte se subdivide en noroccidente y nororiente, pero ambos son geográficamente norte.

La zona norte real se define como la unión de los clusters 2 y 4, pues ambos comparten la latitud más alta del datos.

Ahora, con esto se puede construir base1 con este criterio definitivo (tipo casa + clusters 2 y 4):

base1 <- geo %>%
  filter(tipo == "Casa", cluster_geo %in% c(2, 4))

cat("Registros en base1:", nrow(base1), "\n")
## Registros en base1: 1199
head(base1, 3) %>% kable()
id zona piso estrato preciom areaconst parqueaderos banios habitaciones tipo barrio longitud latitud cluster_geo
1147 Zona Oriente NA 3 250 70 1 3 6 Casa 20 de julio -76.51168 3.43382 2
1169 Zona Oriente NA 3 320 120 1 2 3 Casa 20 de julio -76.51237 3.43369 2
1350 Zona Oriente NA 3 350 220 2 2 4 Casa 20 de julio -76.51537 3.43566 2
table(base1$zona)
## 
##  Zona Centro   Zona Norte   Zona Oeste Zona Oriente     Zona Sur 
##           83          613           27          238          238

conteo total y la tabla completa de zonas:

cat("Registros en base1:", nrow(base1), "\n")
## Registros en base1: 1199
table(base1$zona)
## 
##  Zona Centro   Zona Norte   Zona Oeste Zona Oriente     Zona Sur 
##           83          613           27          238          238

base1 quedó con 1,199 registros de tipo casa ubicados geográficamente en el norte (clusters 2 y 4). Sin embargo, al revisar la etiqueta zona original dentro de este filtro, se observa una mezcla considerable:

  • Solo 613 registros (51.1%) están correctamente etiquetados como “Zona Norte”.
  • El resto corresponde a registros mal codificados: 238 como “Zona Oriente”, 238 como “Zona Sur”, 83 como “Zona Centro” y 27 como “Zona Oeste” — todos ellos geográficamente ubicados en el norte real de la ciudad según sus coordenadas.

Esto confirma que la variable zona original no es confiable para segmentar el mercado y que el criterio geográfico (clustering por latitud/longitud) es el método correcto para definir base1, tal como se hizo. De aquí en adelante, el análisis de la vivienda 1 se basa en este conjunto depurado de 1,199 casas del norte real de la ciudad.

Para verificar visualmente la calidad del filtro geográfico aplicado, se grafican los puntos de base1 en un mapa:

leaflet(base1) %>%
  addProviderTiles(providers$CartoDB.Positron) %>%
  addCircleMarkers(~longitud, ~latitud,
                    color = ~colorFactor("Set1", zona)(zona),
                    radius = 4, opacity = 0.8,
                    popup = ~paste("Zona declarada:", zona, "<br>Precio:", preciom, "M"))

Se puede observar que, a pesar de la mezcla de etiquetas en zona, los puntos de base1 se concentran en la parte superior (norte) de la ciudad, con muy pocos puntos dispersos hacia el sur. Esto valida que el criterio de depuración por clustering geográfico (latitud/longitud) funcionó para aislar la oferta de casas ubicadas realmente en el norte de Cali.

2.análisis exploratorio con foco en la correlación entre preciom y las variables areaconst, estrato, banios, habitaciones y zona, usando base1.

Se analiza la relación entre el precio de la vivienda y sus principales características, utilizando la base depurada de casas en zona norte (base1).

library(corrplot)
## Warning: package 'corrplot' was built under R version 4.5.3
## corrplot 0.95 loaded
vars_cor <- base1 %>%
  select(preciom, areaconst, estrato, banios, habitaciones) %>%
  na.omit()

matriz_cor <- cor(vars_cor)
round(matriz_cor, 2)
##              preciom areaconst estrato banios habitaciones
## preciom         1.00      0.64    0.68   0.56         0.16
## areaconst       0.64      1.00    0.40   0.48         0.31
## estrato         0.68      0.40    1.00   0.43        -0.05
## banios          0.56      0.48    0.43   1.00         0.56
## habitaciones    0.16      0.31   -0.05   0.56         1.00
corrplot(matriz_cor, method = "color", type = "upper",
         addCoef.col = "black", tl.col = "black", tl.srt = 45,
         title = "Matriz de correlación - base1", mar = c(0,0,1,0))

El precio muestra correlaciones positivas moderadas a fuertes con la mayoría de las variables:

  • Estrato (0.68) y área construida (0.64) son las variables con mayor asociación con el precio, lo cual es lógico: viviendas más grandes y en estratos más altos tienden a costar más.

  • Baños (0.56) también muestra una correlación considerable, coherente con que más baños suele acompañar a viviendas de mayor tamaño y valor.

  • Habitaciones (0.16) presenta una correlación baja con el precio, lo que resulta menos intuitivo, pero puede explicarse porque el número de habitaciones varía poco dentro de este segmento (casas de estrato 4-6 en el norte), mientras que el área y el estrato capturan mejor las diferencias de valor.

Entre las variables predictoras, la correlación estrato y baños (0.43) y areaconst y baños (0.48) sugiere que hay algún tipo de multicolinealidad a tener en cuenta al construir el modelo de regresión. La correlación entre estrato y habitaciones es prácticamente nula (-0.05), indicando que son variables independientes de la vivienda.

areaconst y estrato aparecen como los predictores más relevantes del precio en este segmento, mientras que habitaciones parece no resultar significativa en el modelo de regresión múltiple.

Ahora, hacemos el mismo análisis pero con gráficos interactivos de plotly. El más importante es el precio vs. área construida, que está coloreado por estratos en el gráfico:

p1 <- ggplot(base1, aes(x = areaconst, y = preciom, color = factor(estrato),
                        text = paste("Barrio:", barrio, "<br>Precio:", preciom, "M<br>Área:", areaconst, "m²"))) +
  geom_point(alpha = 0.6) +
  theme_minimal() +
  labs(title = "Precio vs. Área construida (por estrato)",
       x = "Área construida (m²)", y = "Precio (millones)", color = "Estrato")

ggplotly(p1, tooltip = "text")

precio según estrato (boxplot) y según zona (dentro de base1, para ver si aún hay diferencias por la mezcla de etiquetas):

p2 <- ggplot(base1, aes(x = factor(estrato), y = preciom, fill = factor(estrato),
                        text = paste("Estrato:", estrato, "<br>Precio:", preciom, "M"))) +
  geom_boxplot(alpha = 0.7) +
  theme_minimal() +
  labs(title = "Precio según estrato", x = "Estrato", y = "Precio (millones)") +
  theme(legend.position = "none")

ggplotly(p2, tooltip = "text")
p3 <- ggplot(base1, aes(x = zona, y = preciom, fill = zona,
                        text = paste("Zona:", zona, "<br>Precio:", preciom, "M"))) +
  geom_boxplot(alpha = 0.7) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
  labs(title = "Precio según zona declarada (dentro de base1)", x = "Zona", y = "Precio (millones)")

ggplotly(p3, tooltip = "text")

-Precio según estrato: se observa una relación clara y creciente entre el estrato y el precio: la mediana pasa de aproximadamente 250M en estrato 3, a 400M en estrato 4, 500M en estrato 5, y salta a cerca de 900M en estrato 6. Con esto confirmamos que la correlación (0.68) observada anteriormente y coherente con el mercado inmobiliario, porque a mayor estrato socioeconómico, mayor valor de la propiedad. También se observan varios outliers superiores en los estratos 4 y 5, viviendas que se salen del rango típico de su estrato.

-Precio según zona declarada: dentro de base1 ya filtrada geográficamente al norte real, aún se observan diferencias notorias de precio entre las zonas declaradas: “Zona Oeste” muestra la mediana más alta 750M y mayor dispersión, mientras que “Zona Oriente” muestra la mediana más baja 150-200M. “Zona Norte”, “Zona Centro” y “Zona Sur”.

3. Modelo de regresión lineal múltiple (Vivienda 1)

Se estima un modelo de regresión lineal múltiple para predecir el precio de la vivienda en función de sus características físicas: área construida, estrato, número de habitaciones, número de parqueaderos y número de baños.

modelo completo con todas las variables:

modelo_completo <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios,
                       data = base1)
summary(modelo_completo)
## 
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos + 
##     banios, data = base1)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -1013.46  -100.61   -14.08    63.48   926.50 
## 
## Coefficients:
##                Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  -331.18398   36.58800  -9.052  < 2e-16 ***
## areaconst       0.62293    0.04466  13.949  < 2e-16 ***
## estrato       106.72620    8.22157  12.981  < 2e-16 ***
## habitaciones  -11.77561    4.71054  -2.500   0.0126 *  
## parqueaderos   44.75137    5.66357   7.902 9.54e-15 ***
## banios         42.16572    6.74871   6.248 6.89e-10 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 187.7 on 768 degrees of freedom
##   (425 observations deleted due to missingness)
## Multiple R-squared:  0.6526, Adjusted R-squared:  0.6504 
## F-statistic: 288.6 on 5 and 768 DF,  p-value: < 2.2e-16

Significancia: todas las variables resultaron estadísticamente significativas (p < 0.05), incluyendo habitaciones (p = 0.0126). No se requiere eliminar ninguna del modelo por criterio de significancia. Se conserva entonces el modelo completo como el modelo final para la Vivienda 1.

Los coeficientes de areaconst, estrato, parqueaderos y baños son positivos y lógicos, porque ante viviendas más grandes, en estratos más altos, con más parqueaderos y más baños tienden a costar más, lo cual es coherente con el comportamiento esperado del mercado.

Coeficiente de determinación (R²): el modelo explica el 65.26% de la variabilidad del precio, lo cual es un ajuste razonable para un modelo de solo 5 predictores en un mercado, donde factores no observados como la ubicación exacta dentro del barrio, acabados, antigüedad, vista, cercanía a vías principales y demás, también influyen en el precio y no están capturados aquí.

4. Validación de supuestos del modelo

Se realiza el diagnóstico de los supuestos clásicos de la regresión lineal: linealidad, normalidad de los residuos, homocedasticidad y ausencia de multicolinealidad.

plot(modelo_completo, which = 1)  # Residuals vs Fitted

-Residuals vs Fitted (linealidad y homocedasticidad):

El gráfico muestra que los residuos no se distribuyen de forma aleatoria y constante alrededor de cero, en realidad se observa un patrón en forma de embudo, donde la dispersión de los residuos es menor para valores ajustados bajos y aumenta considerablemente para valores ajustados altos (viviendas de mayor precio predicho). Esto es un indicio de heterocedasticidad.

Adicionalmente, la línea roja (suavizado) muestra una ligera curvatura en lugar de ser perfectamente horizontal, lo que sugiere que podría existir una relación no lineal entre los predictores y el precio, especialmente en el rango de valores altos.

plot(modelo_completo, which = 2)

- Q-Q Plot (normalidad de los residuos):

Los residuos estandarizados se ajustan razonablemente bien a la línea teórica de normalidad en la parte central de la distribución. Sin embargo, se observan desviaciones en ambas colas. En la cola izquierda residuos muy negativos y en la cola derecha residuos muy positivos, los puntos se alejan considerablemente de la línea diagonal. Esto indica que los residuos no siguen una distribución completamente normal, curtosis alta es decir, hay más valores extremos.

plot(modelo_completo, which = 3)

- Scale-Location (homocedasticidad):

Este gráfico confirma lo observado en el Residuals vs Fitted, la línea roja muestra una tendencia claramente creciente, en lugar de ser horizontal. Esto significa que la magnitud de los residuos (raíz cuadrada de los residuos estandarizados) aumenta a medida que aumenta el valor ajustado del precio, confirmando que hay heterocedasticidad. Lo que indica que el modelo predice con menor precisión (mayor dispersión de errores) las viviendas de precio alto que las de precio bajo, lo cual es un patrón común en modelos de precios inmobiliarios cuando no se transforma la variable respuesta.

Con esto se termina el diagnóstico visual. Ahora es necesario respaldar todo con pruebas formales como Breusch-Pagan (homocedasticidad), Shapiro-Wilk (normalidad) y VIF (multicolinealidad):

library(lmtest)
## Warning: package 'lmtest' was built under R version 4.5.3
## Cargando paquete requerido: zoo
## Warning: package 'zoo' was built under R version 4.5.3
## 
## Adjuntando el paquete: 'zoo'
## The following objects are masked from 'package:base':
## 
##     as.Date, as.Date.numeric
library(car)
## Warning: package 'car' was built under R version 4.5.3
## Cargando paquete requerido: carData
## Warning: package 'carData' was built under R version 4.5.3
## 
## Adjuntando el paquete: 'car'
## The following object is masked from 'package:dplyr':
## 
##     recode
## The following object is masked from 'package:boot':
## 
##     logit
# Homocedasticidad
bptest(modelo_completo)
## 
##  studentized Breusch-Pagan test
## 
## data:  modelo_completo
## BP = 153.46, df = 5, p-value < 2.2e-16
# Normalidad de residuos
shapiro.test(residuals(modelo_completo))
## 
##  Shapiro-Wilk normality test
## 
## data:  residuals(modelo_completo)
## W = 0.87621, p-value < 2.2e-16
# Multicolinealidad
vif(modelo_completo)
##    areaconst      estrato habitaciones parqueaderos       banios 
##     1.420439     1.666202     1.670454     1.292048     2.157528

- Pruebas formales de los supuestos:

  • Homocedasticidad (Breusch-Pagan): BP = 153.46, p-value < 2.2e-16. Como el p-value es menor a 0.05, se rechaza la hipótesis nula de homocedasticidad, confirmando formalmente lo observado en los gráficos de Residuals vs Fitted y Scale-Location, existe heterocedasticidad en los residuos del modelo.

  • Normalidad (Shapiro-Wilk): W = 0.876, p-value < 2.2e-16. Con un p-value menor a 0.05, se rechaza la hipótesis nula de normalidad, confirmando lo observado en el Q-Q Plot, los residuos no siguen una distribución normal.

  • Multicolinealidad (VIF): todos los valores están entre 1.29 y 2.16, muy por debajo del umbral de preocupación (VIF > 5), por lo que no hay evidencia de multicolinealidad entre los predictores.

En este caso el modelo cumple el supuesto de ausencia de multicolinealidad, pero no cumple los supuestos de homocedasticidad ni de normalidad de los residuos. Esto significa que, si bien los coeficientes estimados siguen siendo insesgados, los errores estándar y las pruebas de significancia (valores p) podrían no ser totalment confiables, y los intervalos de confianza de las predicciones podrían estar subestimados o sobreestimados, especialmente para viviendas de precio alto.

5. predicción del precio de la Vivienda 1, usando sus características: casa, 200 m² de área construida, estrato 4 o 5, 1 parqueadero, 2 baños, 4 habitaciones.

vivienda1 <- data.frame(
  areaconst = 200,
  estrato = 4,
  habitaciones = 4,
  parqueaderos = 1,
  banios = 2
)

pred1 <- predict(modelo_completo, newdata = vivienda1, interval = "prediction", level = 0.95)
pred1
##       fit       lwr      upr
## 1 302.288 -66.88957 671.4655
vivienda1_e5 <- vivienda1
vivienda1_e5$estrato <- 5

pred1_e5 <- predict(modelo_completo, newdata = vivienda1_e5, interval = "prediction", level = 0.95)
pred1_e5
##        fit      lwr      upr
## 1 409.0142 39.20178 778.8265

5.1 Predicción del precio - Vivienda 1

Para una casa de 200 m² de área construida, 1 parqueadero, 2 baños y 4 habitaciones, el modelo predice:

  • Estrato 4: precio estimado de 302.3 millones de pesos, con un intervalo de predicción del 95% entre -66.9 y 671.5 millones.
  • Estrato 5: precio estimado de 409.0 millones de pesos, con un intervalo de predicción del 95% entre 39.2 y 778.8 millones.

El precio estimado aumenta al pasar de estrato 4 a estrato 5. El precio puntual para estrato 4 (302.3M) se ajusta dentro del crédito preaprobado de 350 millones, mientras que para estrato 5 (409.0M) lo supera. Dentro del presupuesto de 350 millones, es más viable encontrar una vivienda con las características solicitadas en estrato 4 que en estrato 5.

6.

ofertas_v1 <- base1 %>%
  filter(preciom <= 350,
         estrato %in% c(4, 5)) %>%
  arrange(desc(preciom))

cat("Número de ofertas potenciales encontradas:", nrow(ofertas_v1), "\n")
## Número de ofertas potenciales encontradas: 148
head(ofertas_v1, 10) %>% 
  select(barrio, zona, estrato, preciom, areaconst, banios, habitaciones, parqueaderos, latitud, longitud) %>%
  kable()
barrio zona estrato preciom areaconst banios habitaciones parqueaderos latitud longitud
alfonso lópez Zona Oriente 4 350 126 3 3 NA 3.45853 -76.48383
chipichape Zona Norte 4 350 98 3 4 2 3.48154 -76.52840
colseguros Zona Sur 4 350 214 3 5 1 3.43086 -76.53057
colseguros Zona Sur 4 350 247 2 4 1 3.44987 -76.53464
el bosque Zona Norte 5 350 200 3 4 3 3.48503 -76.53010
el bosque Zona Norte 5 350 300 5 6 3 3.48577 -76.53010
el bosque Zona Norte 5 350 240 3 6 2 3.48635 -76.53136
el bosque Zona Norte 5 350 203 2 5 2 3.48531 -76.51448
la flora Zona Norte 5 350 264 3 4 2 3.46412 -76.50330
la flora Zona Norte 5 350 110 4 3 1 3.48157 -76.52353

6.1. Ofertas potenciales para la Vivienda 1

Se identificaron 148 ofertas dentro de base1 que cumplen simultáneamente con el presupuesto de 350 millones y el estrato solicitado (4 o 5). Esto representa una disponibilidad amplia (aproximadamente el 12% del total de base1), lo que indica que la empresa tiene un número considerable de opciones reales dentro de su presupuesto. Las diez ofertas más cercanas al tope del presupuesto se presentan en la tabla anterior.

Todas las ofertas mostradas alcanzan el límite máximo de 350 millones, lo que indica que el presupuesto asignado permite acceder al segmento superior de precios disponible dentro de estos criterios. Se destaca especialmente la oferta en El Bosque (estrato 5, 200 m², 3 baños, 4 habitaciones, 3 parqueaderos, $350M), que coincide casi exactamente con el área construida solicitada (200 m²) y el número de habitaciones (4), superando ligeramente lo pedido en baños y parqueaderos — una característica favorable más que una limitación.

Se observa también que la mayoría de las ofertas dentro del presupuesto se concentran en los barrios El Bosque, La Flora y Chipichape, mientras que Colseguros aparece asociado a “Zona Sur” en la etiqueta declarada, otro caso de la mezcla de codificación ya documentada (aunque geográficamente pertenece al norte real, según el filtro de base1).

Cabe mencionar que un registro (Alfonso López) presenta un valor faltante (NA) en parqueaderos, por lo que no se puede confirmar si cumple con el requisito mínimo de parqueadero solicitado.

También, se encuentran 6 registros con el nombre de Valle del Lili, Ciudad 2000 y Colseguros barrios pertenecientes al sur de la ciudad, pero que en este conjunto de datos están localizados en el oriente, pero segun la longitud y latitud hacen parte de la zona norte. Por lo tanto se retiran antes de realizar el mapa:

# Excluir barrios adicionales mal ubicados dentro de ofertas_v1
ofertas_v1 <- ofertas_v1 %>%
  filter(!barrio %in% c("el cedro", "valle del lili", "santa isabel", "colseguros andes", "ciudad 2000"))

cat("Registros en ofertas_v1 después de excluir barrios atípicos:", nrow(ofertas_v1), "\n")
## Registros en ofertas_v1 después de excluir barrios atípicos: 126

Ahora construyamos el mapa con estas ofertas potenciales, como pide el enunciado (al menos 5 ofertas en un mapa):

leaflet(ofertas_v1) %>%
  addProviderTiles(providers$CartoDB.Positron) %>%
  addCircleMarkers(~longitud, ~latitud,
                    radius = 6, color = "darkgreen", fillOpacity = 0.8,
                    popup = ~paste0("<b>", barrio, "</b><br>",
                                     "Precio: ", preciom, "M<br>",
                                     "Área: ", areaconst, " m²<br>",
                                     "Estrato: ", estrato, "<br>",
                                     "Baños: ", banios, " | Hab: ", habitaciones, " | Parq: ", parqueaderos))

Las 126 ofertas potenciales se concentran principalmente en la parte superior del mapa (extremo norte), con una densidad especialmente alta alrededor de Montebello, y una segunda concentración notable más al sur del norte (zona intermedia, cerca del centro de la ciudad). Esto confirma que la oferta de casas de estrato 4-5 dentro del presupuesto de 350 millones está distribuida en un corredor relativamente amplio dentro del norte real de la ciudad, dándole a la empresa múltiples opciones de ubicación dentro de esta zona, y no solo un sector puntual.

Con base en el modelo de regresión y la búsqueda directa sobre base1, se recomienda a la empresa considerar principalmente las ofertas de estrato 4 (más holgadas dentro del presupuesto según la predicción del modelo) y priorizar los barrios con mayor concentración de oferta identificados en el mapa, donde existe mayor probabilidad de encontrar una vivienda que además de ajustarse al precio, cumpla con las características físicas solicitadas (200 m², 1 parqueadero, 2 baños, 4 habitaciones).

Punto 7: Repetir el proceso para la Vivienda 2

Ahora replicamos exactamente el mismo flujo para la segunda solicitud: apartamento, zona Sur, estrato 5 o 6, 300 m², 3 parqueaderos, 3 baños, 5 habitaciones, crédito preaprobado de 850 millones.

Empecemos por identificar el cluster geográfico que corresponde a la zona sur real (ya lo vimos en la tabla de latitudes promedio):

base2 <- geo %>%
  filter(tipo == "Apartamento", cluster_geo == 1)

cat("Registros en base2:", nrow(base2), "\n")
## Registros en base2: 2041
head(base2, 3) %>% kable()
id zona piso estrato preciom areaconst parqueaderos banios habitaciones tipo barrio longitud latitud cluster_geo
1724 Zona Norte 01 5 240 87 1 3 3 Apartamento acopi -76.51700 3.36971 1
4386 Zona Norte 01 5 310 137 2 3 4 Apartamento acopi -76.53105 3.38296 1
6857 Zona Norte 03 3 100 49 NA 1 2 Apartamento acopi -76.54531 3.37775 1
table(base2$zona)
## 
##   Zona Norte   Zona Oeste Zona Oriente     Zona Sur 
##           89           15           10         1927

7.1 Construcción de base2 (apartamentos, zona sur)

Se aplicó el mismo criterio de depuración geográfica utilizado para base1, filtrando ahora apartamentos ubicados en el cluster correspondiente a la zona sur real (cluster 1, latitud promedio = 3.375).

base2 quedó constituida y se observa una consistencia mucho mayor que en base1, están correctamente etiquetados como “Zona Sur”, solo 114 registros (5.6%) corresponden a otras zonas mal codificadas (89 “Zona Norte”, 15 “Zona Oeste”, 10 “Zona Oriente”).

Ahora el mapa de verificación de base2

leaflet(base2) %>%
  addProviderTiles(providers$CartoDB.Positron) %>%
  addCircleMarkers(~longitud, ~latitud,
                    color = ~colorFactor("Set1", zona)(zona),
                    radius = 3, opacity = 0.7,
                    popup = ~paste("Zona declarada:", zona, "<br>Precio:", preciom, "M"))

El mapa confirma que la gran mayoría de los puntos de base2 (color morado, “Zona Sur”) se concentran de manera clara y consistente en la parte inferior (sur) de la ciudad, extendiéndose desde el centro hasta el límite sur de la mancha urbana. Solo un número reducido de puntos de otros colores (mal etiquetados) aparece disperso hacia la parte superior del mapa, cerca de El Poblado Campestre y Navarro.

Sigamos con el análisis exploratorio

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

matriz_cor2 <- cor(vars_cor2)
round(matriz_cor2, 2)
##              preciom areaconst estrato banios habitaciones
## preciom         1.00      0.80    0.68   0.75         0.34
## areaconst       0.80      1.00    0.53   0.68         0.41
## estrato         0.68      0.53    1.00   0.62         0.25
## banios          0.75      0.68    0.62   1.00         0.49
## habitaciones    0.34      0.41    0.25   0.49         1.00
corrplot(matriz_cor2, method = "color", type = "upper",
         addCoef.col = "black", tl.col = "black", tl.srt = 45,
         title = "Matriz de correlación - base2", mar = c(0,0,1,0))

  • Área construida (0.80) es la variable con mayor asociación con el precio, superando incluso al estrato.
  • Baños (0.75) también muestra una correlación muy fuerte, más alta que en base1 (0.56).
  • Estrato (0.68) se mantiene como un predictor relevante, en un nivel similar al de base1.
  • Habitaciones (0.34) mejora considerablemente respecto a base1 (0.16), aunque sigue siendo la variable con menor asociación.

Entre los predictores, se observa una correlación alta entre areaconst y banios (0.68) y entre estrato y banios (0.62), lo que sugiere revisar la multicolinealidad en el modelo de regresión con mayor atención que en el caso de base1.

Ahora el gráfico interactivo con plotly (precio vs. área, coloreado por estrato)

p1_v2 <- ggplot(base2, aes(x = areaconst, y = preciom, color = factor(estrato),
                        text = paste("Barrio:", barrio, "<br>Precio:", preciom, "M<br>Área:", areaconst, "m²"))) +
  geom_point(alpha = 0.6) +
  theme_minimal() +
  labs(title = "Precio vs. Área construida (por estrato) - base2",
       x = "Área construida (m²)", y = "Precio (millones)", color = "Estrato")

ggplotly(p1_v2, tooltip = "text")

El gráfico confirma la correlación fuerte (0.80) observada en la matriz: se aprecia una relación positiva y bastante definida entre área construida y precio, más clara y consistente que la observada en base1. El estrato 6 se ubica en la parte superior del gráfico, mayores precios para una misma área, mientras que los puntos rosados estrato 3, se concentran en la parte inferior, con precios bajos incluso para áreas moderadas.

Existen un par de valores atípicos en el eje horizontal — apartamentos de estrato 5 con áreas por encima de 500 m² e incluso cercanos a 900 m² — que muestran precios relativamente bajos (300-650M) para su tamaño, lo cual podría distorsionar el ajuste del modelo de regresión y merece atención en la validación de supuestos.

boxplots de precio según estrato y según zona

p2_v2 <- ggplot(base2, aes(x = factor(estrato), y = preciom, fill = factor(estrato),
                        text = paste("Estrato:", estrato, "<br>Precio:", preciom, "M"))) +
  geom_boxplot(alpha = 0.7) +
  theme_minimal() +
  labs(title = "Precio según estrato - base2", x = "Estrato", y = "Precio (millones)") +
  theme(legend.position = "none")

ggplotly(p2_v2, tooltip = "text")

Se confirma una relación clara y creciente entre estrato y precio en el segmento de apartamentos del sur, en este caso, la mediana pasa de aproximadamente 140M en estrato 3, a 200M en estrato 4, 280M en estrato 5, y sube considerablemente a cerca de 570M en estrato 6. El salto entre estrato 5 y 6 es particularmente marcado, más pronunciado que el observado en base1 para casas del norte.

Ahora bien, el estrato 6 presenta además la mayor dispersión y numerosos valores atípicos superiores varios apartamentos por encima de 1,500 millones, reflejando la heterogeneidad del segmento de lujo dentro de esta categoría. Los estratos 3 y 4 muestran distribuciones mucho más compactas y homogéneas.

Igual que en base1, el estrato es un diferenciador claro y consistente del precio, reforzando su relevancia como predictor en el modelo de regresión. El comportamiento es coherente con la lógica de mercado.

boxplot de precio segpun la zona

p3_v2 <- ggplot(base2, aes(x = zona, y = preciom, fill = zona,
                        text = paste("Zona:", zona, "<br>Precio:", preciom, "M"))) +
  geom_boxplot(alpha = 0.7) +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
  labs(title = "Precio según zona declarada (dentro de base2)", x = "Zona", y = "Precio (millones)")

ggplotly(p3_v2, tooltip = "text")

Dentro de base2, la mayoría de los registros corresponden a “Zona Sur” (mediana ~250M, con abundantes outliers superiores hasta 1,750M reflejando la heterogeneidad del segmento de lujo), consistente con el alto grado de codificación correcta observado antes (94.4%). “Zona Oeste”, aunque con muy pocos registros dentro de este filtro, muestra la mediana más alta (~530M) y mayor dispersión. “Zona Norte” y “Zona Oriente” (las etiquetas minoritarias mal codificadas dentro de base2) presentan medianas más bajas y menor dispersión.

Igual que en base1, persisten diferencias de precio según la etiqueta zona declarada, aun cuando geográficamente todos estos registros pertenecen al sur real de la ciudad. Por tal motivo, se mantendrá el modelo con los criterios del modelo construido para la Vivienda 1, zona tampoco se incluirá como predictor en el modelo de regresión de la Vivienda 2.

Se estima un modelo de regresión lineal múltiple para predecir el precio de los apartamentos en base2:

modelo_completo2 <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios,
                       data = base2)
summary(modelo_completo2)
## 
## Call:
## lm(formula = preciom ~ areaconst + estrato + habitaciones + parqueaderos + 
##     banios, data = base2)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -1311.93   -41.66    -2.27    38.69   864.57 
## 
## Coefficients:
##                Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  -252.38732   19.23625 -13.120  < 2e-16 ***
## areaconst       1.54318    0.06648  23.211  < 2e-16 ***
## estrato        52.82076    3.79118  13.933  < 2e-16 ***
## habitaciones  -25.57012    4.91112  -5.207 2.16e-07 ***
## parqueaderos   79.90876    5.00677  15.960  < 2e-16 ***
## banios         52.58781    4.10943  12.797  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 100.2 on 1696 degrees of freedom
##   (339 observations deleted due to missingness)
## Multiple R-squared:  0.7835, Adjusted R-squared:  0.7829 
## F-statistic:  1228 on 5 and 1696 DF,  p-value: < 2.2e-16

Significancia: todas las variables resultaron altamente significativas (p < 0.001 en todos los casos), incluso con mayor fuerza estadística que en el modelo de base1. Al igual que para la base 1 las cinco variables resultaron estadísticamente significativas (p < 0.001), se conserva el modelo completo como modelo final para la Vivienda 2, sin necesidad de eliminar predictores.

Al igual que en la Vivienda 1, los coeficientes de areaconst, estrato, parqueaderos y banios son positivos y coherentes con la lógica del mercado. Es interesante notar que el efecto marginal del área construida es más del doble en base2 (1.54 vs. 0.62 millones por m²) que en base1, lo cual es consistente con que los apartamentos de zona sur tienden a estar en estratos más altos y en un mercado de mayor valor por metro cuadrado. De forma similar, parqueaderos tiene un efecto más fuerte (79.9 vs. 44.8 millones), razonable en edificios de apartamentos donde el parqueadero es un atributo más escaso y valorado que en casas.

Coeficiente de determinación (R²): el modelo explica el 78.35% de la variabilidad del precio (R² ajustado = 78.29%), un ajuste notablemente mejor que el obtenido en base1 (65.26%). Esto confirma que el precio de los apartamentos del sur está mejor explicado por sus características físicas que el de las casas del norte, posiblemente porque el mercado de apartamentos es más homogéneo, son edificios con especificaciones más estandarizadas que el de casas,

validación de supuestos para este modelo

plot(modelo_completo2, which = 1)

Al igual que en el modelo de base1, se observa un patrón de embudo la dispersión de los residuos es pequeña para valores ajustados bajos y aumenta considerablemente para valores ajustados altos. La línea roja (suavizado) se mantiene relativamente plana en la parte central, pero se desvía ligeramente al alza en el extremo derecho.

Q-Q Plot

plot(modelo_completo2, which = 2)

Los residuos se ajustan razonablemente bien a la línea teórica en la parte central de la distribución, pero se observan desviaciones marcadas en ambas colas, más pronunciadas que en el modelo de base1.

Los residuos no siguen una distribución normal, con colas considerablemente más pesadas que en el modelo de la Vivienda 1. La observación 1708 en particular se comporta como un outlier, que debería revisarse manualmente porque podría tratarse de un error de digitación en el precio o en alguna de las variables predictoras, dado lo extremo de su desviación.

Scale-Location

plot(modelo_completo2, which = 3)

La línea roja (suavizado) muestra una tendencia creciente, similar a la de base1, confirmando heterocedasticidad. La dispersión es notablemente mayor para valores ajustados por encima de 500-700 millones, mientras que se mantiene compacta y estable en el rango de precios bajos.

La observación 1708 vuelve a destacarse como un punto claramente atípico, con la mayor magnitud de residuo estandarizado de todo el conjunto de datos.

pruebas formales (Breusch-Pagan, Shapiro-Wilk, VIF)

bptest(modelo_completo2)
## 
##  studentized Breusch-Pagan test
## 
## data:  modelo_completo2
## BP = 714.13, df = 5, p-value < 2.2e-16
shapiro.test(residuals(modelo_completo2))
## 
##  Shapiro-Wilk normality test
## 
## data:  residuals(modelo_completo2)
## W = 0.75012, p-value < 2.2e-16
vif(modelo_completo2)
##    areaconst      estrato habitaciones parqueaderos       banios 
##     2.230000     1.687558     1.386554     1.983032     2.645342
  • Homocedasticidad (Breusch-Pagan): BP = 714.13, p-value < 2.2e-16. Se rechaza la hipótesis nula de homocedasticidad, confirmando la heterocedasticidad observada gráficamente. El estadístico BP es considerablemente mayor que el de base1 (153.46), lo que indica que este problema es más severo en el modelo de la Vivienda 2.

  • Normalidad (Shapiro-Wilk): W = 0.750, p-value < 2.2e-16. Se rechaza la hipótesis nula de normalidad. El valor de W es notablemente más bajo que el de base1 (0.876), reflejando una desviación de la normalidad más pronunciada, consistente con el outlier extremo (observación 1708) identificado en los gráficos de diagnóstico.

  • Multicolinealidad (VIF): todos los valores están entre 1.39 y 2.65, por debajo del umbral de preocupación (VIF > 5). banios (2.65) y areaconst (2.23) presentan los valores más altos, mayores que en base1, pero aún dentro de un rango aceptable. No hay evidencia de multicolinealidad problemática.

Predicción del precio, usando las características solicitadas: apartamento, 300 m², 3 parqueaderos, 3 baños, 5 habitaciones, estrato 5 o 6.

vivienda2_e5 <- data.frame(
  areaconst = 300,
  estrato = 5,
  habitaciones = 5,
  parqueaderos = 3,
  banios = 3
)

pred2_e5 <- predict(modelo_completo2, newdata = vivienda2_e5, interval = "prediction", level = 0.95)
pred2_e5
##        fit     lwr     upr
## 1 744.3105 545.682 942.939
vivienda2_e6 <- vivienda2_e5
vivienda2_e6$estrato <- 6

pred2_e6 <- predict(modelo_completo2, newdata = vivienda2_e6, interval = "prediction", level = 0.95)
pred2_e6
##        fit      lwr      upr
## 1 797.1313 598.4904 995.7722

Para la misma vivienda pero en estrato 5, el modelo predice un precio de 744.3 millones de pesos, con un intervalo de predicción del 95% entre 545.7 y 942.9 millones.

Para un apartamento de 300 m², 3 parqueaderos, 3 baños y 5 habitaciones, en estrato 6, el modelo predice un precio de 797.1 millones de pesos, con un intervalo de predicción del 95% entre 598.5 y 995.8 millones.

Interpretación: como es de esperarse (coeficiente positivo de estrato, +52.82 millones por unidad), el precio estimado para estrato 5 (744.3M) es menor que para estrato 6 (797.1M), una diferencia de aproximadamente 52.8 millones, consistente con el coeficiente del modelo.

Ambos precios puntuales estimados (744.3M para estrato 5 y 797.1M para estrato 6) se ajustan dentro del crédito preaprobado de 850 millones, dejando un margen de 105.7M y 52.9M respectivamente. Esto indica que, a diferencia de la Vivienda 1 (donde el estrato 5 era la opción más viable dentro de presupuesto), en la Vivienda 2 ambos estratos solicitados (5 o 6) son alcanzables con el crédito disponible, aunque el estrato 6 deja un margen más ajustado.

Punto 6

ofertas_v2 <- base2 %>%
  filter(preciom <= 850,
         estrato %in% c(5, 6)) %>%
  arrange(desc(preciom))

cat("Número de ofertas potenciales encontradas:", nrow(ofertas_v2), "\n")
## Número de ofertas potenciales encontradas: 1117
head(ofertas_v2, 10) %>% 
  select(barrio, zona, estrato, preciom, areaconst, banios, habitaciones, parqueaderos, latitud, longitud) %>%
  kable()
barrio zona estrato preciom areaconst banios habitaciones parqueaderos latitud longitud
ciudad jardín Zona Norte 6 850 191.0 4 3 2 3.37100 -76.54100
ciudad jardín Zona Sur 6 850 187.0 5 4 3 3.36287 -76.53887
ciudad jardín Zona Sur 6 850 168.0 5 4 3 3.35114 -76.54369
ciudad jardín Zona Sur 6 850 192.0 5 3 2 3.36540 -76.52846
ciudad jardín Zona Sur 6 850 191.8 5 4 2 3.35961 -76.53798
ciudad jardín Zona Sur 6 850 186.0 5 4 3 3.35621 -76.54309
el peñon Zona Oeste 6 850 325.0 3 2 2 3.37500 -76.54200
pance Zona Sur 6 850 187.0 4 3 3 3.34782 -76.53530
pance Zona Sur 6 850 352.0 3 3 4 3.34265 -76.53729
pance Zona Sur 6 850 187.0 4 3 3 3.35064 -76.54484

Se identificaron 1,117 ofertas dentro de base2 que cumplen simultáneamente con el presupuesto de 850 millones y el estrato solicitado (5 o 6). Esta cifra representa una disponibilidad muy amplia (más del 54% de base2), sustancialmente mayor a la encontrada para la Vivienda 1 (148 ofertas, ~12% de base1), lo que indica que el presupuesto de 850 millones ofrece un margen de negociación considerablemente más holgado dentro del mercado del sur.

Interpretación: todas las ofertas mostradas alcanzan el límite máximo de 850 millones, concentrándose principalmente en el barrio Ciudad Jardín (7 de las 10 mejores ofertas) y también en Pance y El Peñón. Se destaca la oferta en Ciudad Jardín (estrato 6, 191 m², 4 baños, 3 habitaciones, 2 parqueaderos, $850M), que se ajusta razonablemente al área solicitada (300 m² pedidos vs. 191 m² ofrecidos) aunque con menor tamaño que lo requerido; y la oferta en Pance (352 m², 3 baños, 3 habitaciones, 4 parqueaderos), que sí supera el área mínima solicitada y cumple con creces el requisito de parqueaderos.

Se observa nuevamente el problema de codificación de zona: la primera oferta aparece etiquetada como “Zona Norte” pese a estar geográficamente en el sur real (confirmado por su latitud de 3.371, la más baja de la tabla), reforzando la importancia de haber usado el criterio de clustering geográfico en lugar de la etiqueta original.

leaflet(ofertas_v2) %>%
  addProviderTiles(providers$CartoDB.Positron) %>%
  addCircleMarkers(~longitud, ~latitud,
                    radius = 5, color = "darkblue", fillOpacity = 0.6,
                    popup = ~paste0("<b>", barrio, "</b><br>",
                                     "Precio: ", preciom, "M<br>",
                                     "Área: ", areaconst, " m²<br>",
                                     "Estrato: ", estrato, "<br>",
                                     "Baños: ", banios, " | Hab: ", habitaciones, " | Parq: ", parqueaderos))

El mapa muestra las 1,117 ofertas potenciales identificadas para la Vivienda 2. A diferencia del mapa de la Vivienda 1, aquí se observa una cobertura mucho más amplia y densa, extendiéndose por prácticamente toda la extensión del sur real de la ciudad, desde el sector cercano a Buitrera (parte inferior izquierda) hasta el límite con Ciudad Jardín y El Poblado Campestre (parte superior derecha).

Se identifican dos núcleos de mayor concentración: uno en la parte superior-centro del mapa y otro más disperso hacia el sector inferior. No se observan puntos claramente fuera de la zona sur real, a diferencia de lo ocurrido con base1 no fue necesario aplicar exclusiones manuales adicionales sobre ofertas_v2.

Conclusiones

Tanto para el tipo de vivienda 1 como la vivienda 2, los modelos violaron los supuestos de homocedasticidad y normalidad de los residuos (confirmado con las pruebas de Breusch-Pagan y Shapiro-Wilk), siendo este problema más marcado en el modelo de la Vivienda 2. Esto implica que los intervalos de predicción deben interpretarse con cautela, especialmente en el segmento de precios altos. Sin embargo, entendiendo y conociendo las zonas de la ciudad, se puede confirmar que los modelos están filtrando la informamción correctamente.

Ahora bien, para la vivienda 1 (Casa, Zona Norte, crédito 350 millones) se considera:

  • El modelo de regresión explicó el 65.3% de la variabilidad del precio, con estrato y areaconst como los predictores más influyentes.

  • La predicción puntual para las características solicitadas (200 m², 1 parqueadero, 2 baños, 4 habitaciones) resultó en 302 millones (estrato 4) y 409 millones (estrato 5). Solo el estrato 4 se ajusta cómodamente al presupuesto de 350 millones.

  • La búsqueda directa sobre base1 confirmó esta conclusión: se encontraron 148 ofertas reales dentro del presupuesto, concentradas principalmente en el sector de Montebello y alrededores.

Para la base uno se debería enfocar la búsqueda hacia viviendas de estrato 4, donde el presupuesto ofrece mayor margen; el estrato 5 es alcanzable únicamente en casos puntuales con áreas más ajustadas.

En la Vivienda 2 (Apartamento, Zona Sur, crédito 850 millones) se obtiene:

  • El modelo de regresión tuvo un mejor ajuste que el de la Vivienda 1, explicando el 78.4% de la variabilidad del precio, lo que sugiere un mercado de apartamentos más homogéneo y predecible que el de casas.

  • La predicción puntual para las características solicitadas (300 m², 3 parqueaderos, 3 baños, 5 habitaciones) resultó en 744 millones (estrato 5) y 797 millones (estrato 6), ambos dentro del presupuesto de 850 millones.

  • La búsqueda directa sobre base2 mostró una disponibilidad mucho más amplia: 1,117 ofertas dentro de presupuesto, concentradas en Ciudad Jardín y Pance, aunque ninguna de las mejores opciones alcanza simultáneamente las 5 habitaciones y los 300 m² exactos solicitados.

Para la base 2 presupuesto es suficiente para ambos estratos permitidos (5 o 6); se sugiere priorizar Ciudad Jardín y Pance, y estar dispuesta a flexibilizar el número de habitaciones si se prioriza el área y los parqueaderos solicitados.

Para finalizar, con base en el análisis, ambas solicitudes son realistas dentro de los presupuestos, para la Vivienda 1 tiene mayor probabilidad de éxito enfocándose en estrato 4, mientras que la Vivienda 2 cuenta con amplia disponibilidad en ambos estratos solicitados. Se sugiere presentar a la empresa las ofertas identificadas en los mapas de las secciones 6 y 10 como punto incial de la negociación.