Se inicia la actividad cargando el dataset vivienda desde el paquete paqueteMODELOS, el cual contiene información de viviendas en la ciudad de Cali, incluyendo ubicación, características físicas y precio de venta en millones de pesos.

También se cargan los paquetes necesarios para realizar el análisis exploratorio, modelado estadístico, visualizaciones interactivas y validación de supuestos.

Como siguiente paso se valida la estructura de la base de datos para identificar los tipos de variables disponibles.

library(dplyr)  
library(plotly)  
library(leaflet)
library(kableExtra)
library(tidyr)
library(naniar)
library(lmtest)

Cargar Datos

# Cargar datos
devtools::install_github("centromagis/paqueteMODELOS", force =TRUE)

── R CMD build ─────────────────────────────────────────────────────────────────
      ✔  checking for file 'C:\Users\User\AppData\Local\Temp\RtmpSgvWZB\remotes618410987368\Centromagis-paqueteMODELOS-3b06257/DESCRIPTION'
      ─  preparing 'paqueteMODELOS':
   checking DESCRIPTION meta-information ...  ✔  checking DESCRIPTION meta-information
      ─  checking for LF line-endings in source and make files and shell scripts (404ms)
  ─  checking for empty or unneeded directories
      ─  building 'paqueteMODELOS_0.1.0.tar.gz'
     
library(paqueteMODELOS)
data("vivienda")
str(vivienda)  # Estructura del data.frame base de R
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> 
summary(vivienda)  # Resumen estadístico de todas las columnas
       id           zona               piso              estrato     
 Min.   :   1   Length:8322        Length:8322        Min.   :3.000  
 1st Qu.:2080   Class :character   Class :character   1st Qu.:4.000  
 Median :4160   Mode  :character   Mode  :character   Median :5.000  
 Mean   :4160                                         Mean   :4.634  
 3rd Qu.:6240                                         3rd Qu.:5.000  
 Max.   :8319                                         Max.   :6.000  
 NA's   :3                                            NA's   :3      
    preciom         areaconst       parqueaderos        banios      
 Min.   :  58.0   Min.   :  30.0   Min.   : 1.000   Min.   : 0.000  
 1st Qu.: 220.0   1st Qu.:  80.0   1st Qu.: 1.000   1st Qu.: 2.000  
 Median : 330.0   Median : 123.0   Median : 2.000   Median : 3.000  
 Mean   : 433.9   Mean   : 174.9   Mean   : 1.835   Mean   : 3.111  
 3rd Qu.: 540.0   3rd Qu.: 229.0   3rd Qu.: 2.000   3rd Qu.: 4.000  
 Max.   :1999.0   Max.   :1745.0   Max.   :10.000   Max.   :10.000  
 NA's   :2        NA's   :3        NA's   :1605     NA's   :3       
  habitaciones        tipo              barrio             longitud     
 Min.   : 0.000   Length:8322        Length:8322        Min.   :-76.59  
 1st Qu.: 3.000   Class :character   Class :character   1st Qu.:-76.54  
 Median : 3.000   Mode  :character   Mode  :character   Median :-76.53  
 Mean   : 3.605                                         Mean   :-76.53  
 3rd Qu.: 4.000                                         3rd Qu.:-76.52  
 Max.   :10.000                                         Max.   :-76.46  
 NA's   :3                                              NA's   :3       
    latitud     
 Min.   :3.333  
 1st Qu.:3.381  
 Median :3.416  
 Mean   :3.418  
 3rd Qu.:3.452  
 Max.   :3.498  
 NA's   :3      
glimpse(vivienda)
Rows: 8,322
Columns: 13
$ id           <dbl> 1147, 1169, 1350, 5992, 1212, 1724, 2326, 4386, 1209, 159…
$ zona         <chr> "Zona Oriente", "Zona Oriente", "Zona Oriente", "Zona Sur…
$ piso         <chr> NA, NA, NA, "02", "01", "01", "01", "01", "02", "02", "02…
$ estrato      <dbl> 3, 3, 3, 4, 5, 5, 4, 5, 5, 5, 6, 4, 5, 6, 4, 5, 5, 4, 5, …
$ preciom      <dbl> 250, 320, 350, 400, 260, 240, 220, 310, 320, 780, 750, 62…
$ areaconst    <dbl> 70, 120, 220, 280, 90, 87, 52, 137, 150, 380, 445, 355, 2…
$ parqueaderos <dbl> 1, 1, 2, 3, 1, 1, 2, 2, 2, 2, NA, 3, 2, 2, 1, 4, 2, 2, 2,…
$ banios       <dbl> 3, 2, 2, 5, 2, 3, 2, 3, 4, 3, 7, 5, 6, 2, 4, 4, 4, 3, 2, …
$ habitaciones <dbl> 6, 3, 4, 3, 3, 3, 3, 4, 6, 3, 6, 5, 6, 2, 5, 5, 4, 3, 3, …
$ tipo         <chr> "Casa", "Casa", "Casa", "Casa", "Apartamento", "Apartamen…
$ barrio       <chr> "20 de julio", "20 de julio", "20 de julio", "3 de julio"…
$ longitud     <dbl> -76.51168, -76.51237, -76.51537, -76.54000, -76.51350, -7…
$ latitud      <dbl> 3.43382, 3.43369, 3.43566, 3.43500, 3.45891, 3.36971, 3.4…

Vivienda 1

Se filtran de la base original solo las viviendas de tipo “Casa” ubicadas en la “Zona Norte” de Cali, obteniendo así un df de trabajo para el análisis de la primera solicitud de compra.

# Creamos subconjunto para Vivienda 1
base1 <- vivienda %>%
  filter(tipo == "Casa", zona == "Zona Norte")

No. de viviendas en la base Vivienda 1 Zona Norte

# Verificamos cuántas viviendas hay en esta base
nrow(base1)
[1] 722

3 Primeros registros relevantees

# Mostramos los primeros 3 registros relevantes
base1 %>%
  select(id, barrio, estrato, preciom, areaconst, parqueaderos, banios, habitaciones) %>%
  head(3) %>%
  kable("html", caption = "Primeros 3 registros: Casas en Zona Norte") %>%
  kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover"))
Primeros 3 registros: Casas en Zona Norte
id barrio estrato preciom areaconst parqueaderos banios habitaciones
1209 acopi 5 320 150 2 4 6
1592 acopi 5 780 380 2 3 3
4057 acopi 6 750 445 NA 7 6

Las primeras observaciones muestran variabilidad en precios, áreas, y estratos,esto se debe tener en cuenta para el análisis.

Las tablas resumen permiten identificarque las casas en esta zona están distribuidas principalmente entre estratos 4, 5 y 6.

# Tabla: Conteo por estrato
base1 %>%
  count(estrato) %>%
  kable("html", col.names = c("Estrato", "Cantidad"), caption = "Cantidad de casas por estrato") %>%
  kable_styling(full_width = FALSE)
Cantidad de casas por estrato
Estrato Cantidad
3 235
4 161
5 271
6 55

Conteo por barrio

# Tabla: Conteo por barrio
base1 %>%
  count(barrio) %>%
  arrange(desc(n)) %>%
  kable("html", col.names = c("Barrio", "Cantidad"), caption = "Cantidad de casas por barrio en Zona Norte") %>%
  kable_styling(full_width = FALSE)
Cantidad de casas por barrio en Zona Norte
Barrio Cantidad
la flora 99
acopi 70
villa del prado 40
el bosque 37
prados del norte 31
san vicente 31
vipasa 30
la merced 24
urbanización la flora 23
brisas de los 22
salomia 20
zona norte 19
santa monica 16
versalles 16
santa mónica residencial 15
Cali 13
los andes 13
villa del sol 12
ciudad los álamos 11
juanamb√∫ 11
granada 10
los guaduales 10
la rivera 9
villas de veracruz 7
calima 6
chipichape 5
popular 5
santa monica residencial 5
flora industrial 4
la campiña 4
urbanización la merced 4
villa de veracruz 4
alamos 3
barranquilla 3
calimio norte 3
centenario 3
floralia 3
las delicias 3
base aérea 2
la floresta 2
la rivera ii 2
las ceibas 2
los guayacanes 2
menga 2
pacara 2
paseo de los 2
paso del comercio 2
porvenir 2
san luis 2
torres de comfandi 2
urbanización barranquilla 2
La Flora 1
Santa Monica 1
Villa Del Prado 1
Villas De Veracruz 1
alameda del río 1
atanasio girardot 1
barrio tranquilo y 1
berlin 1
brisas del guabito 1
calibella 1
cambulos 1
chapinero 1
colinas del bosque 1
cristales 1
el cedro 1
el gran limonar 1
el guabito 1
el sena 1
el trébol 1
evaristo garcía 1
gaitan 1
jorge eliecer gaitán 1
la base 1
la esmeralda 1
la rivera i 1
la riviera 1
la villa del 1
las acacias 1
las américas 1
las granjas 1
manzanares 1
metropolitano del norte 1
nueva tequendama 1
oasis de comfandi 1
occidente 1
parque residencial el 1
poblado campestre 1
portada de comfandi 1
portales de comfandi 1
quintas de salomia 1
rozo la torre 1
san luís 1
santa bárbara 1
santa monica norte 1
santa mónica 1
santander 1
tejares de san 1
unión de vivienda 1
urbanización la nueva 1
valle del lili 1
villa colombia 1
zona oriente 1

MAPA LEAFLET

leaflet(base1) %>%
  addTiles() %>%
  addCircleMarkers(
    lng = ~longitud,
    lat = ~latitud,
    popup = ~paste("<b>Barrio:</b>", barrio,
                   "<br><b>Precio:</b>", preciom, "millones",
                   "<br><b>Área:</b>", areaconst, "m²",
                   "<br><b>Estrato:</b>", estrato),
    color = "blue",
    radius = 6,
    stroke = FALSE,
    fillOpacity = 0.8
  ) %>%
  setView(lng = -76.52, lat = 3.45, zoom = 12)

En el mapa interactivo se muestran las ubicaciones geográficas de las viviendas disponibles. La mayoría están bien localizadas en el norte de la ciudad.

EDA Base1 vivienda Zona Norte

En esta fase se exploró la estructura de los datos y se identificaron valores faltantes o inconsistencias potenciales. Se observan algunos registros con precios muy bajos o áreas construidas extremadamente pequeñas, así como valores N` en variables clave como baños y parqueaderos.

Se decide eliminar únicamente aquellos registros que representen errores evidentes de codificación o información faltante en variables fundamentales para el análisis.

Esta limpieza garantiza que el modelo posterior no se vea afectado por datos irreales o poco confiables.

# Estructura general
glimpse(base1)
Rows: 722
Columns: 13
$ id           <dbl> 1209, 1592, 4057, 4460, 6081, 7824, 7987, 3495, 141, 243,…
$ zona         <chr> "Zona Norte", "Zona Norte", "Zona Norte", "Zona Norte", "…
$ piso         <chr> "02", "02", "02", "02", "02", "02", "02", "03", NA, NA, N…
$ estrato      <dbl> 5, 5, 6, 4, 5, 4, 5, 5, 3, 3, 3, 3, 5, 3, 4, 4, 5, 5, 4, …
$ preciom      <dbl> 320, 780, 750, 625, 750, 600, 420, 490, 230, 190, 180, 50…
$ areaconst    <dbl> 150, 380, 445, 355, 237, 160, 200, 118, 160, 435, 120, 21…
$ parqueaderos <dbl> 2, 2, NA, 3, 2, 1, 4, 2, NA, NA, NA, NA, NA, NA, NA, NA, …
$ banios       <dbl> 4, 3, 7, 5, 6, 4, 4, 4, 2, 0, 3, 6, 5, 5, 3, 3, 4, 5, 5, …
$ habitaciones <dbl> 6, 3, 6, 5, 6, 5, 5, 4, 3, 0, 3, 6, 4, 8, 4, 3, 4, 6, 4, …
$ tipo         <chr> "Casa", "Casa", "Casa", "Casa", "Casa", "Casa", "Casa", "…
$ barrio       <chr> "acopi", "acopi", "acopi", "acopi", "acopi", "acopi", "ac…
$ longitud     <dbl> -76.51341, -76.51674, -76.52950, -76.53179, -76.54044, -7…
$ latitud      <dbl> 3.47968, 3.48721, 3.38527, 3.40590, 3.36862, 3.42125, 3.4…
summary(base1)
       id             zona               piso              estrato     
 Min.   :  58.0   Length:722         Length:722         Min.   :3.000  
 1st Qu.: 766.2   Class :character   Class :character   1st Qu.:3.000  
 Median :2257.0   Mode  :character   Mode  :character   Median :4.000  
 Mean   :2574.6                                         Mean   :4.202  
 3rd Qu.:4225.0                                         3rd Qu.:5.000  
 Max.   :8319.0                                         Max.   :6.000  
                                                                       
    preciom         areaconst       parqueaderos        banios      
 Min.   :  89.0   Min.   :  30.0   Min.   : 1.000   Min.   : 0.000  
 1st Qu.: 261.2   1st Qu.: 140.0   1st Qu.: 1.000   1st Qu.: 2.000  
 Median : 390.0   Median : 240.0   Median : 2.000   Median : 3.000  
 Mean   : 445.9   Mean   : 264.9   Mean   : 2.182   Mean   : 3.555  
 3rd Qu.: 550.0   3rd Qu.: 336.8   3rd Qu.: 3.000   3rd Qu.: 4.000  
 Max.   :1940.0   Max.   :1440.0   Max.   :10.000   Max.   :10.000  
                                   NA's   :287                      
  habitaciones        tipo              barrio             longitud     
 Min.   : 0.000   Length:722         Length:722         Min.   :-76.59  
 1st Qu.: 3.000   Class :character   Class :character   1st Qu.:-76.53  
 Median : 4.000   Mode  :character   Mode  :character   Median :-76.52  
 Mean   : 4.507                                         Mean   :-76.52  
 3rd Qu.: 5.000                                         3rd Qu.:-76.50  
 Max.   :10.000                                         Max.   :-76.47  
                                                                        
    latitud     
 Min.   :3.333  
 1st Qu.:3.452  
 Median :3.468  
 Mean   :3.460  
 3rd Qu.:3.482  
 Max.   :3.496  
                
# Conteo de NAs por variable
sapply(base1, function(x) sum(is.na(x)))
          id         zona         piso      estrato      preciom    areaconst 
           0            0          372            0            0            0 
parqueaderos       banios habitaciones         tipo       barrio     longitud 
         287            0            0            0            0            0 
     latitud 
           0 
# Revisión de valores únicos en variables clave
table(base1$estrato)

  3   4   5   6 
235 161 271  55 
table(base1$banios)

  0   1   2   3   4   5   6   7   8   9  10 
 10  17 165 187 171 101  46  11  11   1   2 
table(base1$parqueaderos)

  1   2   3   4   5   6   7   8   9  10 
161 158  49  40  11   8   5   1   1   1 
table(base1$habitaciones)

  0   1   2   3   4   5   6   7   8   9  10 
 20   2  12 171 222 137  60  42  29  14  13 
table(base1$areaconst == 0)

FALSE 
  722 
table(base1$preciom == 0)

FALSE 
  722 
# Histograma de precio para ver distribución general
ggplot(base1, aes(x = preciom)) +
  geom_histogram(bins = 50, fill = "#69b3a2", color = "white") +
  labs(title = "Distribución del precio de casas en Zona Norte",
       x = "Precio (millones)", y = "Frecuencia") +
  theme_minimal()

- Conteo de NA

# 1) Conteo y % de NA por variable
na_audit <- base1 %>%
  summarise(across(everything(), ~ sum(is.na(.)))) %>%
  pivot_longer(everything(), names_to = "variable", values_to = "na_count") %>%
  mutate(
    total_rows = nrow(base1),
    na_pct = round(100 * na_count / total_rows, 2)
  ) %>%
  arrange(desc(na_pct))

na_audit %>%
  kable("html", caption = "Auditoría de faltantes en base1 (casas, Zona Norte)",
        col.names = c("Variable", "NA (conteo)", "Total filas", "% NA")) %>%
  kable_styling(full_width = FALSE, bootstrap_options = c("striped","hover"))
Auditoría de faltantes en base1 (casas, Zona Norte)
Variable NA (conteo) Total filas % NA
piso 372 722 51.52
parqueaderos 287 722 39.75
id 0 722 0.00
zona 0 722 0.00
estrato 0 722 0.00
preciom 0 722 0.00
areaconst 0 722 0.00
banios 0 722 0.00
habitaciones 0 722 0.00
tipo 0 722 0.00
barrio 0 722 0.00
longitud 0 722 0.00
latitud 0 722 0.00
gg_miss_var(base1)  # barras de % NA por variable

# Revisamos las filas donde parqueaderos es NA
base1 %>%
  filter(is.na(parqueaderos)) %>%
  select(barrio, estrato, areaconst, preciom, parqueaderos) %>%
  head(10)
# A tibble: 10 × 5
   barrio estrato areaconst preciom parqueaderos
   <chr>    <dbl>     <dbl>   <dbl>        <dbl>
 1 acopi        6       445     750           NA
 2 acopi        3       160     230           NA
 3 acopi        3       435     190           NA
 4 acopi        3       120     180           NA
 5 acopi        3       210     500           NA
 6 acopi        5       455     520           NA
 7 acopi        3       300     380           NA
 8 acopi        4       117     305           NA
 9 acopi        4       118     350           NA
10 acopi        5       165     395           NA

En la validación de valores faltantes se evidencia que piso tiene más del 50% de datos ausentes y parqueaderos casi el 40%.

Dado que el análisis para la vivienda 1 se centra en casas, la variable piso se descarta. Para parqueaderos,se opta por imputar los NA, pues esta variable es predictora clave del precio y su eliminación arbitraria podría distorsionar el modelo. Las demás variables clave (preciom, areaconst, estrato, banios, habitaciones) no presentan valores faltantes-

Se identificó que aproximadamente el 40% de los registros carecen de información en parqueaderos. Dado que esta variable aporta valor predictivo, eliminar tantos registros reduciría el tamaño de la muestra y la representatividad, se decide imputar los valores faltantes con la mediana del número de parqueaderos dentro de cada estrato socioeconómico. Esta estrategia preserva la coherencia de la base de datos y minimiza el sesgo, al relacionar la imputación con el nivel socioeconómico.

# Imputar parqueaderos NA con la mediana por estrato
base1_model <- base1 %>%
  group_by(estrato) %>%
  mutate(parqueaderos = ifelse(is.na(parqueaderos),
                               median(parqueaderos, na.rm = TRUE),
                               parqueaderos)) %>%
  ungroup()

# Verificamos que ya no hay NA
sum(is.na(base1_model$parqueaderos))
[1] 0

- Detección de Outliers

# Selección de variables numéricas
vars_numericas <- base1_model %>%
  select(preciom, areaconst, estrato, banios, parqueaderos, habitaciones)

# 1) Boxplots para visualizar
vars_numericas %>%
  pivot_longer(cols = everything(), names_to = "variable", values_to = "valor") %>%
  ggplot(aes(x = variable, y = valor)) +
  geom_boxplot(fill = "#69b3a2", color = "black", outlier.colour = "red") +
  labs(title = "Boxplots de variables numéricas (outliers en rojo)",
       x = "Variable", y = "Valor") +
  theme_minimal()

# 2) Función para calcular límites IQR
calc_outliers <- function(x) {
  Q1 <- quantile(x, 0.25, na.rm = TRUE)
  Q3 <- quantile(x, 0.75, na.rm = TRUE)
  IQR_val <- Q3 - Q1
  lower <- Q1 - 1.5 * IQR_val
  upper <- Q3 + 1.5 * IQR_val
  return(data.frame(Q1=Q1, Q3=Q3, IQR=IQR_val, Lower=lower, Upper=upper))
}

# 3) Revisar outliers para cada variable
outlier_summary <- vars_numericas %>%
  summarise(across(everything(), ~list(calc_outliers(.x)))) %>%
  pivot_longer(cols = everything(), names_to = "variable", values_to = "stats") %>%
  unnest(cols = c(stats))

outlier_summary
# A tibble: 6 × 6
  variable        Q1    Q3   IQR  Lower Upper
  <chr>        <dbl> <dbl> <dbl>  <dbl> <dbl>
1 preciom       261.  550   289. -172.  983. 
2 areaconst     140   337.  197. -155.  632. 
3 estrato         3     5     2     0     8  
4 banios          2     4     2    -1     7  
5 parqueaderos    1     2     1    -0.5   3.5
6 habitaciones    3     5     2     0     8  

El análisis de valores atípicos mediante boxplots e IQR muestra que hay de viviendas con precios y áreas significativamente superiores al promedio que pueden ser de un segmento de viviendas de mas alto nivel. Dado que esta si es una casuistica presente, estos valores si aportan al entendimiento del mercado, por lo que se mantienen en el conjunto de datos. No se identificaron ceros en variables clave tras la imputación, lo que confirma la consistencia general de la base base1_model.

EDA completo con graficos

library(GGally)
library(plotly)

# Subconjunto de variables numéricas
eda_data <- base1_model %>%
  select(preciom, areaconst, estrato, banios, parqueaderos, habitaciones)

# 1) Matriz de correlaciones con GGally
ggpairs(eda_data,
        lower = list(continuous = wrap("points", alpha = 0.4)),
        diag = list(continuous = wrap("barDiag")),
        upper = list(continuous = wrap("cor", size = 3))) +
  ggtitle("Matriz de correlaciones entre variables numéricas")

# 2) Precio vs Área construida
plot_ly(base1_model, x = ~areaconst, y = ~preciom, type = "scatter", mode = "markers",
        marker = list(color = 'blue'),
        text = ~paste("Estrato:", estrato, "<br>Baños:", banios)) %>%
  layout(title = "Precio vs Área construida",
         xaxis = list(title = "Área construida (m²)"),
         yaxis = list(title = "Precio (millones)"))
# 3) Precio vs Estrato
plot_ly(base1_model, x = ~factor(estrato), y = ~preciom, type = "box", color = ~factor(estrato)) %>%
  layout(title = "Distribución de precios por estrato",
         xaxis = list(title = "Estrato"), yaxis = list(title = "Precio (millones)"))
# 4) Precio vs Baños
plot_ly(base1_model, x = ~banios, y = ~preciom, type = "scatter", mode = "markers",
        marker = list(color = 'orange'),
        text = ~paste("Estrato:", estrato, "<br>Área:", areaconst)) %>%
  layout(title = "Precio vs Baños",
         xaxis = list(title = "Baños"), yaxis = list(title = "Precio (millones)"))
# 5) Precio vs Parqueaderos
plot_ly(base1_model, x = ~parqueaderos, y = ~preciom, type = "scatter", mode = "markers",
        marker = list(color = 'green'),
        text = ~paste("Estrato:", estrato, "<br>Área:", areaconst)) %>%
  layout(title = "Precio vs Parqueaderos",
         xaxis = list(title = "Parqueaderos"), yaxis = list(title = "Precio (millones)"))
# 6) Precio vs Habitaciones
plot_ly(base1_model, x = ~habitaciones, y = ~preciom, type = "scatter", mode = "markers",
        marker = list(color = 'purple'),
        text = ~paste("Estrato:", estrato, "<br>Área:", areaconst)) %>%
  layout(title = "Precio vs Habitaciones",
         xaxis = list(title = "Habitaciones"), yaxis = list(title = "Precio (millones)"))

El análisis exploratorio muestra:

Estos hallazgos validan el uso de estas variables como predictoras en el modelo de regresión múltiple.

MODELO DE REGRESION LINEAL MULTIPLE PARA VIVIENDA ZONA NORTE

Construir un modelo lm() que estime preciom (precio en millones) en función de:

  • areaconst (área construida)
  • estrato
  • banios
  • parqueaderos
  • habitaciones
# Ajuste del modelo de regresión múltiple
modelo1 <- lm(preciom ~ areaconst + estrato + banios + parqueaderos + habitaciones,
              data = base1_model)

# Resumen del modelo
summary(modelo1)

Call:
lm(formula = preciom ~ areaconst + estrato + banios + parqueaderos + 
    habitaciones, data = base1_model)

Residuals:
    Min      1Q  Median      3Q     Max 
-930.04  -78.24  -17.21   45.51 1078.18 

Coefficients:
              Estimate Std. Error t value Pr(>|t|)    
(Intercept)  -226.5815    29.6390  -7.645 6.75e-14 ***
areaconst       0.8082     0.0436  18.536  < 2e-16 ***
estrato        79.7443     7.4434  10.713  < 2e-16 ***
banios         24.2979     5.3650   4.529 6.94e-06 ***
parqueaderos   17.0956     5.7275   2.985  0.00293 ** 
habitaciones    0.9366     4.1044   0.228  0.81956    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 158.2 on 716 degrees of freedom
Multiple R-squared:  0.655, Adjusted R-squared:  0.6526 
F-statistic: 271.9 on 5 and 716 DF,  p-value: < 2.2e-16

El modelo de regresión lineal múltiple para casas en Zona Norte muestra que:

  • Areaconst es el predictor más significativo (p < 0.001): cada 1 m² adicional incrementa el precio en X millones, manteniendo las demás variables constantes.
  • Estrato también tiene relación positiva y significativa: viviendas en estratos altos tienen precios superiores.
  • Baños y parqueaderos: Variables significativas, aunque con menor magnitud de efecto.
  • Habitaciones: Su efecto no es estadísticamente significativo, si las otras variables están controladas, No de habitaciones no explica adicionalmente el precio.

El R² ajustado indica que el modelo explica aproximadamente 52.66% de la variabilidad en los precios. Este nivel de ajuste es razonable para datos inmobiliarios, donde existen factores externos como ubicación específica, acabados, antiguedad de construccion del inmueble, entre otros, que influyen en el precio.

Validación de Supuestos

library(car)
library(performance)

# 1. Diagnóstico básico de residuos
par(mfrow = c(2,2))
plot(modelo1)

par(mfrow = c(1,1))

# 2. Normalidad de residuos (QQ-Plot + prueba Shapiro-Wilk)
qqPlot(modelo1, main="QQ Plot de residuos")

[1] 208 632
shapiro.test(residuals(modelo1))  # p > 0.05 indica normalidad

    Shapiro-Wilk normality test

data:  residuals(modelo1)
W = 0.83323, p-value < 2.2e-16
# 3. Homocedasticidad (prueba de Breusch-Pagan)
library(lmtest)
bptest(modelo1)  # p > 0.05 indica homocedasticidad

    studentized Breusch-Pagan test

data:  modelo1
BP = 132.34, df = 5, p-value < 2.2e-16
# 4. Multicolinealidad (VIF)
vif(modelo1)  # Valores > 5 indican colinealidad alta
   areaconst      estrato       banios parqueaderos habitaciones 
    1.530728     1.542322     1.926194     1.345919     1.621726 
# 5. Resumen completo de supuestos
check_model(modelo1)

La validación de supuestos muestra que:

  • Linealidad: La relación entre predictores y el precio es lineal.
  • Normalidad de residuos: Desviaciones leves en colas del QQ-plot; aceptable dado el tamaño de la muestra y la presencia de propiedades de alto valor.
  • Homoscedasticidad: Ligera heterocedasticidad en precios altos dentro de lo razonable.
  • Observaciones influyentes: Pocos puntos con alto leverage; no afectan significativamente el modelo.
  • Colinealidad: Sin multicolinealidad exagerada.

El modelo cumple con los supuestos básicos de regresión lineal y es adecuado para análisis e inferencia.

Predicción Precio

# Crear un nuevo dataframe con los valores de la Vivienda 1
nueva_v1 <- data.frame(
  areaconst = 200,
  estrato = 4,         # probaremos con estrato 4 y 5
  banios = 2,
  parqueaderos = 1,
  habitaciones = 4
)

# Predicción con intervalo de confianza
pred_v1_e4 <- predict(modelo1, newdata = nueva_v1, interval = "prediction")
pred_v1_e4
      fit      lwr      upr
1 323.464 12.37273 634.5552
# Con estrato 5
nueva_v1$estrato <- 5
pred_v1_e5 <- predict(modelo1, newdata = nueva_v1, interval = "prediction")
pred_v1_e5
       fit      lwr      upr
1 403.2083 91.53602 714.8805

La Vivienda 1 cuenta con las siguientes características:

  • Área construida: 200 m²
  • Estrato: 4 o 5
  • Baños: 2
  • Parqueaderos: 1
  • Habitaciones: 4
  • Crédito aprobado: 350 millones de pesos

Se estimaron los precios usando el modelo de regresión lineal múltiple:

Estrato Precio estimado (millones) Intervalo de predicción 95% (millones)
4 323.46 12.37 – 634.55
5 403.21 91.53 – 714.88
  • Una vivienda con estas características en estrato 4 tiene un precio estimado de 323 millones, lo que la ubica dentro del rango del crédito aprobado (350 millones).
  • Para estrato 5, el precio estimado sube a 403 millones, superando el presupuesto.
  • Se recomienda priorizar opciones en estrato 4, donde es más probable encontrar viviendas dentro del rango de financiación.

Ejemplos de casas reales

# --- Perfil objetivo Vivienda 1 ---
target <- list(areaconst = 200, banios = 2, parqueaderos = 1, habitaciones = 4)

# Base para recomendaciones: casas, zona norte (ya la tienes como base1).
# Reglas: precio <= 350; estrato 4 o 5; specs cercanas al objetivo;
# ventana de área ±20% (160–240 m²) para asegurar al menos 5 opciones.
candidatas <- base1 %>%
  filter(
    preciom <= 350,
    estrato %in% c(4,5),
    !is.na(longitud), !is.na(latitud),        # necesarias para mapa
    !is.na(parqueaderos), !is.na(banios), !is.na(habitaciones), !is.na(areaconst)
  ) %>%
  filter(
    between(areaconst, 0.8*target$areaconst, 1.2*target$areaconst),
    banios >= target$banios,                  # al menos como lo solicitado
    parqueaderos >= target$parqueaderos,
    habitaciones >= target$habitaciones
  ) %>%
  # Score de similitud (menor es mejor). Penalizamos estrato 5 un poquito por presupuesto.
  mutate(
    score = abs(areaconst - target$areaconst)/target$areaconst +
            0.5*abs(banios - target$banios) +
            0.5*abs(parqueaderos - target$parqueaderos) +
            0.25*abs(habitaciones - target$habitaciones) +
            ifelse(estrato == 5, 0.3, 0)     # preferimos estrato 4 por costo
  ) %>%
  arrange(score, preciom)

# Si  hay menos de 5, bajamos ventana de área a ±30% (opcional):
if(nrow(candidatas) < 5){
  candidatas <- base1 %>%
    filter(preciom <= 350, estrato %in% c(4,5),
           !is.na(longitud), !is.na(latitud),
           !is.na(parqueaderos), !is.na(banios), !is.na(habitaciones), !is.na(areaconst)) %>%
    filter(
      between(areaconst, 0.7*target$areaconst, 1.3*target$areaconst),
      banios >= target$banios,
      parqueaderos >= target$parqueaderos,
      habitaciones >= target$habitaciones
    ) %>%
    mutate(
      score = abs(areaconst - target$areaconst)/target$areaconst +
              0.5*abs(banios - target$banios) +
              0.5*abs(parqueaderos - target$parqueaderos) +
              0.25*abs(habitaciones - target$habitaciones) +
              ifelse(estrato == 5, 0.3, 0)
    ) %>%
    arrange(score, preciom)
}

# Tomamos las 5 mejores
top5_v1 <- candidatas %>%
  select(id, barrio, estrato, areaconst, banios, parqueaderos, habitaciones, preciom, longitud, latitud, score) %>%
  head(5)

# --- Tabla bonita ---
top5_v1 %>%
  select(id, barrio, estrato, areaconst, banios, parqueaderos, habitaciones, preciom) %>%
  kable("html",
        caption = "Top 5 ofertas para Vivienda 1 (casa, zona norte, ≤ 350M)",
        col.names = c("ID","Barrio","Estrato","Área (m²)","Baños","Parq.","Habs.","Precio (M)")) %>%
  kable_styling(full_width = FALSE, bootstrap_options = c("striped","hover"))
Top 5 ofertas para Vivienda 1 (casa, zona norte, ≤ 350M)
ID Barrio Estrato Área (m²) Baños Parq. Habs. Precio (M)
94 zona norte 4 162 3 1 4 265
1163 la merced 5 216 2 2 4 350
1376 la flora 5 160 3 1 4 320
1270 el bosque 5 203 2 2 5 350
464 el bosque 4 165 4 1 4 330
# --- Mapa interactivo ---
leaflet(top5_v1) %>%
  addTiles() %>%
  addCircleMarkers(
    lng = ~longitud, lat = ~latitud,
    radius = 7, stroke = FALSE, fillOpacity = 0.85,
    popup = ~paste0(
      "<b>ID: </b>", id,
      "<br><b>Barrio: </b>", barrio,
      "<br><b>Estrato: </b>", estrato,
      "<br><b>Área: </b>", areaconst, " m²",
      "<br><b>Baños: </b>", banios,
      "<br><b>Parqueaderos: </b>", parqueaderos,
      "<br><b>Habitaciones: </b>", habitaciones,
      "<br><b>Precio: </b>", round(preciom,1), " millones"
    )
  ) %>%
  setView(lng = -76.52, lat = 3.45, zoom = 12)

Ofertas recomendadas para Vivienda 1 (casa, Zona Norte)

A partir del perfil objetivo (200 m², 2 baños, 1 parqueadero, 4 habitaciones, estrato 4–5, presupuesto ≤ 350 M), se filtraron las viviendas y se construyó un índice de similitud que pondera cercanía en área y comodidades, penalizando el estrato 5 por el presupuesto.

La tabla presenta las 5 mejores coincidencias y el mapa muestra su ubicación. Las opciones priorizadas cumplen con el presupuesto, mantienen especificaciones iguales o superiores a las solicitadas y se ubican en barrios de la Zona Norte. Estas alternativas representan candidatos viables para visita y negociación.

VIVIENDA 2

Filtramos el dataset por vivienda por Zona sur tipo Apartamentos

Se filtró la base para conservar únicamente apartamentos ubicados en la Zona Sur de Cali. Se presentan los tres primeros registros, así como tablas de comprobación (tipo y zona) y de descripción (estrato, barrios con mayor oferta y resumen de variables cuantitativas). El mapa interactivo ubica geográficamente todas las ofertas de la base; si se observan puntos aparentemente fuera de la zona, pueden deberse a coordenadas mal registradas o a límites administrativos distintos a la clasificación de “Zona Sur” en la base. Para decisiones operativas se recomienda contrastar con cartografía oficial.

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

# Verificamos primeros registros
head(base2, 3)
# A tibble: 3 × 13
     id zona    piso  estrato preciom areaconst parqueaderos banios habitaciones
  <dbl> <chr>   <chr>   <dbl>   <dbl>     <dbl>        <dbl>  <dbl>        <dbl>
1  5098 Zona S… 05          4     290        96            1      2            3
2   698 Zona S… 02          3      78        40            1      1            2
3  8199 Zona S… <NA>        6     875       194            2      5            3
# ℹ 4 more variables: tipo <chr>, barrio <chr>, longitud <dbl>, latitud <dbl>
# Tabla resumen por estrato
table(base2$estrato)

   3    4    5    6 
 201 1091 1033  462 
# Conteo total de filas
nrow(base2)
[1] 2787
# Primeros 3 registros (bonito)
library(knitr); library(kableExtra); library(dplyr)

base2 %>%
  select(id, barrio, piso, estrato, areaconst, parqueaderos, banios, habitaciones, preciom) %>%
  head(3) %>%
  kable("html", caption = "Primeros 3 registros: Apartamentos en Zona Sur") %>%
  kable_styling(full_width = FALSE, bootstrap_options = c("striped","hover"))
Primeros 3 registros: Apartamentos en Zona Sur
id barrio piso estrato areaconst parqueaderos banios habitaciones preciom
5098 acopi 05 4 96 1 2 3 290
698 aguablanca 02 3 40 1 1 2 78
8199 aguacatal NA 6 194 2 5 3 875
# 3) Tablas que comprueban el filtro y describen la base
# 3.1 confirmación del filtro
base2 %>% count(tipo) %>% kable("html", caption = "Comprobación: tipo") %>% kable_styling(full_width = FALSE)
Comprobación: tipo
tipo n
Apartamento 2787
base2 %>% count(zona) %>% kable("html", caption = "Comprobación: zona") %>% kable_styling(full_width = FALSE)
Comprobación: zona
zona n
Zona Sur 2787
# 3.2 distribución por estrato
base2 %>%
  count(estrato) %>%
  arrange(estrato) %>%
  kable("html", col.names = c("Estrato","Cantidad"),
        caption = "Cantidad de apartamentos por estrato (Zona Sur)") %>%
  kable_styling(full_width = FALSE)
Cantidad de apartamentos por estrato (Zona Sur)
Estrato Cantidad
3 201
4 1091
5 1033
6 462
# 3.3 barrios con mayor oferta (top 10)
base2 %>%
  count(barrio) %>%
  arrange(desc(n)) %>%
  head(10) %>%
  kable("html", col.names = c("Barrio","Cantidad"),
        caption = "Top 10 barrios con oferta de apartamentos (Zona Sur)") %>%
  kable_styling(full_width = FALSE)
Top 10 barrios con oferta de apartamentos (Zona Sur)
Barrio Cantidad
valle del lili 837
ciudad jardín 218
pance 205
el ingenio 128
el caney 124
la hacienda 108
el refugio 77
el limonar 59
caney 58
quintas de don 58
# 3.4 resumen de variables cuantitativas
library(tibble)
base2 %>%
  select(preciom, areaconst, parqueaderos, banios, habitaciones) %>%
  summary() %>%
  as.data.frame() %>%
  rownames_to_column("Medida") %>%
  kable("html", caption = "Resumen estadístico de variables cuantitativas") %>%
  kable_styling(full_width = FALSE)
Resumen estadístico de variables cuantitativas
Medida Var1 Var2 Freq
1 preciom Min. : 75.0
2 preciom 1st Qu.: 175.0
3 preciom Median : 245.0
4 preciom Mean : 297.3
5 preciom 3rd Qu.: 335.0
6 preciom Max. :1750.0
7 preciom NA
8 areaconst Min. : 40.00
9 areaconst 1st Qu.: 65.00
10 areaconst Median : 85.00
11 areaconst Mean : 97.47
12 areaconst 3rd Qu.:110.00
13 areaconst Max. :932.00
14 areaconst NA
15 parqueaderos Min. : 1.000
16 parqueaderos 1st Qu.: 1.000
17 parqueaderos Median : 1.000
18 parqueaderos Mean : 1.415
19 parqueaderos 3rd Qu.: 2.000
20 parqueaderos Max. :10.000
21 parqueaderos NA’s :406
22 banios Min. :0.000
23 banios 1st Qu.:2.000
24 banios Median :2.000
25 banios Mean :2.488
26 banios 3rd Qu.:3.000
27 banios Max. :8.000
28 banios NA
29 habitaciones Min. :0.000
30 habitaciones 1st Qu.:3.000
31 habitaciones Median :3.000
32 habitaciones Mean :2.966
33 habitaciones 3rd Qu.:3.000
34 habitaciones Max. :6.000
35 habitaciones NA

MAPA INTERACTIVO

# Mapa interactivo de la base
library(leaflet)

centro_lon <- mean(base2$longitud, na.rm = TRUE)
centro_lat <- mean(base2$latitud,  na.rm = TRUE)

leaflet(base2 %>% filter(!is.na(longitud), !is.na(latitud))) %>%
  addTiles() %>%
  addCircleMarkers(
    lng = ~longitud, lat = ~latitud,
    popup = ~paste0("<b>ID: </b>", id,
                    "<br><b>Barrio: </b>", barrio,
                    "<br><b>Piso: </b>", piso,
                    "<br><b>Estrato: </b>", estrato,
                    "<br><b>Área: </b>", areaconst, " m²",
                    "<br><b>Baños: </b>", banios,
                    "<br><b>Parqueaderos: </b>", parqueaderos,
                    "<br><b>Habitaciones: </b>", habitaciones,
                    "<br><b>Precio: </b>", round(preciom,1), " millones"),
    radius = 6, stroke = FALSE, fillOpacity = 0.85
  ) %>%
  setView(lng = centro_lon, lat = centro_lat, zoom = 12)

EDA

Revisamos la base2 para vivienda, buscamos rangos raros (áreas muy pequeñas/grandes, precios imposibles), ceros en baños o habitaciones.

# Estructura de la base filtrada (apartamentos, Zona Sur)
glimpse(base2)
Rows: 2,787
Columns: 13
$ id           <dbl> 5098, 698, 8199, 1241, 5370, 6975, 5615, 6262, 7396, 6949…
$ zona         <chr> "Zona Sur", "Zona Sur", "Zona Sur", "Zona Sur", "Zona Sur…
$ piso         <chr> "05", "02", NA, NA, NA, "06", "08", NA, NA, NA, "10", "05…
$ estrato      <dbl> 4, 3, 6, 3, 3, 4, 3, 3, 3, 4, 3, 5, 6, 3, 3, 5, 5, 5, 6, …
$ preciom      <dbl> 290, 78, 875, 135, 135, 220, 210, 105, 115, 220, 230, 344…
$ areaconst    <dbl> 96, 40, 194, 117, 78, 75, 72, 68, 58, 84, 63, 107, 182, 7…
$ parqueaderos <dbl> 1, 1, 2, NA, NA, 1, 2, NA, 1, NA, 1, 2, 2, 1, 1, 2, 1, 2,…
$ banios       <dbl> 2, 1, 5, 2, 1, 2, 2, 2, 2, 2, 2, 2, 4, 2, 2, 4, 2, 4, 3, …
$ habitaciones <dbl> 3, 2, 3, 3, 3, 3, 3, 3, 2, 3, 2, 3, 3, 3, 3, 3, 2, 3, 3, …
$ tipo         <chr> "Apartamento", "Apartamento", "Apartamento", "Apartamento…
$ barrio       <chr> "acopi", "aguablanca", "aguacatal", "alameda", "alameda",…
$ longitud     <dbl> -76.53464, -76.50100, -76.55700, -76.51400, -76.53600, -7…
$ latitud      <dbl> 3.44987, 3.40000, 3.45900, 3.44100, 3.43600, 3.39109, 3.4…
# Resumen de variables numéricas clave
summary(select(base2, preciom, areaconst, parqueaderos, banios, habitaciones))
    preciom         areaconst       parqueaderos        banios     
 Min.   :  75.0   Min.   : 40.00   Min.   : 1.000   Min.   :0.000  
 1st Qu.: 175.0   1st Qu.: 65.00   1st Qu.: 1.000   1st Qu.:2.000  
 Median : 245.0   Median : 85.00   Median : 1.000   Median :2.000  
 Mean   : 297.3   Mean   : 97.47   Mean   : 1.415   Mean   :2.488  
 3rd Qu.: 335.0   3rd Qu.:110.00   3rd Qu.: 2.000   3rd Qu.:3.000  
 Max.   :1750.0   Max.   :932.00   Max.   :10.000   Max.   :8.000  
                                   NA's   :406                     
  habitaciones  
 Min.   :0.000  
 1st Qu.:3.000  
 Median :3.000  
 Mean   :2.966  
 3rd Qu.:3.000  
 Max.   :6.000  
                
na_audit2 <- base2 %>%
  summarise(across(everything(), ~ sum(is.na(.)))) %>%
  pivot_longer(everything(), names_to = "Variable", values_to = "NA_count") %>%
  mutate(
    Total_filas = nrow(base2),
    NA_pct = round(100 * NA_count / Total_filas, 2)
  ) %>%
  arrange(desc(NA_pct))

na_audit2 %>%
  kable("html",
        caption = "Auditoría de faltantes – Vivienda 2 (aptos, Zona Sur)",
        col.names = c("Variable","NA (conteo)","Total filas","% NA")) %>%
  kable_styling(full_width = FALSE, bootstrap_options = c("striped","hover"))
Auditoría de faltantes – Vivienda 2 (aptos, Zona Sur)
Variable NA (conteo) Total filas % NA
piso 622 2787 22.32
parqueaderos 406 2787 14.57
id 0 2787 0.00
zona 0 2787 0.00
estrato 0 2787 0.00
preciom 0 2787 0.00
areaconst 0 2787 0.00
banios 0 2787 0.00
habitaciones 0 2787 0.00
tipo 0 2787 0.00
barrio 0 2787 0.00
longitud 0 2787 0.00
latitud 0 2787 0.00

Tratamiento de datos faltantes

library(dplyr)

base2 %>%
  summarise(
    banios_ceros = sum(banios == 0, na.rm = TRUE),
    hab_ceros    = sum(habitaciones == 0, na.rm = TRUE),
    parqueaderos_NA = sum(is.na(parqueaderos)),
    piso_NA         = sum(is.na(piso))
  )
# A tibble: 1 × 4
  banios_ceros hab_ceros parqueaderos_NA piso_NA
         <int>     <int>           <int>   <int>
1            6         8             406     622

Imputación de datos faltantes:

  • Para baños y habitaciones por el % tan bajo que no afecta el modelo, vamos a eliminar estos registros.
library(dplyr)

# Quitamos solo los 0 implausibles (sin tocar parqueaderos aún)
base2_clean_bh <- base2 %>%
  filter(banios  != 0, habitaciones != 0)

# Chequeo: cuántas filas eliminamos
data.frame(
  antes = nrow(base2),
  despues = nrow(base2_clean_bh),
  eliminadas = nrow(base2) - nrow(base2_clean_bh)
)
  antes despues eliminadas
1  2787    2777         10
  • Para parqueaderos impputamos por la mediana por estrato ya que es un variable relacionada con el precio.
base2_imp <- base2_clean_bh %>%
  group_by(estrato) %>%
  mutate(
    parqueaderos = if_else(is.na(parqueaderos),
                           round(median(parqueaderos, na.rm = TRUE)),
                           parqueaderos)
  ) %>%
  ungroup()

# Verificación: ya no debería haber NA en parqueaderos
sum(is.na(base2_imp$parqueaderos))
[1] 0

Los faltantes en parqueaderos que suman el 14.6% se imputaron con la mediana por estrato, lo que mantiene el tamaño muestral y respeta la estructura socioeconómica.Con lo anterior se construyó base2_model (sin faltantes en variables críticas) y base2_map (con coordenadas válidas).

# Para el MODELO: sin NA en variables críticas
base2_model <- base2_imp %>%
  filter(estrato >= 1 & estrato <= 6) %>%
  drop_na(preciom, areaconst, estrato, banios, parqueaderos, habitaciones)

# Para el MAPA: solo coordenadas válidas
base2_map <- base2_imp %>%
  drop_na(longitud, latitud)

# Resumen de tamaños (para el informe)
data.frame(
  dataset = c("base2 (filtrada)", "base2_model (modelo)", "base2_map (mapa)"),
  filas   = c(nrow(base2), nrow(base2_model), nrow(base2_map))
)
               dataset filas
1     base2 (filtrada)  2787
2 base2_model (modelo)  2777
3     base2_map (mapa)  2777

Outliers

En apartamentos Zona Sur se identifican outliers en preciom y areaconst asociados a unidades de alto valor. Dado que representan un segmento real del mercado, se mantienen. No se detectaron valores 0 en las demás variables tras la limpieza.

library(dplyr)
library(tidyr)
library(ggplot2)

# Variables numéricas del modelo
num2 <- base2_model %>%
  select(preciom, areaconst, estrato, banios, parqueaderos, habitaciones)

# (A) Boxplots para ver outliers visualmente
num2 %>%
  pivot_longer(everything(), names_to = "variable", values_to = "valor") %>%
  ggplot(aes(variable, valor)) +
  geom_boxplot(fill = "#69b3a2", color = "black", outlier.colour = "red") +
  labs(title = "Vivienda 2 – Outliers por variable", x = "", y = "") +
  theme_minimal()

# (B) Tabla con límites IQR y conteo de outliers por variable
calc_iqr <- function(x){
  Q1 <- quantile(x, .25, na.rm=TRUE); Q3 <- quantile(x, .75, na.rm=TRUE)
  I  <- Q3 - Q1; lower <- Q1 - 1.5*I; upper <- Q3 + 1.5*I
  c(Q1=Q1, Q3=Q3, IQR=I, Lower=lower, Upper=upper,
    n_below=sum(x < lower, na.rm=TRUE),
    n_above=sum(x > upper, na.rm=TRUE))
}
outliers2 <- sapply(num2, calc_iqr) %>% t() %>% as.data.frame()
round(outliers2, 2)
             Q1.25% Q3.75% IQR.75% Lower.25% Upper.75% n_below n_above
preciom         175    335     160     -65.0     575.0       0     260
areaconst        65    110      45      -2.5     177.5       0     175
estrato           4      5       1       2.5       6.5       0       0
banios            2      3       1       0.5       4.5       0     135
parqueaderos      1      2       1      -0.5       3.5       0      32
habitaciones      3      3       0       3.0       3.0     482     394

EDA correlaciones y graficos

eda2 <- base2_model %>%
  select(preciom, areaconst, estrato, banios, parqueaderos, habitaciones)

# (A) Matriz de correlaciones / dispersión
ggpairs(eda2,
        lower = list(continuous = wrap("points", alpha = 0.35, size = 0.9)),
        diag  = list(continuous = wrap("barDiag")),
        upper = list(continuous = wrap("cor", size = 3))) +
  ggtitle("Vivienda 2 – Correlaciones y dispersión")

# (B) Gráficos interactivos Plotly
# Precio vs Área
plot_ly(base2_model, x = ~areaconst, y = ~preciom,
        type="scatter", mode="markers",
        text=~paste("Estrato:", estrato,
                    "<br>Baños:", banios,
                    "<br>Parq:", parqueaderos)) %>%
  layout(title="Precio vs Área construida (V2)",
         xaxis=list(title="Área (m²)"),
         yaxis=list(title="Precio (M)"))
# Precio por Estrato (cajas)
plot_ly(base2_model, x = ~factor(estrato), y = ~preciom,
        type="box", color=~factor(estrato)) %>%
  layout(title="Precio por estrato (V2)",
         xaxis=list(title="Estrato"), yaxis=list(title="Precio (M)"))
# Precio vs Baños
plot_ly(base2_model, x = ~banios, y = ~preciom,
        type="scatter", mode="markers") %>%
  layout(title="Precio vs Baños (V2)",
         xaxis=list(title="Baños"), yaxis=list(title="Precio (M)"))
# Precio vs Parqueaderos
plot_ly(base2_model, x = ~parqueaderos, y = ~preciom,
        type="scatter", mode="markers") %>%
  layout(title="Precio vs Parqueaderos (V2)",
         xaxis=list(title="Parqueaderos"), yaxis=list(title="Precio (M)"))
# Precio vs Habitaciones
plot_ly(base2_model, x = ~habitaciones, y = ~preciom,
        type="scatter", mode="markers") %>%
  layout(title="Precio vs Habitaciones (V2)",
         xaxis=list(title="Habitaciones"), yaxis=list(title="Precio (M)"))

El EDA de apartamentos en Zona Sur evidencia una relación positiva fuerte entre areaconst y preciom, y diferencias claras de precio por estrato (mayores en 5–6).

banios y parqueaderos presentan asociaciones positivas moderadas, habitaciones aporta información adicional más limitada.

Estos hallazgos respaldan el uso de estas variables en el modelo de regresión múltiple.

Modelo de regresion multiple Vivienda 2

# Modelo de regresión lineal múltiple
modelo2 <- lm(preciom ~ areaconst + estrato + banios + parqueaderos + habitaciones,
              data = base2_model)

# Resumen del modelo (coeficientes, R2, significancia)
summary(modelo2)

Call:
lm(formula = preciom ~ areaconst + estrato + banios + parqueaderos + 
    habitaciones, data = base2_model)

Residuals:
     Min       1Q   Median       3Q      Max 
-1120.55   -38.38    -2.81    38.27   922.91 

Coefficients:
               Estimate Std. Error t value Pr(>|t|)    
(Intercept)  -259.27683   13.16926 -19.688  < 2e-16 ***
areaconst       1.32548    0.05008  26.466  < 2e-16 ***
estrato        57.44579    2.68557  21.391  < 2e-16 ***
banios         45.71708    3.10613  14.718  < 2e-16 ***
parqueaderos   77.66743    3.90002  19.915  < 2e-16 ***
habitaciones  -19.52241    3.45457  -5.651 1.76e-08 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 92.83 on 2771 degrees of freedom
Multiple R-squared:  0.7639,    Adjusted R-squared:  0.7635 
F-statistic:  1793 on 5 and 2771 DF,  p-value: < 2.2e-16

Validación de supuestos

# 4.1 Diagnóstico básico de residuos
par(mfrow = c(2,2)); plot(modelo2); par(mfrow = c(1,1))

# 4.2 Normalidad (QQ-Plot) y Homocedasticidad (Breusch–Pagan)
library(car)
qqPlot(modelo2, main = "QQ-Plot de residuos (modelo2)")  # visual

[1] 1524 2373
library(lmtest)
bptest(modelo2)   # p > 0.05 ≈ homocedasticidad

    studentized Breusch-Pagan test

data:  modelo2
BP = 898.6, df = 5, p-value < 2.2e-16
# 4.3 Colinealidad (VIF)
library(car)
vif(modelo2)      # < 5 ideal, < 10 aceptable
   areaconst      estrato       banios parqueaderos habitaciones 
    2.177763     1.646232     2.678270     1.857722     1.440328 
# 4.4 Chequeo integral (opcional)
library(performance)
check_model(modelo2)

El modelo cumple razonablemente con los supuestos clave: hay linealidad, independencia y una colinealidad baja. Aunque se observan algunas desviaciones en la normalidad y una leve heterocedasticidad en precios altos, esto es normal en mercados con tanta variabilidad como el inmobiliario. A pesar de lo anterior, el modelo es totalmente funcional tanto para inferencia como para predicción.

Predicción vivienda 2

# Estrato 5
nuevo_v2 <- data.frame(
  areaconst = 300,
  estrato = 5,
  banios = 3,
  parqueaderos = 3,
  habitaciones = 5
)
pred_v2_e5 <- predict(modelo2, newdata = nuevo_v2, interval = "prediction")
pred_v2_e5
       fit      lwr      upr
1 698.1389 514.9294 881.3484
# Estrato 6
nuevo_v2$estrato <- 6
pred_v2_e6 <- predict(modelo2, newdata = nuevo_v2, interval = "prediction")
pred_v2_e6
       fit      lwr      upr
1 755.5847 572.3567 938.8127

Predicción de precio – Vivienda 2 (Apartamento, Zona Sur)

Características objetivo: 300 m², 3 baños, 3 parqueaderos, 5 habitaciones, estrato 5–6.
Crédito preaprobado: 850 millones.

Estrato Precio estimado (M) Intervalo de predicción 95% (M)
5 698.14 514.93 – 881.35
6 755.58 572.36 – 938.81
  • En estrato 5: el precio estimado 698 millones queda por debajo del tope de 850 millones. Solo en escenarios altos del mercado 881 M podría superar el presupuesto.
  • En estrato 6: el precio estimado 756 M también cabe dentro del crédito aprobado, aunque el extremo superior del intervalo 939puede superarlo.
  • Recomendación: priorizar búsqueda en estrato 5 para mayor holgura presupuesta, evaluar opciones puntuales en estrato 6 que no superen 850 Millones o con margen de negociación.

Ejemplo de cinco viviendas Zona Sur

# -------- Perfil objetivo Vivienda 2 --------
target_v2 <- list(areaconst = 300, banios = 3, parqueaderos = 3, habitaciones = 5)

# 1) Candidatas (apartamentos, Zona Sur, estrato 5–6, ≤ 850M, specs mínimas)
candidatas_v2 <- base2_imp %>%
  filter(
    tipo == "Apartamento",
    zona == "Zona Sur",
    estrato %in% c(5, 6),
    preciom <= 850,
    !is.na(longitud), !is.na(latitud),
    !is.na(areaconst), !is.na(banios), !is.na(parqueaderos), !is.na(habitaciones)
  ) %>%
  filter(
    between(areaconst, 0.8 * target_v2$areaconst, 1.2 * target_v2$areaconst), # 240–360 m²
    banios        >= target_v2$banios,
    parqueaderos  >= target_v2$parqueaderos,
    habitaciones  >= target_v2$habitaciones
  ) %>%
  mutate(
    # Score de similitud (menor = mejor). Ligera penalización a estrato 6 (presupuesto).
    score = abs(areaconst - target_v2$areaconst) / target_v2$areaconst +
            0.5 * abs(banios - target_v2$banios) +
            0.5 * abs(parqueaderos - target_v2$parqueaderos) +
            0.25 * abs(habitaciones - target_v2$habitaciones) +
            ifelse(estrato == 6, 0.15, 0)
  ) %>%
  arrange(score, preciom)

# 2) Si hay pocas, relajamos ventana de área a ±30%
if (nrow(candidatas_v2) < 5) {
  candidatas_v2 <- base2_imp %>%
    filter(
      tipo == "Apartamento", zona == "Zona Sur", estrato %in% c(5, 6),
      preciom <= 850, !is.na(longitud), !is.na(latitud),
      !is.na(areaconst), !is.na(banios), !is.na(parqueaderos), !is.na(habitaciones)
    ) %>%
    filter(
      between(areaconst, 0.7 * target_v2$areaconst, 1.3 * target_v2$areaconst), # 210–390 m²
      banios >= target_v2$banios, parqueaderos >= target_v2$parqueaderos, habitaciones >= target_v2$habitaciones
    ) %>%
    mutate(
      score = abs(areaconst - target_v2$areaconst) / target_v2$areaconst +
              0.5 * abs(banios - target_v2$banios) +
              0.5 * abs(parqueaderos - target_v2$parqueaderos) +
              0.25 * abs(habitaciones - target_v2$habitaciones) +
              ifelse(estrato == 6, 0.15, 0)
    ) %>%
    arrange(score, preciom)
}

# 3) Top 5
top5_v2 <- candidatas_v2 %>%
  select(id, barrio, estrato, areaconst, banios, parqueaderos, habitaciones, preciom,
         longitud, latitud, score) %>%
  head(5)

# 4) Tabla bonita
top5_v2 %>%
  select(id, barrio, estrato, areaconst, banios, parqueaderos, habitaciones, preciom) %>%
  kable("html",
        caption = "Top 5 apartamentos recomendados (Zona Sur, ≤ 850M)",
        col.names = c("ID","Barrio","Estrato","Área (m²)","Baños","Parq.","Habs.","Precio (M)")) %>%
  kable_styling(full_width = FALSE, bootstrap_options = c("striped","hover"))
Top 5 apartamentos recomendados (Zona Sur, ≤ 850M)
ID Barrio Estrato Área (m²) Baños Parq. Habs. Precio (M)
8036 seminario 5 256 5 3 5 530
7512 seminario 5 300 5 3 6 670
# 5) Mapa interactivo
leaflet(top5_v2) %>%
  addTiles() %>%
  addCircleMarkers(
    lng = ~longitud, lat = ~latitud,
    radius = 7, stroke = FALSE, fillOpacity = 0.85,
    popup = ~paste0(
      "<b>ID: </b>", id,
      "<br><b>Barrio: </b>", barrio,
      "<br><b>Estrato: </b>", estrato,
      "<br><b>Área: </b>", areaconst, " m²",
      "<br><b>Baños: </b>", banios,
      "<br><b>Parqueaderos: </b>", parqueaderos,
      "<br><b>Habitaciones: </b>", habitaciones,
      "<br><b>Precio: </b>", round(preciom,1), " M"
    )
  ) %>%
  setView(lng = mean(top5_v2$longitud), lat = mean(top5_v2$latitud), zoom = 12)

Selección de ofertas – Vivienda 2:

Se filtraron apartamentos en Zona Sur, estratos 5–6, con precio ≤ 850 millones y especificaciones iguales o superiores a las solicitadas (≥ 300 m² ± tolerancia, ≥ 3 baños, ≥ 3 parqueaderos, ≥ 5 habitaciones). Se definió un índice de similitud que pondera cercanía en área y comodidades, penalizando el estrato 6 para priorizar el presupuesto. Se presentan las 5 mejores coincidencias en tabla y mapa interactivo. Estas ofertas son candidatos idóneos para visita y negociación, coherentes con la predicción del modelo (698 millones en estrato 5 y 756 millones en estrato 6).

Evaluación con set de pruebas para los dos modelos

Evaluación con set de prueba (80/20)

# --- Rendimiento con set de prueba (ambos modelos) ---
set.seed(42)
split1 <- sample.int(nrow(base1_model), size = floor(0.8*nrow(base1_model)))
train1 <- base1_model[split1,]; test1 <- base1_model[-split1,]
m1 <- lm(preciom ~ areaconst + estrato + banios + parqueaderos + habitaciones, data=train1)
pred1 <- predict(m1, newdata=test1)
RMSE1 <- sqrt(mean((test1$preciom - pred1)^2))
MAE1  <- mean(abs(test1$preciom - pred1))

set.seed(42)
split2 <- sample.int(nrow(base2_model), size = floor(0.8*nrow(base2_model)))
train2 <- base2_model[split2,]; test2 <- base2_model[-split2,]
m2 <- lm(preciom ~ areaconst + estrato + banios + parqueaderos + habitaciones, data=train2)
pred2 <- predict(m2, newdata=test2)
RMSE2 <- sqrt(mean((test2$preciom - pred2)^2))
MAE2  <- mean(abs(test2$preciom - pred2))

data.frame(
  Modelo = c("Casas Norte", "Aptos Sur"),
  RMSE   = c(RMSE1, RMSE2),
  MAE    = c(MAE1,  MAE2)
)
       Modelo      RMSE       MAE
1 Casas Norte 176.69646 110.36794
2   Aptos Sur  73.59518  52.10209
# Promedios y porcentajes
mean1   <- mean(test1$preciom)
MAPE1   <- mean(abs((test1$preciom - pred1) / test1$preciom)) * 100
pctMAE1 <- 100 * MAE1 / mean1

mean2   <- mean(test2$preciom)
MAPE2   <- mean(abs((test2$preciom - pred2) / test2$preciom)) * 100
pctMAE2 <- 100 * MAE2 / mean2

# Tabla de métricas (nombres sin %)
tabla_metricas <- data.frame(
  Modelo            = c("Casas Norte", "Aptos Sur"),
  RMSE              = c(RMSE1, RMSE2),
  MAE               = c(MAE1,  MAE2),
  Precio_medio_test = c(mean1, mean2),
  MAPE_pct          = c(MAPE1, MAPE2),
  MAE_sobre_media_pct = c(pctMAE1, pctMAE2)
)

# (opcional) redondear y mostrar bonito
library(dplyr); library(knitr); library(kableExtra)
tabla_metricas %>%
  mutate(across(where(is.numeric), ~round(., 2))) %>%
  kable("html", caption = "Rendimiento con set de prueba (80/20)") %>%
  kable_styling(full_width = FALSE, bootstrap_options = c("striped","hover"))
Rendimiento con set de prueba (80/20)
Modelo RMSE MAE Precio_medio_test MAPE_pct MAE_sobre_media_pct
Casas Norte 176.7 110.37 462.21 24.47 23.88
Aptos Sur 73.6 52.10 300.49 19.03 17.34

Para Casas en Zona Norte, el modelo tiene un RMSE de 176.7 millones y un MAE de unos 110.4 millones en el set de prueba. Eso indica que los errores son algo dispersos, lo cual era esperable por la alta variabilidad en los precios de casas en esa zona (hay mucha diferencia entre unas y otras).

En cambio, para Apartamentos en Zona Sur, el rendimiento es bastante mejor: RMSE = 73.6 M y MAE = 52.1 M, lo que muestra que el modelo logra predecir con más precisión. Esto tiene sentido ya que los apartamentos en esa zona tienden a tener precios más homogéneos.

Los resultados encajan bien con lo que vimos en los diagnósticos: linealidad razonable, algo de heterocedasticidad en los precios más altos, y colas pesadas en la distribución. Por eso, si se va a hacer inferencia, se recomienda usar errores estándar robustos. Aun con sus detalles, ambos modelos son útiles para priorizar y predecir dentro del rango observado.

ANALISIS GENERAL CASO

C&A recibió dos solicitudes de vivienda en Cali con perfiles y presupuestos específicos. Se analizó la base vivienda (paquete paqueteMODELOS) aplicando filtrado por zona Y tipo, limpieza, imputación razonada, EDA, regresión lineal múltiple y verificación de supuestos. Se generaron predicciones y se priorizaron 5 ofertas para cada caso con mapas interactivos.

Vivienda 1

Casa, Zona Norte, crédito ≤ 350 M

Perfil: 200 m², 1 parqueadero, 2 baños, 4 habitaciones, estrato 4–5.

Modelo (casas, Zona Norte): preciom - areaconst + estrato + banios + parqueaderos + habitaciones.

Ajuste: R² adj = 0.653. Predictores significativos: areaconst, estrato, banios, parqueaderos; habitaciones no aporta de forma estadística una vez se controlan las demás.

Predicción:

Estrato 4: 323.5 M (IC95% 12.4–634.6 M) dentro del presupuesto.

Estrato 5: 403.2 M (IC95% 91.5–714.9 M) supera el tope estimado.

Recomendación: concentrar la búsqueda en estrato 4. Se presentan 5 casas ≤ 350 millones que cumplen o superan el perfil, con mapa para priorizar visitas.

Vivienda 2

Apartamento, Zona Sur, crédito ≤ 850 M

Perfil: 300 m², 3 parqueaderos, 3 baños, 5 habitaciones, estrato 5–6.

Modelo (apartamentos, Zona Sur): misma fórmula.

Ajuste: (reporta el R² adj de tu summary(modelo2)).

Predicción:

Estrato 5: 698.1 M (IC95% 514.9–881.3 M) se encuentra en el en rango, con disponible.

Estrato 6: 755.6 M (IC95% 572.4–938.8 M) se encuentra en el rango sin embargo el extremo superior puede exceder 850 millones

Recomendación: priorizar estrato 5 y opciones de estrato 6 con margen de negociación. Se listan 5 apartamentos ≤ 850 millones con mapa.

Área construida y estrato son los principales impulsores del precio, baños y parqueaderos tienen efecto positivo moderado.

Se observan propiedades de alto valor (outliers altos) que reflejan la heterogeneidad real del mercado, se mantuvieron.

Conclusiónes: