Caso C&A

Asesoría para la compra de dos viviendas por parte de una compañía internacional que desea ubicar a dos de sus empleados con sus familias en la ciudad.

Las solicitudes incluyen las siguientes condiciones:

Características Vivienda 1 Vivienda 2
Tipo Casa Apartamento
área construida 200 300
parqueaderos 1 3
baños 2 3
habitaciones 4 5
estrato 4 o 5 5 o 6
zona Norte Sur
crédito preaprobado 350 millones 850 millones

Análisis Exploratorio de Datos

Previo a llevar a cabo las estimaciones, validaciones y comparación de modelos requeridos; es necesario realizar inicialmente un análisis exploratorio de los datos:

dir.create("figs", showWarnings = FALSE)
({
library(dplyr)
library(ggplot2)
library(knitr)
library(corrplot)
library(car)
library(ggfortify)
library(kableExtra)
library(patchwork)
library(rlang)
library(tidyverse)
library(FactoMineR)
library(factoextra)
library(cluster)
library(paqueteMODELOS)
library(naniar)
library(stringr)
library(tidymodels)
library(tidyr)
library(plotly)
library(Metrics)
library(stringr)
library(caret)
library(lmtest) 
  
})
##  [1] "lmtest"         "zoo"            "caret"          "lattice"       
##  [5] "Metrics"        "plotly"         "yardstick"      "workflowsets"  
##  [9] "workflows"      "tune"           "rsample"        "recipes"       
## [13] "parsnip"        "modeldata"      "infer"          "dials"         
## [17] "scales"         "tidymodels"     "naniar"         "paqueteMODELOS"
## [21] "summarytools"   "gridExtra"      "GGally"         "broom"         
## [25] "boot"           "cluster"        "factoextra"     "FactoMineR"    
## [29] "lubridate"      "forcats"        "stringr"        "purrr"         
## [33] "readr"          "tidyr"          "tibble"         "tidyverse"     
## [37] "rlang"          "patchwork"      "kableExtra"     "ggfortify"     
## [41] "car"            "carData"        "corrplot"       "knitr"         
## [45] "ggplot2"        "dplyr"          "stats"          "graphics"      
## [49] "grDevices"      "datasets"       "utils"          "methods"       
## [53] "base"

Cargue Base de Datos

Para empezar se carga la base de datos desde el paquete MODELOS, suministrada en el ejercicio:

devtools::install_github("centromagis/paqueteMODELOS", force =TRUE)
## 
## ── R CMD build ─────────────────────────────────────────────────────────────────
##       ✔  checking for file 'C:\Users\angel\AppData\Local\Temp\Rtmpy6kOID\remotes29cc66f93c9e\Centromagis-paqueteMODELOS-3b06257/DESCRIPTION' (352ms)
##       ─  preparing 'paqueteMODELOS':
##    checking DESCRIPTION meta-information ...  ✔  checking DESCRIPTION meta-information
##       ─  checking for LF line-endings in source and make files and shell scripts
##   ─  checking for empty or unneeded directories
##       ─  building 'paqueteMODELOS_0.1.0.tar.gz'
##      
## 
library(paqueteMODELOS)
data("vivienda")
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…
#Creo una copia de mi dataset original
vivienda_original <- vivienda

tipos <- data.frame(
  Variable = names(vivienda),
  Tipo = sapply(vivienda, class)
)

knitr::kable(tipos, caption = "Tabla de variables y sus tipos", align = "lc")
Tabla de variables y sus tipos
Variable Tipo
id id numeric
zona zona character
piso piso character
estrato estrato numeric
preciom preciom numeric
areaconst areaconst numeric
parqueaderos parqueaderos numeric
banios banios numeric
habitaciones habitaciones numeric
tipo tipo character
barrio barrio character
longitud longitud numeric
latitud latitud numeric

Efectivamente se puede observar que existen las variables indicadas en el ejericio, zona, piso, estrato, preciom, areaconst, parqueaderos, banios, habitaciones, tipo, barrio, longitud, latitud.

#Eliminar variable id
vivienda <- vivienda %>% select(-id)

#Convertir variable estrato a categorica
vivienda$estrato <- as.factor(vivienda$estrato)

tipos <- data.frame(
  Variable = names(vivienda),
  Tipo = sapply(vivienda, class)
)
knitr::kable(tipos, caption = "Tabla de variables y sus tipos", align = "lc")
Tabla de variables y sus tipos
Variable Tipo
zona zona character
piso piso character
estrato estrato factor
preciom preciom numeric
areaconst areaconst numeric
parqueaderos parqueaderos numeric
banios banios numeric
habitaciones habitaciones numeric
tipo tipo character
barrio barrio character
longitud longitud numeric
latitud latitud numeric

Análisis de Datos Faltantes

En este paso se detectan cuáles eran los registros que presentaban datos faltantes. Se genera una lista con los registros que contienen datos faltantes y es la que se muestra a continuación:

# Número de valores faltantes por variable
na_por_variable <- sapply(vivienda, function(x) sum(is.na(x)))
na_por_variable <- sort(na_por_variable, decreasing = TRUE)

# Convertir en tabla
na_tabla <- data.frame(Variable = names(na_por_variable), NA_Conteo = na_por_variable)

knitr::kable(na_tabla, caption = "Número de valores faltantes por variable")
Número de valores faltantes por variable
Variable NA_Conteo
piso piso 2638
parqueaderos parqueaderos 1605
zona zona 3
estrato estrato 3
areaconst areaconst 3
banios banios 3
habitaciones habitaciones 3
tipo tipo 3
barrio barrio 3
longitud longitud 3
latitud latitud 3
preciom preciom 2

Como se observa en la tabla anterior, las variables que presentan mayor cantidad de datos faltantes son las variables pisos y parqueaderos con 2638 y 1605 datos faltantes respectivamente.

Test de Hipótesis para Datos Faltantes

Posteriormente, se realiza la prueba estadística para evaluar si los datos faltantes existentes en nuestra base de datos, existen aleatoriamente y no obedecen a ninguna otra variable, observada o no observada.

mcar_test(vivienda)  

Como se puede observar el P valor es 0, es decir muy inferior a 0.05, por lo tanto se indica que se rechaza la hipótesis Nula, es decir que los datos faltantes no están completamente al azar, es decir que NO son datos MCAR, por lo tanto no se van a eliminar valores faltantes.

Imputación de datos faltantes

Teniendo en cuenta que el manejo de datos faltantes no se puede realizar eliminando valores, se reemplaza por el valor de la mediana en las variables numéricas y por la moda en las variables categóricas y posteriormente se verifica que ya no aparezcan datos faltantes, después de realizar este proceso.

# Función para calcular la moda
moda <- function(x) {
  ux <- na.omit(unique(x))
  ux[which.max(tabulate(match(x, ux)))]
}

# Reemplazo automático de NA por mediana o moda según el tipo de variable
for (col in names(vivienda)) {
  if (any(is.na(vivienda[[col]]))) {
    
    # Si es numérica → usar mediana
    if (is.numeric(vivienda[[col]])) {
      mediana <- median(vivienda[[col]], na.rm = TRUE)
      vivienda[[col]][is.na(vivienda[[col]])] <- mediana
    
    # Si es carácter o factor → usar moda
    } else if (is.character(vivienda[[col]]) || is.factor(vivienda[[col]])) {
      moda_val <- moda(vivienda[[col]])
      vivienda[[col]][is.na(vivienda[[col]])] <- moda_val
    }
  }
}
# Verificar que en mi dataset Vivienda no hay valores faltantes

sum(is.na(vivienda)) 
## [1] 0

Identificación y Eliminación de Duplicados

En este punto, se procede a identificar cuántos registros se encuentran duplicados en nuestra base de datos.

# Verificar filas duplicadas completas
duplicados <- vivienda[duplicated(vivienda), ]

# Mostrar cuántos hay
cat("Número de registros duplicados:", nrow(duplicados))
## Número de registros duplicados: 61
head(duplicados)

A partir del análisis realizado, se identifican los registros duplicados, por eso se procede a eliminarlos, y se verifica nuevamente que ya no hayan quedado registros duplicados.

# Eliminar filas duplicadas
vivienda <- vivienda %>% distinct()

#Verifico que ya no aparezcan duplicados
duplicados <- vivienda[duplicated(vivienda), ]
cat("Número de registros duplicados:", nrow(duplicados))
## Número de registros duplicados: 0
head(duplicados)

Estadística Descriptiva

Una vez eliminados los registros duplicados e imputados los datos faltantes, se procede a realizar la estadística descriptiva: Para las variables numéricas se determinan valores mínimos, máximos, media, mediana, primer y tercer cuartil y para las variables categóricas Tabla de frecuencias y porcentajes:

# Seleccionar variables numéricas
vivienda_num <- vivienda %>%
  select(where(is.numeric))

# Resumen general
summary(vivienda_num)
##     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   : 434.7   Mean   : 175.2   Mean   : 1.869   Mean   : 3.113  
##  3rd Qu.: 548.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  
##   habitaciones       longitud         latitud     
##  Min.   : 0.000   Min.   :-76.59   Min.   :3.333  
##  1st Qu.: 3.000   1st Qu.:-76.54   1st Qu.:3.381  
##  Median : 3.000   Median :-76.53   Median :3.416  
##  Mean   : 3.607   Mean   :-76.53   Mean   :3.418  
##  3rd Qu.: 4.000   3rd Qu.:-76.52   3rd Qu.:3.452  
##  Max.   :10.000   Max.   :-76.46   Max.   :3.498

A continuación se realiza la limpieza de la Variable Barrio, y se calculan las tablas de frecuencia para las variables categóricas:

library(stringr)
library(stringi)
#Limpieza variable Barrio

fix_text <- function(x){
  # 1) Normalizar codificación
  y <- stri_encode(x, from = "windows-1252", to = "UTF-8")
  idx_na <- is.na(y)
  if (any(idx_na)) y[idx_na] <- stri_encode(x[idx_na], from = "latin1", to = "UTF-8")

  # 2) Reemplazos puntuales 
  repl <- c(
    "Alameda Del Ra-O" = "Alameda Del Rio",
    "Alfa\\^Sa\\(C\\)Rez Real" = "Alferez Real",
   "Alto Jorda!N "= "Alto Jordan",
    "Alfonso Laaaaaaa³Pez" = "Alfonso Lopez",
    "Alfonso Laaaaaaa³Pez I" = "Alfonso Lopez",
    "Alfonso Laaaaaa³Pez" = "Alfonso Lopez",
    "Alfonso Laaaaaa³Pez I" = "Alfonso Lopez",
    "Antonio Naria\\+/-O" = "Antonio Narino",
    "Base Aa\\^Sa\\(C\\)Rea" = "Base Aerea",
    "Benjama-N Herrera" = "Benjamin Herrera",
    "Boyaca!" = "Boyaca",
    "Bretaa\\+/-A" = "Bretana",
    "Caa\\+/-" = "Canas",
    "Ciudad Caaaa³Rdoba" = "Ciudad Cordoba",
    "Ciudad Jarda-N" = "Ciudad Jardin",
    "Ciudad Los A!Lamos" = "Ciudad Los Alamos",
    "Ciudad Mela\\^Sa\\(C\\)Ndez" = "Ciudad Melendez",
    "Ciudadela Del Ra-O" = "Ciudadela Del Rio",
    "Cristaaaa³Bal Colaaaa³N" = "Cristobal Colon",
    "Cristobal Colaaaa³N" = "Cristobal Colon",
    "Cristaaaaa³Bal Colaaaaa³N" = "Cristobal Colon",
    "Cristobal Colaaaaa³N" = "Cristobal Colon",
    "El Tra\\^Sa\\(C\\)Bol" = "El Troncal",
    "Evaristo Garca-A" = "Evaristo Garcia",
    "JuanambaSa<< ?" = "Juanambu",
    "JuanambaSa<<" = "Juanambu",
    "Juna-N" = "Junin",
    "La Campia\\+/-A" = "La Campina",
    "Los Ca!Mbulos" = "Los Cambulos",
    "Marroqua-N Iii" = "Marroquin",
    "Mela\\^Sa\\(C\\)Ndez" = "Melendez",
    "Na!Poles" = "Napoles",
    "Pacara!" = "Pacara",
    "RepaSa<<Blica De Israel" = "Republica De Israel",
    "Rincaaaa³N De Salomia" = "Rincon De Salomia",
    "Rincaaaaa³N" = "Rincon",
    "San Joaqua-N" = "San Joaquin",
    "San Nicola!S" = "San Nicolas",
    "Santa Ba!Rbara" = "Santa Barbara",
    "Santa Maaaaaaa³Nica" = "Santa Monica",
    "Santa Maaaaaaa³Nica Alta" = "Santa Monica Alta",
    "Santa Maaaaaaa³Nica Popular" = "Santa Monica Popular",
    "Santa Maaaaaaa³Nica Residencial" = "Santa Monica Residencial",
    "Simaaaa³N Bolivar" = "Simon Bolivar",
    "Terraaaa³N Colorado" = "Terron Colorado",
    "Uniaaaa³N De Vivienda" = "Unidades De Vivienda",
   "RepaSa<<Blica De Israel" = "Republica de Israel",
    "Urbanizaciaaaa³N" = "Urbanizacion",
    "Urbanizaciaaaaa³N" = "Urbanizacion",
    "Urbanizaciaaaaaaa³N" = "Urbanizacion",
    "El Jarda-N" = "El Jordan",
    "El Jorda!N" = "El Jordan",
    "El Paraa-So" = "El Prado",
    "El Pea\\+/-On" = "El Penon",
    "Alto Jorda!N" = "Alto Jordan",
    "(?i)^Alto Jorda!N$" = "Alto Jordan",
    "(?i)^Base Aa\\^Sa\\(C\\)Rea$" = "Base Aerea",
    "Santa Manica" = "Santa Monica",
   "Ciudad Jarda-N" = "Ciudad Jardin",
   "Santa Manica Residencial" = "Santa Monica"
  )
  y <- str_replace_all(y, repl)

  # 3) Reglas genéricas útiles
  # Urbanizacia... -> Urbanizacion + resto del nombre
  y <- str_replace(y, "^Urbanizacia.*?\\s", "Urbanizacion ")
  # Variantes rotas de "Santa Monica"
  y <- str_replace_all(y, "Santa M[aA]+[^[:alpha:]]*nica", "Santa Monica")
  # Comprimir repeticiones absurdas: "Laaaaaaaa" -> "Laa"
  y <- str_replace_all(y, "([A-Za-z])\\1{2,}", "\\1\\1")
  # Quitar " Lopez I" final
  y <- str_replace_all(y, "L\\s*opez\\s*I\\b", "Lopez")

  # 4) Mojibake suelto y caracteres no ASCII
  #y <- str_replace_all(y, c("³" = "o", "Â" = ""))   # casos comunes
  y <- stri_trans_general(y, "Latin-ASCII")          # sin tildes
  y <- str_replace_all(y, "[^\\x00-\\x7F]", "")      # purga cualquier no-ASCII remanente

  # 5) Limpieza final
  y <- str_squish(y)
  y <- str_to_title(y)
  y
  
  # Homologar variantes de Alfonso Lopez (Laapez / Laapez I / Lopez I)
y <- str_replace_all(y, c(
  "(?i)^Alfonso\\s+La+pez(?:\\s+I)?$" = "Alfonso Lopez",
  "(?i)^Alfonso\\s+Lopez(?:\\s+I)?$" = "Alfonso Lopez"
))
  # Homologar variantes de Alameda Del Rio
y <- str_replace_all(y, c(
  "(?i)^Alameda\\s+Del\\s+Ra-?O$" = "Alameda Del Rio",
  "(?i)^Alameda\\s+Del\\s+Rio$"  = "Alameda Del Rio"
))

# Homologar variantes de Alferez Real
y <- str_replace_all(y, c(
  "(?i)^Alfa\\^Sa\\(C\\)Rez\\s+Real$" = "Alferez Real",
  "(?i)^Alferez\\s+Real$"            = "Alferez Real"
  
))
# Homologar Antonio Narino
y <- str_replace_all(y, c(
  "(?i)^Antonio\\s+Naria\\+/-O$" = "Antonio Narino",
  "(?i)^Antonio\\s+Nari[nñ]o$"  = "Antonio Narino"
))
# Homologar Las Americas
y <- str_replace_all(y, c(
  "(?i)^Las\\s+Ama\\^Sa\\(C\\)Ricas$" = "Las Americas",
  "(?i)^Las\\s+Americas$"             = "Las Americas"
))
}

# Usar:
vivienda$barrio <- fix_text(vivienda$barrio)
# Seleccionar solo variables categoricas
vivienda_cat <- vivienda %>%
  select(where(~is.character(.) || is.factor(.)))

# Función para tabla con frecuencia y porcentaje

tabla_frecuencias <- function(x) {
  tab <- as.data.frame(table(x, useNA = "ifany"))
  colnames(tab) <- c("Categoria", "Frecuencia")
  tab$Porcentaje <- round(100 * tab$Frecuencia / sum(tab$Frecuencia), 2)
  return(tab)
}

# Aplicar a todas las variables categoricas
resumen_categoricas <- lapply(names(vivienda_cat), function(var) {
  cat("### ", var, "\n\n")
  print(knitr::kable(tabla_frecuencias(vivienda_cat[[var]]),
                     caption = paste("Distribución de", var)))
  cat("\n\n")
})
## ###  zona 
## 
## 
## 
## Table: Distribución de zona
## 
## |Categoria    | Frecuencia| Porcentaje|
## |:------------|----------:|----------:|
## |Zona Centro  |        124|       1.50|
## |Zona Norte   |       1908|      23.10|
## |Zona Oeste   |       1195|      14.47|
## |Zona Oriente |        350|       4.24|
## |Zona Sur     |       4684|      56.70|
## 
## 
## ###  piso 
## 
## 
## 
## Table: Distribución de piso
## 
## |Categoria | Frecuencia| Porcentaje|
## |:---------|----------:|----------:|
## |01        |        852|      10.31|
## |02        |       4063|      49.18|
## |03        |       1089|      13.18|
## |04        |        603|       7.30|
## |05        |        561|       6.79|
## |06        |        245|       2.97|
## |07        |        203|       2.46|
## |08        |        207|       2.51|
## |09        |        144|       1.74|
## |10        |        129|       1.56|
## |11        |         82|       0.99|
## |12        |         83|       1.00|
## 
## 
## ###  estrato 
## 
## 
## 
## Table: Distribución de estrato
## 
## |Categoria | Frecuencia| Porcentaje|
## |:---------|----------:|----------:|
## |3         |       1445|      17.49|
## |4         |       2105|      25.48|
## |5         |       2728|      33.02|
## |6         |       1983|      24.00|
## 
## 
## ###  tipo 
## 
## 
## 
## Table: Distribución de tipo
## 
## |Categoria   | Frecuencia| Porcentaje|
## |:-----------|----------:|----------:|
## |Apartamento |       5061|      61.26|
## |Casa        |       3200|      38.74|
## 
## 
## ###  barrio 
## 
## 
## 
## Table: Distribución de barrio
## 
## |Categoria                        | Frecuencia| Porcentaje|
## |:--------------------------------|----------:|----------:|
## |20 De Julio                      |          3|       0.04|
## |3 De Julio                       |          1|       0.01|
## |Acopi                            |        157|       1.90|
## |Agua Blanca                      |          1|       0.01|
## |Aguablanca                       |          2|       0.02|
## |Aguacatal                        |        109|       1.32|
## |Alameda                          |         16|       0.19|
## |Alameda Del Rio                  |          3|       0.04|
## |Alamos                           |         14|       0.17|
## |Alborada                         |          1|       0.01|
## |Alcazares                        |          2|       0.02|
## |Alferez Real                     |          7|       0.08|
## |Alfonso Lopez                    |         23|       0.28|
## |Alto Jorda!N                     |          1|       0.01|
## |Altos De Guadalupe               |          4|       0.05|
## |Altos De Menga                   |          3|       0.04|
## |Altos De Santa                   |          1|       0.01|
## |Antonio Narino                   |          2|       0.02|
## |Aranjuez                         |         15|       0.18|
## |Arboleda                         |          5|       0.06|
## |Arboleda Campestre Candelaria    |          1|       0.01|
## |Arboledas                        |         38|       0.46|
## |Atanasio Girardot                |          9|       0.11|
## |Autopista Sur                    |          1|       0.01|
## |Bajo Aguacatal                   |          1|       0.01|
## |Barranquilla                     |          6|       0.07|
## |Barrio 7de Agosto                |          1|       0.01|
## |Barrio El Recuerdo               |          1|       0.01|
## |Barrio Eucara-Stico              |          1|       0.01|
## |Barrio Obrero                    |          1|       0.01|
## |Barrio Tranquilo Y               |          1|       0.01|
## |Base Aa^Sa(C)Rea                 |          2|       0.02|
## |Belalcazar                       |          4|       0.05|
## |Belisario Caicedo                |          2|       0.02|
## |Bella Suiza                      |         18|       0.22|
## |Bella Suiza Alta                 |          4|       0.05|
## |Bellavista                       |         43|       0.52|
## |Benjama-N Herrera                |          8|       0.10|
## |Berlin                           |          1|       0.01|
## |Bloques Del Limonar              |          1|       0.01|
## |Bochalema                        |         33|       0.40|
## |Bolivariano                      |          1|       0.01|
## |Bosques De Alboleda              |          1|       0.01|
## |Bosques Del Limonar              |         20|       0.24|
## |Boyaca!                          |          1|       0.01|
## |Bretaa+/-A                       |         16|       0.19|
## |Brisas De Guadalupe              |          1|       0.01|
## |Brisas De Los                    |         81|       0.98|
## |Brisas Del Guabito               |          1|       0.01|
## |Brisas Del Limonar               |          1|       0.01|
## |Bueno Madrid                     |          1|       0.01|
## |Buenos Aires                     |          7|       0.08|
## |Caa+/-Asgordas                   |          7|       0.08|
## |Caa+/-Averalejo                  |         12|       0.15|
## |Caa+/-Averales                   |         21|       0.25|
## |Caa+/-Averales Los Samanes       |          1|       0.01|
## |Caldas                           |          1|       0.01|
## |Cali                             |         37|       0.45|
## |Cali Bella                       |          1|       0.01|
## |Cali Canto                       |          1|       0.01|
## |Calibella                        |          1|       0.01|
## |Calicanto                        |          8|       0.10|
## |Calicanto Vii                    |          1|       0.01|
## |Calima                           |          6|       0.07|
## |Calimio Norte                    |          5|       0.06|
## |Calipso                          |         11|       0.13|
## |Cambulos                         |          3|       0.04|
## |Camino Real                      |         36|       0.44|
## |Campestre                        |          1|       0.01|
## |Caney                            |         88|       1.07|
## |Caney Especial                   |          5|       0.06|
## |Capri                            |         56|       0.68|
## |Cascajal                         |          1|       0.01|
## |Cataya Real                      |          1|       0.01|
## |Ceibas                           |          1|       0.01|
## |Centelsa                         |          1|       0.01|
## |Centenario                       |         16|       0.19|
## |Centro                           |          4|       0.05|
## |Cerro Cristales                  |         22|       0.27|
## |Cerros De Guadalupe              |          1|       0.01|
## |Champagnat                       |         14|       0.17|
## |Chapinero                        |          7|       0.08|
## |Chiminangos                      |         17|       0.21|
## |Chiminangos 1 Etapa              |          1|       0.01|
## |Chiminangos 2 Etapa              |          2|       0.02|
## |Chipichape                       |         30|       0.36|
## |Ciudad 2000                      |         94|       1.14|
## |Ciudad Antejardin                |          1|       0.01|
## |Ciudad Bochalema                 |         47|       0.57|
## |Ciudad Capri                     |         13|       0.16|
## |Ciudad Cardoba                   |         15|       0.18|
## |Ciudad Cardoba Reservado         |          1|       0.01|
## |Ciudad Cordoba                   |         20|       0.24|
## |Ciudad Country                   |          1|       0.01|
## |Ciudad Del Campo                 |          1|       0.01|
## |Ciudad Jarda-N                   |        514|       6.22|
## |Ciudad Jardin                    |         22|       0.27|
## |Ciudad Jardin Pance              |          1|       0.01|
## |Ciudad Los A!Lamos               |         25|       0.30|
## |Ciudad Los Alamos                |          1|       0.01|
## |Ciudad Mela^Sa(C)Ndez            |          1|       0.01|
## |Ciudad Melendez                  |          1|       0.01|
## |Ciudad Modelo                    |          7|       0.08|
## |Ciudad Pacifica                  |          3|       0.04|
## |Ciudad Real                      |          3|       0.04|
## |Ciudad Talanga                   |          1|       0.01|
## |Ciudad Universitaria             |          1|       0.01|
## |Ciudadela Comfandi               |         17|       0.21|
## |Ciudadela Del Ra-O               |          1|       0.01|
## |Ciudadela Melendez               |          1|       0.01|
## |Ciudadela Paso Ancho             |          1|       0.01|
## |Ciudadela Pasoancho              |         21|       0.25|
## |Colinas De Menga                 |          3|       0.04|
## |Colinas Del Bosque               |          1|       0.01|
## |Colinas Del Sur                  |          7|       0.08|
## |Colon                            |          1|       0.01|
## |Colseguros                       |         44|       0.53|
## |Colseguros Andes                 |          5|       0.06|
## |Comfenalco                       |          1|       0.01|
## |Compartir                        |          1|       0.01|
## |Conjunto Gibraltar               |          1|       0.01|
## |Cristabal Colan                  |          2|       0.02|
## |Cristales                        |         83|       1.00|
## |Cristobal Colan                  |         14|       0.17|
## |Cuarto De Legua                  |         43|       0.52|
## |Departamental                    |         29|       0.35|
## |Ed Benjamin Herrera              |          1|       0.01|
## |El Bosque                        |         50|       0.61|
## |El Caney                         |        205|       2.48|
## |El Castillo                      |          5|       0.06|
## |El Cedro                         |          8|       0.10|
## |El Diamante                      |          2|       0.02|
## |El Dorado                        |          6|       0.07|
## |El Gran Limonar                  |          8|       0.10|
## |El Guabal                        |         19|       0.23|
## |El Guabito                       |          1|       0.01|
## |El Ingenio                       |        203|       2.46|
## |El Ingenio 3                     |          1|       0.01|
## |El Ingenio I                     |         19|       0.23|
## |El Ingenio Ii                    |         40|       0.48|
## |El Jarda-N                       |         15|       0.18|
## |El Jorda!N                       |          1|       0.01|
## |El Lido                          |         58|       0.70|
## |El Limonar                       |        133|       1.61|
## |El Nacional                      |          1|       0.01|
## |El Paraa-So                      |          3|       0.04|
## |El Pea+/-On                      |         59|       0.71|
## |El Prado                         |          2|       0.02|
## |El Refugio                       |        119|       1.44|
## |El Rodeo                         |          1|       0.01|
## |El Sena                          |          1|       0.01|
## |El Tra^Sa(C)Bol                  |          5|       0.06|
## |El Troncal                       |         19|       0.23|
## |El Vallado                       |          1|       0.01|
## |Eucara-Stico                     |          2|       0.02|
## |Evaristo Garca-A                 |          2|       0.02|
## |Farrallones De Pance             |          1|       0.01|
## |Fenalco Kennedy                  |          1|       0.01|
## |Fepicol                          |          1|       0.01|
## |Flora                            |          1|       0.01|
## |Flora Industrial                 |         16|       0.19|
## |Floralia                         |          6|       0.07|
## |Fonaviemcali                     |          1|       0.01|
## |Francisco Eladio Ramirez         |          1|       0.01|
## |Fuentes De La                    |          1|       0.01|
## |Gaitan                           |          1|       0.01|
## |Gran Limonar                     |         23|       0.28|
## |Granada                          |         15|       0.18|
## |Guadalupe                        |         21|       0.25|
## |Guadalupe Alto                   |          1|       0.01|
## |Guaduales                        |          2|       0.02|
## |Guayaquil                        |         16|       0.19|
## |Hacienda Alferez Real            |          1|       0.01|
## |Ingenio                          |          1|       0.01|
## |Ingenio I                        |          1|       0.01|
## |Ingenio Ii                       |          1|       0.01|
## |Jamundi                          |          4|       0.05|
## |Jamundi Alfaguara                |          1|       0.01|
## |Jorge Eliecer Gaita!N            |          1|       0.01|
## |Jorge Isaacs                     |          1|       0.01|
## |Jose Manuel Marroqua-N           |          1|       0.01|
## |Juanamba^Sa^<<                   |         53|       0.64|
## |Juanambu                         |          2|       0.02|
## |Juna-N                           |          6|       0.07|
## |Junin                            |         18|       0.22|
## |La Alborada                      |          5|       0.06|
## |La Alianza                       |          4|       0.05|
## |La Arboleda                      |         18|       0.22|
## |La Base                          |         15|       0.18|
## |La Buitrera                      |          3|       0.04|
## |La Campia+/-A                    |         13|       0.16|
## |La Cascada                       |          7|       0.08|
## |La Ceibas                        |          1|       0.01|
## |La Esmeralda                     |          1|       0.01|
## |La Flora                         |        363|       4.39|
## |La Floresta                      |         17|       0.21|
## |La Fortaleza                     |          4|       0.05|
## |La Gran Colombia                 |          1|       0.01|
## |La Hacienda                      |        165|       2.00|
## |La Independencia                 |         12|       0.15|
## |La Libertad                      |          2|       0.02|
## |La Luisa                         |          1|       0.01|
## |La Merced                        |         26|       0.31|
## |La Morada                        |          1|       0.01|
## |La Nueva Base                    |          8|       0.10|
## |La Playa                         |          1|       0.01|
## |La Portada Al                    |          1|       0.01|
## |La Primavera                     |          1|       0.01|
## |La Reforma                       |          1|       0.01|
## |La Rivera                        |         11|       0.13|
## |La Rivera I                      |          2|       0.02|
## |La Rivera Ii                     |          2|       0.02|
## |La Riverita                      |          1|       0.01|
## |La Riviera                       |          1|       0.01|
## |La Selva                         |         11|       0.13|
## |La Villa Del                     |          1|       0.01|
## |Laflora                          |          1|       0.01|
## |Lares De Comfenalco              |          1|       0.01|
## |Las Acacias                      |         12|       0.15|
## |Las Americas                     |          3|       0.04|
## |Las Camelias                     |          1|       0.01|
## |Las Ceibas                       |         23|       0.28|
## |Las Delicias                     |          5|       0.06|
## |Las Granjas                      |         10|       0.12|
## |Las Quintas De                   |          1|       0.01|
## |Las Vegas                        |          1|       0.01|
## |Las Vegas De                     |          1|       0.01|
## |Libertadores                     |          3|       0.04|
## |Los Alamos                       |          1|       0.01|
## |Los Alca!Zares                   |          5|       0.06|
## |Los Alcazares                    |         17|       0.21|
## |Los Andes                        |         21|       0.25|
## |Los Ca!Mbulos                    |          6|       0.07|
## |Los Cambulos                     |         25|       0.30|
## |Los Cristales                    |        154|       1.86|
## |Los Cristales Club               |          1|       0.01|
## |Los Farallones                   |          4|       0.05|
## |Los Guaduales                    |         26|       0.31|
## |Los Guayacanes                   |          3|       0.04|
## |Los Jockeys                      |          1|       0.01|
## |Los Libertadores                 |          4|       0.05|
## |Los Parques Barranquilla         |          6|       0.07|
## |Los Robles                       |          1|       0.01|
## |Lourdes                          |          2|       0.02|
## |Mamellan                         |          1|       0.01|
## |Manzanares                       |          5|       0.06|
## |Mariano Ramos                    |          1|       0.01|
## |Marroqua-N Ii                    |          1|       0.01|
## |Mayapan Las Vegas                |         46|       0.56|
## |Mela^Sa(C)Ndez                   |         22|       0.27|
## |Melendez                         |         52|       0.63|
## |Menga                            |         23|       0.28|
## |Metropolitano Del Norte          |         21|       0.25|
## |Miradol Del Aguacatal            |          1|       0.01|
## |Miraflores                       |         26|       0.31|
## |Morichal De Comfandi             |          3|       0.04|
## |Multicentro                      |         27|       0.33|
## |Municipal                        |          3|       0.04|
## |Na!Poles                         |         29|       0.35|
## |Napoles                          |          2|       0.02|
## |Normanda-A                       |        153|       1.85|
## |Normanda-A West Point            |          1|       0.01|
## |Normandia                        |          5|       0.06|
## |Norte                            |          9|       0.11|
## |Norte La Flora                   |          1|       0.01|
## |Nueva Base                       |          1|       0.01|
## |Nueva Floresta                   |         15|       0.18|
## |Nueva Tequendama                 |         72|       0.87|
## |Oasis De Comfandi                |          6|       0.07|
## |Oasis De Pasoancho               |          1|       0.01|
## |Occidente                        |         11|       0.13|
## |Pacara                           |         19|       0.23|
## |Pacara!                          |          4|       0.05|
## |Palmas Del Ingenio               |          1|       0.01|
## |Pampa Linda                      |         26|       0.31|
## |Pampalinda                       |         12|       0.15|
## |Panamericano                     |          9|       0.11|
## |Pance                            |        410|       4.96|
## |Parcelaciones Pance              |         61|       0.74|
## |Parque Residencial El            |          1|       0.01|
## |Paseo De Los                     |          2|       0.02|
## |Paso Del Comercio                |          6|       0.07|
## |Pasoancho                        |          6|       0.07|
## |Poblado Campestre                |          2|       0.02|
## |Ponce                            |          1|       0.01|
## |Popular                          |          6|       0.07|
## |Portada De Comfandi              |          2|       0.02|
## |Portales De Comfandi             |          1|       0.01|
## |Porvenir                         |          3|       0.04|
## |Prados De Oriente                |          6|       0.07|
## |Prados Del Limonar               |         21|       0.25|
## |Prados Del Norte                 |        126|       1.53|
## |Prados Del Sur                   |          2|       0.02|
## |Primavera                        |          2|       0.02|
## |Primero De Mayo                  |         37|       0.45|
## |Primitivo Crespo                 |          3|       0.04|
## |Puente Del Comercio              |          6|       0.07|
## |Puente Palma                     |          1|       0.01|
## |Quintas De Don                   |         72|       0.87|
## |Quintas De Salomia               |          4|       0.05|
## |Rafael Uribe Uribe               |          1|       0.01|
## |Refugio                          |          2|       0.02|
## |Repa^Sa^<<Blica De Israel        |          1|       0.01|
## |Rincan De Salomia                |          1|       0.01|
## |Rincon De La                     |          1|       0.01|
## |Riveras Del Valle                |          1|       0.01|
## |Rozo La Torre                    |          1|       0.01|
## |Saavedra Galindo                 |          4|       0.05|
## |Salomia                          |         39|       0.47|
## |Samanes                          |          1|       0.01|
## |Samanes De Guadalupe             |          1|       0.01|
## |Sameco                           |          1|       0.01|
## |San Antonio                      |         24|       0.29|
## |San Bosco                        |          8|       0.10|
## |San Carlos                       |          4|       0.05|
## |San Cayetano                     |          9|       0.11|
## |San Fernando                     |         55|       0.67|
## |San Fernando Nuevo               |         10|       0.12|
## |San Fernando Viejo               |         18|       0.22|
## |San Joaqua-N                     |         16|       0.19|
## |San Joaquin                      |          4|       0.05|
## |San Juan Bosco                   |          7|       0.08|
## |San Judas                        |          1|       0.01|
## |San Judas Tadeo                  |          2|       0.02|
## |San Lua-S                        |          1|       0.01|
## |San Luis                         |          2|       0.02|
## |San Nicola!S                     |          1|       0.01|
## |San Nicolas                      |          1|       0.01|
## |San Pedro                        |          3|       0.04|
## |San Vicente                      |         47|       0.57|
## |Santa                            |          1|       0.01|
## |Santa Anita                      |         50|       0.61|
## |Santa Anita Sur                  |          1|       0.01|
## |Santa Ba!Rbara                   |          3|       0.04|
## |Santa Elena                      |         10|       0.12|
## |Santa Fe                         |          8|       0.10|
## |Santa Helena De                  |          1|       0.01|
## |Santa Isabel                     |         63|       0.76|
## |Santa Manica                     |          3|       0.04|
## |Santa Manica Alta                |          1|       0.01|
## |Santa Manica Popular             |          7|       0.08|
## |Santa Manica Residencial         |         39|       0.47|
## |Santa Monica                     |         52|       0.63|
## |Santa Monica Norte               |          2|       0.02|
## |Santa Monica Popular             |          2|       0.02|
## |Santa Monica Residencial         |          5|       0.06|
## |Santa Rita                       |         45|       0.54|
## |Santa Rosa                       |          1|       0.01|
## |Santa Teresita                   |        263|       3.18|
## |Santafe                          |          1|       0.01|
## |Santander                        |          1|       0.01|
## |Santo Domingo                    |          6|       0.07|
## |Sector Aguacatal                 |          1|       0.01|
## |Sector Caa+/-Averalejo Guadalupe |          2|       0.02|
## |Seminario                        |         32|       0.39|
## |Sierras De Normanda-A            |          1|       0.01|
## |Siete De Agosto                  |          8|       0.10|
## |Siman Bolivar                    |          1|       0.01|
## |Tejares Cristales                |          4|       0.05|
## |Tejares De San                   |         14|       0.17|
## |Templete                         |          4|       0.05|
## |Tequendama                       |         43|       0.52|
## |Tequendema                       |          1|       0.01|
## |Terran Colorado                  |          1|       0.01|
## |Torres De Comfandi               |         57|       0.69|
## |Unian De Vivienda                |          3|       0.04|
## |Unicentro Cali                   |          1|       0.01|
## |Urbanizacian Barranquilla        |          4|       0.05|
## |Urbanizacian Boyaca!             |          1|       0.01|
## |Urbanizacian Colseguros          |          3|       0.04|
## |Urbanizacian La Flora            |         83|       1.00|
## |Urbanizacian La Merced           |          4|       0.05|
## |Urbanizacian La Nueva            |          4|       0.05|
## |Urbanizacian Las Cascadas        |          1|       0.01|
## |Urbanizacian Nueva Granada       |          3|       0.04|
## |Urbanizacian Pacara              |          1|       0.01|
## |Urbanizacian Ra-O Lili           |          5|       0.06|
## |Urbanizacian San Joaquin         |          4|       0.05|
## |Urbanizacian Tequendama          |          7|       0.08|
## |Urbanizacion El Saman            |          1|       0.01|
## |Urbanizacion Gratamira           |          1|       0.01|
## |Urbanizacion Lili                |          2|       0.02|
## |Valle De Lili                    |          1|       0.01|
## |Valle Del Lili                   |        995|      12.04|
## |Valle Grande                     |          1|       0.01|
## |Versalles                        |         71|       0.86|
## |Villa Colombia                   |          6|       0.07|
## |Villa De Veracruz                |          6|       0.07|
## |Villa Del Lago                   |         10|       0.12|
## |Villa Del Parque                 |          1|       0.01|
## |Villa Del Prado                  |         52|       0.63|
## |Villa Del Sol                    |         25|       0.30|
## |Villa Del Sur                    |          5|       0.06|
## |Villas De Veracruz               |          9|       0.11|
## |Vipasa                           |         32|       0.39|
## |Zona Centro                      |          1|       0.01|
## |Zona Norte                       |         32|       0.39|
## |Zona Norte Los                   |          1|       0.01|
## |Zona Oeste                       |         26|       0.31|
## |Zona Oriente                     |         18|       0.22|
## |Zona Residencial                 |          1|       0.01|
## |Zona Sur                         |         74|       0.90|

Detección de Valores atípicos e inconsistencias

En esta sección se identifican valores atípicos e inconsistencias para cada variable.

# Seleccionar variables numéricas
vivienda_num <- vivienda %>% select(where(is.numeric))

# Función para contar outliers por IQR
contar_outliers <- function(x) {
  Q1 <- quantile(x, 0.25, na.rm = TRUE)
  Q3 <- quantile(x, 0.75, na.rm = TRUE)
  IQR <- Q3 - Q1
  sum(x < (Q1 - 1.5 * IQR) | x > (Q3 + 1.5 * IQR), na.rm = TRUE)
}

# Aplicar a todas las variables
outliers_por_variable <- sapply(vivienda_num, contar_outliers)

# Convertir en tabla
tabla_outliers <- data.frame(
  Variable = names(outliers_por_variable),
  N_Outliers = outliers_por_variable
)

knitr::kable(tabla_outliers, caption = "Número de valores atípicos por variable numérica")
Número de valores atípicos por variable numérica
Variable N_Outliers
preciom preciom 552
areaconst areaconst 381
parqueaderos parqueaderos 565
banios banios 72
habitaciones habitaciones 884
longitud longitud 130
latitud latitud 0
for (var in names(vivienda_num)) {
  print(
    ggplot(vivienda, aes_string(y = var)) +
      geom_boxplot(fill = "#fdbb84", color = "#e34a33", outlier.color = "black") +
      theme_minimal() +
      labs(title = paste("Boxplot -", var), y = var)
  )
}

Como se puede observar, de la base original, solo la variable latitud no presentaba valores faltantes.

Debido a la fuerte asimetría y heterocedasticidad en preciom, se realiza transformación logaritmica. Los predictores se mantienen en su escala natural para facilitar la interpretación económica de los coeficientes.

Transformación logaritmica preciom

# Añadir la variable objetivo transformada y mantener el dataset igual
vivienda <- vivienda %>%
  dplyr::mutate(log_preciom = log1p(preciom))

# (opcional) Verificar
summary(dplyr::select(vivienda, preciom, log_preciom))
##     preciom        log_preciom   
##  Min.   :  58.0   Min.   :4.078  
##  1st Qu.: 220.0   1st Qu.:5.398  
##  Median : 330.0   Median :5.802  
##  Mean   : 434.7   Mean   :5.845  
##  3rd Qu.: 548.0   3rd Qu.:6.308  
##  Max.   :1999.0   Max.   :7.601

Punto 1. Filtro base y verificación (CASA – Zona Norte)

Diagnóstico de Valores

Para realizar este análisis se inicia realizando un diagnóstico de los valores ‘tipo’ y ‘zona’, para evidenciar si efectivamente hay valores en esas categorías, y para dar una idea de que tantos valores pueden haber, también ayuda a visualizar si la base de datos no se encuentra con valores normalizados de estas categorías de texto:

# --- Diagnóstico de los valores reales en 'tipo' y 'zona'
suppressPackageStartupMessages({ library(dplyr); library(stringr); library(knitr) })

# Conteos globales
tab_tipo  <- vivienda %>% count(tipo,  sort = TRUE)
tab_zona  <- vivienda %>% count(zona,  sort = TRUE)
tab_cruce <- vivienda %>% count(tipo, zona, sort = TRUE)

knitr::kable(tab_tipo,  caption = "Distribución global por 'tipo'")
Distribución global por ‘tipo’
tipo n
Apartamento 5061
Casa 3200
knitr::kable(tab_zona,  caption = "Distribución global por 'zona'")
Distribución global por ‘zona’
zona n
Zona Sur 4684
Zona Norte 1908
Zona Oeste 1195
Zona Oriente 350
Zona Centro 124
knitr::kable(tab_cruce, caption = "Cruce global 'tipo' x 'zona' (Top)")
Cruce global ‘tipo’ x ‘zona’ (Top)
tipo zona n
Apartamento Zona Sur 2759
Casa Zona Sur 1925
Apartamento Zona Norte 1191
Apartamento Zona Oeste 1026
Casa Zona Norte 717
Casa Zona Oriente 289
Casa Zona Oeste 169
Casa Zona Centro 100
Apartamento Zona Oriente 61
Apartamento Zona Centro 24

Validación Espacial

Ahora se procede a realizar una validación espacial,de la información consultada

# Normaliza etiquetas mínimamente
viv_sp <- vivienda %>%
  mutate(zona = str_squish(str_to_title(as.character(zona))))

# Resumen lat y lon por zona
latlon_por_zona <- viv_sp %>%
  group_by(zona) %>%
  summarise(
    n = n(),
    lat_mediana = median(latitud, na.rm = TRUE),
    lat_p25     = quantile(latitud, 0.25, na.rm = TRUE),
    lat_p75     = quantile(latitud, 0.75, na.rm = TRUE),
    lon_mediana = median(longitud, na.rm = TRUE),
    lon_p25     = quantile(longitud, 0.25, na.rm = TRUE),
    lon_p75     = quantile(longitud, 0.75, na.rm = TRUE),
    .groups = "drop"
  ) %>%
  arrange(desc(lat_mediana))  # para ver claramente Norte–Sur

knitr::kable(latlon_por_zona, caption = "Resumen espacial por zona: latitudes y longitudes (toda la base)")
Resumen espacial por zona: latitudes y longitudes (toda la base)
zona n lat_mediana lat_p25 lat_p75 lon_mediana lon_p25 lon_p75
Zona Norte 1908 3.472310 3.457000 3.485000 -76.52003 -76.52900 -76.50372
Zona Oeste 1195 3.449000 3.437720 3.452000 -76.54857 -76.55239 -76.54196
Zona Centro 124 3.439935 3.435992 3.446000 -76.52868 -76.53314 -76.52291
Zona Oriente 350 3.437975 3.422718 3.449642 -76.50699 -76.51600 -76.49555
Zona Sur 4684 3.384670 3.370000 3.408480 -76.53104 -76.54029 -76.52100
# Cortes robustos tomados de los cuartiles de TODA la ciudad
lat_cut_ns <- with(viv_sp, {
  p25_norte <- quantile(latitud[grepl("Norte", zona)], 0.25, na.rm = TRUE)
  p75_sur   <- quantile(latitud[grepl("Sur",   zona)], 0.75, na.rm = TRUE)
  mean(c(p25_norte, p75_sur), na.rm = TRUE)  # punto medio entre ambos
})

lon_cut_ew <- with(viv_sp, {
  p75_oeste <- quantile(longitud[grepl("Oeste", zona)], 0.75, na.rm = TRUE)
  p25_orien <- quantile(longitud[grepl("Oriente", zona)], 0.25, na.rm = TRUE)
  mean(c(p75_oeste, p25_orien), na.rm = TRUE)
})

# Clasificaciones geográficas binarias
viv_geo <- viv_sp %>%
  mutate(
    ns_geo   = if_else(latitud >= lat_cut_ns, "Zona Norte", "Zona Sur", missing = NA_character_),
    ew_geo   = if_else(longitud >= lon_cut_ew, "Zona Oriente", "Zona Oeste", missing = NA_character_),
    ns_etq   = case_when(grepl("Norte", zona) ~ "Zona Norte",
                         grepl("Sur",   zona) ~ "Zona Sur",
                         TRUE ~ "Otra"),
    ew_etq   = case_when(grepl("Oriente", zona) ~ "Zona Oriente",
                         grepl("Oeste",   zona) ~ "Zona Oeste",
                         TRUE ~ "Otra")
  )

# Matrices de confusión (ignorando "Otra")
ns_tab <- viv_geo %>% filter(ns_etq != "Otra", !is.na(ns_geo)) %>%
  count(ns_etq, ns_geo) %>% tidyr::pivot_wider(names_from = ns_geo, values_from = n, values_fill = 0)
ew_tab <- viv_geo %>% filter(ew_etq != "Otra", !is.na(ew_geo)) %>%
  count(ew_etq, ew_geo) %>% tidyr::pivot_wider(names_from = ew_geo, values_from = n, values_fill = 0)

knitr::kable(ns_tab, caption = "Matriz de confusión Norte/Sur (etiqueta vs. latitud)")
Matriz de confusión Norte/Sur (etiqueta vs. latitud)
ns_etq Zona Norte Zona Sur
Zona Norte 1659 249
Zona Sur 460 4224
knitr::kable(ew_tab, caption = "Matriz de confusión Oeste/Oriente (etiqueta vs. longitud)")
Matriz de confusión Oeste/Oriente (etiqueta vs. longitud)
ew_etq Zona Oeste Zona Oriente
Zona Oeste 1127 68
Zona Oriente 33 317
# Métricas simples de acierto
acc_ns <- viv_geo %>% filter(ns_etq != "Otra", !is.na(ns_geo)) %>%
  summarise(acc = mean(ns_etq == ns_geo)) %>% pull(acc)
acc_ew <- viv_geo %>% filter(ew_etq != "Otra", !is.na(ew_geo)) %>%
  summarise(acc = mean(ew_etq == ew_geo)) %>% pull(acc)

cat(sprintf("Exactitud Norte/Sur (solo etiquetas Norte/Sur): %.1f%%\n", 100*acc_ns))
## Exactitud Norte/Sur (solo etiquetas Norte/Sur): 89.2%
cat(sprintf("Exactitud Oeste/Oriente (solo etiquetas Oeste/Oriente): %.1f%%\n", 100*acc_ew))
## Exactitud Oeste/Oriente (solo etiquetas Oeste/Oriente): 93.5%
# Normaliza etiquetas mínimamente
# Centroides (medianas) por zona
centroides <- viv_sp %>%
  filter(!is.na(latitud), !is.na(longitud)) %>%
  group_by(zona) %>%
  summarise(lat0 = median(latitud), lon0 = median(longitud), .groups = "drop")

# Asignación por "zona más cercana" en 2D (lat, lon)
viv_idx <- viv_sp %>%
  mutate(.row_id = row_number()) %>%
  filter(!is.na(latitud), !is.na(longitud)) %>%
  select(.row_id, latitud, longitud, zona_etq = zona)

zona_geo_nn <- viv_idx %>%
  tidyr::crossing(centroides) %>%
  mutate(dist2 = (latitud - lat0)^2 + (longitud - lon0)^2) %>%
  group_by(.row_id) %>%
  slice_min(order_by = dist2, n = 1, with_ties = FALSE) %>%
  ungroup() %>%
  select(.row_id, zona_geo = zona)

# Unimos y armamos la matriz de confusión 5x5
viv_cmp <- viv_sp %>%
  mutate(.row_id = row_number()) %>%
  left_join(zona_geo_nn, by = ".row_id")

mat_conf_5 <- viv_cmp %>%
  filter(!is.na(zona_geo)) %>%
  count(zona_etq = zona, zona_geo) %>%
  tidyr::pivot_wider(names_from = zona_geo, values_from = n, values_fill = 0)

knitr::kable(mat_conf_5, caption = "Matriz de confusión (5 clases): etiqueta vs. zona_geo por centroides")
Matriz de confusión (5 clases): etiqueta vs. zona_geo por centroides
zona_etq Zona Centro Zona Norte Zona Oeste Zona Oriente Zona Sur
Zona Centro 97 6 9 11 1
Zona Norte 158 1401 122 69 158
Zona Oeste 107 40 987 19 42
Zona Oriente 37 15 8 243 47
Zona Sur 638 86 168 160 3632
# Exactitud global (5 clases)
acc_5 <- viv_cmp %>% filter(!is.na(zona_geo)) %>%
  summarise(acc = mean(zona == zona_geo)) %>% pull(acc)
cat(sprintf("Exactitud total (5 clases por centroides): %.1f%%\n", 100*acc_5))
## Exactitud total (5 clases por centroides): 77.0%
# Casos que "no coinciden" 
muestras_desac <- viv_cmp %>% filter(!is.na(zona_geo), zona != zona_geo) %>%
  select(barrio, zona, zona_geo, latitud, longitud, preciom) %>% head(15)
knitr::kable(muestras_desac, caption = "Ejemplos de discrepancias etiqueta vs. zona_geo (Top 15)")
Ejemplos de discrepancias etiqueta vs. zona_geo (Top 15)
barrio zona zona_geo latitud longitud preciom
3 De Julio Zona Sur Zona Centro 3.43500 -76.54000 400
Acopi Zona Norte Zona Sur 3.36971 -76.51700 240
Acopi Zona Norte Zona Centro 3.42627 -76.51974 220
Acopi Zona Norte Zona Sur 3.38296 -76.53105 310
Acopi Zona Norte Zona Sur 3.38527 -76.52950 750
Acopi Zona Norte Zona Sur 3.40590 -76.53179 625
Acopi Zona Norte Zona Sur 3.36862 -76.54044 750
Acopi Zona Norte Zona Oeste 3.43505 -76.54999 520
Acopi Zona Norte Zona Oeste 3.42125 -76.55210 600
Acopi Zona Norte Zona Sur 3.40050 -76.55363 420
Acopi Zona Norte Zona Sur 3.37823 -76.52680 490
Acopi Zona Norte Zona Sur 3.40770 -76.53638 320
Acopi Zona Norte Zona Centro 3.42400 -76.54173 385
Acopi Zona Norte Zona Sur 3.37775 -76.54531 100
Acopi Zona Norte Zona Centro 3.42006 -76.52875 175

Para asegurar que el filtrado “Casa – Zona Norte” y los mapas reflejen correctamente la ubicación de las ofertas, se revisa la consistencia entre la etiqueta de zona (Norte, Sur, Centro, Oriente, Oeste) y la posición geográfica (latitud/longitud). Como se puede observar no hay registros sin coordenadas.

Inicialmente, se chequean los valores de latitud para revisar posición Norte–Sur y longitud para posición Oeste–Oriente (medianas y cuartiles por zona).

Luego se utiliza la Matriz de confusión entre la etiqueta y una clasificación geográfica derivada (umbrales por cuartiles). Para esto se crea una nueva variable denominada zona_geo (5 clases) basada en el centroide (mediana de latitud/longitud) y comparada con la etiqueta de cada una de las cinco zonas (Centro, Norte, Sur, Oriente y Oeste).

Al comparar zona vs. zona_geo en una matriz de confusión de 5 clases, se obtiene una exactitud del 77%, lo que indica alta consistencia general y revela discrepancias localizadas en barrios fronterizos o con coordenadas imprecisas. Es decir, la aproximación geométrica simple (centro más cercano) coincide en 77% de los casos con la etiqueta de zona que se encontraba en la base de datos. Esta verificación es diagnóstica y no sustituye la etiqueta oficial.

Este análisis permite detectar errores de geocodificación y barrios fronterizos que explican aparentes inconsistencias en el mapa, y documenta con métricas que la zona sea coherente con la ubicación.

La validación espacial confirma que la clasificación por zona es en general coherente con la ubicación geográfica: se obtuvo 89.2% de exactitud para Norte/Sur (por latitud) y 93.5% para Oeste/Oriente (por longitud). Aun así, entre 10–13% de los registros etiquetados como Norte/Sur caen en la franja opuesta, principalmente por barrios limítrofes y coordenadas con error.

La validación espacial confirma que podemos trabajar con la etiqueta de zona como criterio del caso, dejando registro de los casos atípicos y su impacto, y garantizando que las decisiones (filtro, mapas y modelo) se sustentan en evidencia geográfica.

Mapa con la ubicación de los Puntos

A continuación se procede a elaborar un mapa donde se pueden observar los diferentes puntos de ofertas graficados por zonas:

suppressPackageStartupMessages({library(dplyr); library(stringr); library(leaflet)})

# Normalizar etiquetas y quedarnos con registros con coordenadas
viv_mapa <- vivienda %>%
  mutate(zona = str_squish(str_to_title(as.character(zona)))) %>%
  filter(!is.na(longitud), !is.na(latitud))

# Paleta categórica por zona
pal_zona <- colorFactor(palette = "Dark2", domain = sort(unique(viv_mapa$zona)))

leaflet(viv_mapa) %>%
  addTiles() %>%
  addCircleMarkers(
    lng = ~longitud, lat = ~latitud,
    color = ~pal_zona(zona), fillOpacity = 0.8, radius = 5, stroke = FALSE,
    popup = ~paste0("<b>", barrio, "</b><br>", zona,
                    "<br>Precio: ", round(preciom,1), " M")
  ) %>%
  addLegend("topright", pal = pal_zona, values = ~zona, title = "Zona")

Gráficamente hay puntos de ofertas que tienen mal clasificada la zona, lo que confirma el análisis descriptivo reelizado anteriormente, sin embargo a simple vista, muestra que la mayoría de los puntos aparentemente se localizan en la zona correcta.

También se puede evidenciar que los valores de coordenadas no se evidenció anomalías sistemáticas, como problemas de coordenadas invertidas entre latitud y longitud, ni problema de signos, porque osino no se hubieran ubicado todas en la Ciudad de Cali.

Para reclasificar las ofertas en las zonas correctamente, podría construirse una clasificación geográfica alternativa (p. ej., por centroides de barrio/zona); sin embargo, no se usó para reemplazar la etiqueta de zona oficial porque no cambia de forma sustantiva las conclusiones del análisis solicitado y sólo aporta como verificación diagnóstica.

Realizar Filtro Solicitado

En cumplimiento a lo solicitado en el ejericio se realiza un filtro a la base de datos, que incluye solo las ofertas de casas, de la zona norte de la ciudad, denominado base1. El cual se realizó cuando

Mapa de Zona Norte

A continuación se muestra el mapa con las ofertas de la zona norte utilizadas para el análisis:

suppressPackageStartupMessages({
  library(dplyr); library(stringr); library(leaflet)
})

# 1) Construir base1 SOLO con Zona Norte y coords válidas
base1 <- vivienda %>%
  mutate(
    tipo_norm = str_squish(str_to_lower(as.character(tipo))),
    zona_norm = str_squish(str_to_lower(as.character(zona)))
  ) %>%

filter(tipo_norm == "casa",
         str_detect(zona_norm, "norte")) %>%    # captura "Zona Norte", "norte", etc.
  select(-tipo_norm, -zona_norm)

# Primeras 3 filas para comprobar

knitr::kable(
  base1 %>% 
    select(barrio, zona, tipo, estrato, areaconst, banios, habitaciones, parqueaderos, preciom) %>%
    head(3),
  caption = "Ejemplo del filtro realizado: Primeras 3 ofertas: Casa – Zona Norte (base1)"

)
Ejemplo del filtro realizado: Primeras 3 ofertas: Casa – Zona Norte (base1)
barrio zona tipo estrato areaconst banios habitaciones parqueaderos preciom
Acopi Zona Norte Casa 5 150 4 6 2 320
Acopi Zona Norte Casa 5 380 3 3 2 780
Acopi Zona Norte Casa 6 445 7 6 2 750
pal_zona <- colorFactor(palette = "Dark2", domain = sort(unique(base1$zona)))  # será solo 'Zona Norte'

  leaflet(base1) %>%
    addProviderTiles("CartoDB.Positron") %>%
    addCircleMarkers(
      lng = ~longitud, lat = ~latitud,
      color = ~pal_zona(zona), fillOpacity = 0.8,
      radius = 5, stroke = FALSE,
      popup = ~paste0("<b>", barrio, "</b><br>", zona,
                      "<br>Precio: ", round(preciom, 1), " M")
      # , clusterOptions = markerClusterOptions()  # <- opcional: clustering
    ) %>%
    addLegend("topright", pal = pal_zona, values = ~zona, title = "Zona") %>%
    fitBounds(
      lng1 = min(base1$longitud, na.rm = TRUE),
      lat1 = min(base1$latitud,  na.rm = TRUE),
      lng2 = max(base1$longitud, na.rm = TRUE),
      lat2 = max(base1$latitud,  na.rm = TRUE)
    )

Luego se crean las tablas que comprueban el filtro y caracterizan el subconjunto:

# tipo = Casa, zona = Zona Norte 
knitr::kable(base1 %>% count(tipo, zona), 
             caption = "Comprobación del filtro: 'tipo' x 'zona' en base1")
Comprobación del filtro: ‘tipo’ x ‘zona’ en base1
tipo zona n
Casa Zona Norte 717
# Estructura del estrato en el subconjunto
knitr::kable(base1 %>% count(estrato, sort = TRUE),
             caption = "Distribución de 'estrato' – base1")
Distribución de ‘estrato’ – base1
estrato n
5 268
3 234
4 160
6 55
# Estadísticos de precio y área 
resumen_base1 <- base1 %>%
  summarise(
    n          = n(),
    precio_min = min(preciom, na.rm = TRUE),
    precio_p50 = median(preciom, na.rm = TRUE),
    precio_p90 = quantile(preciom, 0.90, na.rm = TRUE),
    precio_max = max(preciom, na.rm = TRUE),
    area_p25   = quantile(areaconst, 0.25, na.rm = TRUE),
    area_p50   = median(areaconst, na.rm = TRUE),
    area_p75   = quantile(areaconst, 0.75, na.rm = TRUE)
  )
knitr::kable(resumen_base1, caption = "Resumen de precio (millones) y área (m²) – base1")
Resumen de precio (millones) y área (m²) – base1
n precio_min precio_p50 precio_p90 precio_max area_p25 area_p50 area_p75
717 89 390 780 1940 140 240 337

Los puntos de ofertas en el mapa, se concentran en el cuadrante norte de la ciudad, con algunas ofertas pegadas al límite con el centro. Esa dispersión en el borde es esperable por barrios limítrofes y la propia naturaleza “comercial” de la etiqueta zona. La latitud de Zona Norte tiene mediana 3.472, IQR [3.457–3.485], lo que claramente se encuentra por encima de Zona Sur (IQR [3.370–3.408]).

Para este análisis se trabaja con las ofertas que tienen clasificada correctamente la zona. Teniendo en cuenta los resultados, se puede decir que hay 717 casas, cuyo Precio (M COP) tiene una mediana de 390, y en el P90 780, con máximos y mínimos de 1.940 y 89 respectivamente, lo que sugiere que el mercado es heterogéneo y con cola derecha larga (pocas casas de muy alto precio que elevan el rango).

El Área (m²): tiene una mediana de 240, pero el 50% de los datos varían entre 140 y 337. Nuestro requisito es de un área de 200 m², lo cual nos indica que existe oferta con áreas cercanas a lo pedido.

Respecto al presupuesto indicado de 350 millones, podemos decir que el presupuesto está ligeramente por debajo del centro del mercado, ya que la mediana está en 390 millones, por lo tanto es posible que haya opciones, pero menos de la mitad de las casas del norte estarán dentro del tope del presupuesto.

Punto 2. Análisis exploratorio de datos enfocado en la correlación entre la variable respuesta (precio de la casa) en función del área construida, estrato, numero de baños, numero de habitaciones y zona donde se ubica la vivienda.

Este corresponde a un análisis global de la base de datos. Adicionalmente, para realizar este análisis se asume que donde en el enunciado indica “(precio de la casa)” se refiere a precio de la vivienda, y se hace el análisis independiente si se trata de casa o apartamento.

Datos para el EDA (Viviendas, con variables de interés)

El análisis exploratorio se centró en la relación entre el precio de la vivienda (transformado con logaritmo natural para estabilizar la varianza) y las variables cuantitativas y categóricas más relevantes: área construida, estrato, número de baños, número de habitaciones y zona.

vivienda_eda <- vivienda %>%
  mutate(
    tipo  = str_squish(str_to_title(as.character(tipo))),
    zona  = str_squish(str_to_title(as.character(zona))),
  
    log_preciom = if (!"log_preciom" %in% names(.)) log1p(preciom) else log_preciom
  ) %>%
  select(preciom, log_preciom, areaconst, parqueaderos, banios, habitaciones,
         estrato, zona, tipo) %>%
  drop_na()

glimpse(vivienda_eda)
## Rows: 8,261
## Columns: 9
## $ preciom      <dbl> 250, 320, 350, 400, 260, 240, 220, 310, 320, 780, 750, 62…
## $ log_preciom  <dbl> 5.525453, 5.771441, 5.860786, 5.993961, 5.564520, 5.48479…
## $ 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, 2, 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, …
## $ estrato      <fct> 3, 3, 3, 4, 5, 5, 4, 5, 5, 5, 6, 4, 5, 6, 4, 5, 5, 4, 5, …
## $ zona         <chr> "Zona Oriente", "Zona Oriente", "Zona Oriente", "Zona Sur…
## $ tipo         <chr> "Casa", "Casa", "Casa", "Casa", "Apartamento", "Apartamen…

El análisis confirma que las variables área construida, estrato, zona, número de baños y habitaciones tienen un papel importante en la determinación del precio de la vivienda. El área y el estrato son los factores más fuertemente correlacionados con el precio, mientras que baños y habitaciones reflejan el efecto de las amenidades. La zona introduce un componente espacial que explica variaciones significativas en el mercado inmobiliario de Cali.

Matriz de correlación numérica

La matriz de correlación entre las variables numéricas de interés (log_precio, área construida, parqueaderos, baños y habitaciones) muestra las siguientes relaciones:

## Correlación (Spearman es más robusta a no normalidad)
vars_num <- vivienda_eda %>% 
  select(log_preciom, areaconst, parqueaderos, banios, habitaciones)

cor_spear <- cor(vars_num, method = "spearman")

fig_cor <- plot_ly(
  z = cor_spear,
  x = colnames(cor_spear),
  y = colnames(cor_spear),
  type = "heatmap",
  colorscale = "RdBu",
  zmin = -1, zmax = 1
) %>%
  layout(title = "Correlación (Spearman) — variables numéricas")

fig_cor

El área construida es la variable más fuertemente asociada al precio (coeficiente cercano a 0.7–0.8), seguida por el número de baños y habitaciones. Los resultados también sugieren que las amenidades (baños, habitaciones, parqueaderos) aportan valor, pero en menor medida que el tamaño. Además, la existencia de correlación entre predictores refuerza la necesidad de aplicar regresión múltiple, para evaluar sus efectos conjuntos y descartar variables redundantes.

Se observa colinealidad moderada entre habitaciones y área construida, lo cual es lógico ya que viviendas más grandes suelen tener más cuartos.

Correlaciones precio–predictor por tipo de vivienda

La tabla presenta las correlaciones de Spearman entre el logaritmo del precio (log_precio) y las variables predictoras (área construida, número de baños, parqueaderos y habitaciones), separadas para apartamentos y casas:

## Correlación Spearman de log_preciom vs. cada predictor, por 'tipo'
corr_por_tipo <- vivienda_eda %>%
  pivot_longer(cols = c(areaconst, parqueaderos, banios, habitaciones),
               names_to = "predictor", values_to = "x") %>%
  group_by(tipo, predictor) %>%
  summarise(rho = suppressWarnings(cor(log_preciom, x, method = "spearman")),
            .groups = "drop") %>%
  arrange(tipo, desc(abs(rho)))

knitr::kable(corr_por_tipo, digits = 3,
             caption = "Correlación (Spearman) de log_preciom con predictores, por tipo")
Correlación (Spearman) de log_preciom con predictores, por tipo
tipo predictor rho
Apartamento areaconst 0.890
Apartamento banios 0.772
Apartamento parqueaderos 0.458
Apartamento habitaciones 0.326
Casa areaconst 0.733
Casa banios 0.657
Casa parqueaderos 0.532
Casa habitaciones 0.209

Para los apartamentos, el área construida es el predictor más fuerte del precio (p = 0.890), indicando una relación casi lineal: a mayor área, mayor valor del apartamento.

El número de baños (p = 0.772) y los parqueaderos (p = 0.458) también muestran correlaciones moderadas-altas, reflejando que estas amenidades influyen notablemente en el precio.

El número de habitaciones (p = 0.326) tiene menor peso explicativo en apartamentos, probablemente porque el diseño cambia el espacio sin que un mayor número de cuartos siempre implique un precio significativamente más alto.

Para las casas, el área construida mantiene una correlación positiva importante (p = 0.733), sin embargo es menor que los apartamentos, lo que sugiere que en las casas el área no es tan representativa como en los aparatamentos.

El número de baños (p= 0.657) y parqueaderos (p = 0.532) muestran correlaciones más pequeñas, confirmando que estas amenidades aportan al valor de la propieda, sin embargo, variables como el número de habitaciones no es significativa para las casas.

log_preciom vs. área, con color por estrato

# Global
fig_area <- plot_ly(
  data = vivienda_eda,
  x = ~areaconst, y = ~log_preciom,
  color = ~estrato, colorscale = "Viridis",
  type = "scatter", mode = "markers",
  text = ~paste("Tipo:", tipo, "<br>Zona:", zona)
) %>%
  layout(title = "log(Precio) vs. Área construida — color: estrato",
         xaxis = list(title = "Área (m²)"),
         yaxis = list(title = "log(Precio)"))
fig_area
# Facetas por zona (ahora sí con zz definido dentro del lapply)
zonas_ord <- sort(unique(vivienda_eda$zona))

lista_sub <- lapply(zonas_ord, function(zz) {
  plot_ly(
    data = dplyr::filter(vivienda_eda, zona == zz),
    x = ~areaconst, y = ~log_preciom,
    color = ~estrato, colorscale = "Viridis",
    type = "scatter", mode = "markers",
    text = ~paste("Tipo:", tipo)
  ) %>%
    layout(title = zz,
           xaxis = list(title = "Área (m²)"),
           yaxis = list(title = "log(Precio)"))
})

fig_area_facet <- subplot(lista_sub, nrows = 1, shareX = TRUE, shareY = TRUE, margin = 0.03) %>%
  layout(title = "log(Precio) vs Área por Zona (color = estrato)")
fig_area_facet

Los graficos refuerzan lo analizado con la correlación de Spearman, evidenciando que el área construida es el predictor más fuerte del precio en todo el mercado inmobiliario.

Adicionalmente se observa la interacción entre área y estrato, donde viviendas con igual área pueden tener precios muy diferentes según el estrato, lo que confirma la importancia de incluir ambas variables en el modelo de regresión.

Efecto de variables discretas: cajas de log_preciom por estrato y por zona

A continuación se realiza un análisis de gráficos de cajas y bigotes para mirar el efecto de los precios y el estrato socioeconómico y también mirar el efecto de los predios respecto a la zona geográfica dónde están ubicados.

fig_box_estrato <- plot_ly(
  vivienda_eda, x = ~estrato, y = ~log_preciom,
  type = "box", color = ~estrato
) %>%
  layout(title = "Distribución de log(Precio) por Estrato",
         xaxis = list(title = "Estrato"),
         yaxis = list(title = "log(Precio)"))
fig_box_estrato
fig_box_zona <- plot_ly(
  vivienda_eda, x = ~zona, y = ~log_preciom,
  type = "box", color = ~zona
) %>%
  layout(title = "Distribución de log(Precio) por Zona",
         xaxis = list(title = "Zona"),
         yaxis = list(title = "log(Precio)"))
fig_box_zona

En general se observa como los precios aumentan sistemáticamente con el estrato socioeconómico, lo que es esperable. En estrato 3, la mediana del precio es la más baja, con un rango intercuartílico reducido y fuerte concentración de precios entre 4.8 y 5.5 en log(precio) y en estrato 6 la mediana es la más alta y la caja está desplazada hacia la parte superior del eje, reflejando que este estrato concentra las viviendas de mayor precio. Por lo tanto el estrato es un determinante clave del precio

La localización geográfica (zona) también influye en el precio. En Cali, las viviendas más costosas tienden a concentrarse en el Oeste, mientras que las más económicas predominan en el Oriente.

Dispersión de banios y habitaciones (efecto tamaño/Características adicionales)

A continuación se realiza un análisis de como el número de habitaciones y de baños influyen en el precio.

# Baños vs log(Precio)
fig_banios <- plot_ly(
  data = vivienda_eda,
  x = ~banios, y = ~log_preciom,
  color = ~estrato, colorscale = "Viridis",
  type = "scatter", mode = "markers",
  text = ~paste("Tipo:", tipo, "<br>Zona:", zona)
) %>%
  layout(
    title = "log(Precio) vs Número de baños",
    xaxis = list(title = "Número de baños"),
    yaxis = list(title = "log(Precio)")
  )

# Habitaciones vs log(Precio)
fig_hab <- plot_ly(
  data = vivienda_eda,
  x = ~habitaciones, y = ~log_preciom,
  color = ~estrato, colorscale = "Viridis",
  type = "scatter", mode = "markers",
  text = ~paste("Tipo:", tipo, "<br>Zona:", zona)
) %>%
  layout(
    title = "log(Precio) vs Número de habitaciones",
    xaxis = list(title = "Número de habitaciones"),
    yaxis = list(title = "log(Precio)")
  )

# Mostrar lado a lado
fig_banios
fig_hab

Existe también una relación positiva: a mayor número de baños, mayor precio, sin embargo, el efecto es más marcado en estratos 5 y 6, donde los incrementos en baños se asocian con precios mucho más altos.

La correlación del numero de habitaciones con el precio es positiva, pero más débil que con área y baños.

Se observa que muchas viviendas tienen 3–5 habitaciones, lo que genera una saturación en el rango medio, con precios muy variables en ese intervalo.

Matriz de Correlaciones

Como conclusión al análisis realizado en este numeral, se incluye la matriz de correlaciones.

library(corrplot)

vars_cor <- vivienda_eda %>%
  dplyr::select(preciom, log_preciom, areaconst, parqueaderos, banios, habitaciones)

M <- cor(vars_cor, use = "complete.obs")
corrplot(M, method = "color", type = "upper", addCoef.col = "black",
         tl.col = "black", tl.srt = 45, title = "Matriz de correlaciones", mar = c(0,0,1,0))

La matriz confirma los patrones ya observados en los gráficos exploratorios:

Precio vs área construida

Existe una correlación positiva importante (ρ ≈ 0.69 con preciom y 0.67 con log_preciom). Esto reafirma que el tamaño de la vivienda es uno de los factores más determinantes en la formación del precio.

Precio vs número de baños

La correlación es moderada-alta (ρ ≈ 0.67 con preciom, 0.72 con log_preciom). Indica que el número de baños es una amenidad clave que incrementa el valor de la vivienda.

Precio vs parqueaderos

Presenta correlación positiva moderada (ρ ≈ 0.62 con preciom, 0.54 con log_preciom). Confirma que la disponibilidad de parqueaderos influye, aunque menos que el área y los baños.

Precio vs número de habitaciones

La correlación es baja (ρ ≈ 0.26 con preciom, 0.32 con log_preciom). Esto sugiere que la cantidad de cuartos por sí sola no explica bien el precio, pues depende en mayor medida de la distribución del área y de otros atributos (estrato, zona, baños).

Colinealidad entre predictores

Se observa correlación moderada entre area, baños, parqueaderos y habitaciones (ρ entre 0.5 y 0.6). Esto es esperable porque viviendas de mayor área tienden a tener más habitaciones y baños. Aunque no es una colinealidad extrema, debe revisarse en la regresión múltiple para evitar redundancias.

Punto 3. Modelo de Regresión Lineal Múltiple

A continuación se estima el modelo de regresión lineal múltiple con las variables del punto anterior (precio = f(área construida, estrato, número de cuartos, número de parqueaderos, número de baños ) )

### Modelo de regresión lineal múltiple
# Usamos log_preciom como respuesta (para estabilizar la varianza)

modelo <- lm(
  log_preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios,
  data = vivienda_eda
)

# Resumen del modelo
summary(modelo)
## 
## Call:
## lm(formula = log_preciom ~ areaconst + estrato + habitaciones + 
##     parqueaderos + banios, data = vivienda_eda)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -2.10674 -0.19639 -0.00856  0.17746  1.85137 
## 
## Coefficients:
##               Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  4.593e+00  1.332e-02 344.790   <2e-16 ***
## areaconst    1.469e-03  3.317e-05  44.283   <2e-16 ***
## estrato4     2.847e-01  1.076e-02  26.462   <2e-16 ***
## estrato5     5.557e-01  1.060e-02  52.416   <2e-16 ***
## estrato6     9.436e-01  1.287e-02  73.314   <2e-16 ***
## habitaciones 5.850e-03  3.265e-03   1.792   0.0732 .  
## parqueaderos 5.450e-02  4.077e-03  13.369   <2e-16 ***
## banios       1.252e-01  3.863e-03  32.418   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.3006 on 8253 degrees of freedom
## Multiple R-squared:  0.7981, Adjusted R-squared:  0.7979 
## F-statistic:  4660 on 7 and 8253 DF,  p-value: < 2.2e-16
# Tabla ordenada y clara con broom
library(broom)
tabla_modelo <- tidy(modelo, conf.int = TRUE)

knitr::kable(tabla_modelo, digits = 3,
             caption = "Estimación de coeficientes del modelo de regresión múltiple")
Estimación de coeficientes del modelo de regresión múltiple
term estimate std.error statistic p.value conf.low conf.high
(Intercept) 4.593 0.013 344.790 0.000 4.566 4.619
areaconst 0.001 0.000 44.283 0.000 0.001 0.002
estrato4 0.285 0.011 26.462 0.000 0.264 0.306
estrato5 0.556 0.011 52.416 0.000 0.535 0.576
estrato6 0.944 0.013 73.314 0.000 0.918 0.969
habitaciones 0.006 0.003 1.792 0.073 -0.001 0.012
parqueaderos 0.055 0.004 13.369 0.000 0.047 0.062
banios 0.125 0.004 32.418 0.000 0.118 0.133

Resumen de los Residuos Los residuos están centrados cerca de cero (mediana ≈ -0.0086), lo cual es una buena señal de ajuste.

El rango va de -2.10 a 1.85 en log(precio), indicando que hay algunas observaciones con diferencias grandes entre el precio observado y el predicho (posibles outliers).

Coeficientes del modelo

Intercepto (4.593, p < 0.001): valor de referencia del log(precio) cuando todas las variables son 0. No tiene interpretación práctica directa, pero sirve como punto de base del modelo.

Área construida (0.001469, p < 0.001): por cada m² adicional de área, el log(precio) aumenta en promedio 0.0015 unidades, manteniendo las demás variables constantes.

*Aproximadamente, un aumento de 100 m² incrementa el precio en un 15%.

*Estrato 4 (0.285, p < 0.001): respecto al estrato 3 (categoría base), el log(precio) aumenta en 0.285 unidades. Equivale a un incremento de alrededor del 28% en el precio.

*Estrato 5 (0.556, p < 0.001): respecto al estrato 3, aumenta el log(precio) en 0.556 unidades. Equivale a un Incremento cercano al 55% en el precio.

*Estrato 6 (0.944, p < 0.001): respecto al estrato 3, el log(precio) aumenta en 0.944 unidades. Esto implica casi un 95% de incremento en el precio.

*Habitaciones (0.006, p = 0.073): no es estadísticamente significativo (p > 0.05). Sugiere que, controlando por área y baños, el número de habitaciones no tiene un efecto independiente fuerte en el precio.

*Parqueaderos (0.055, p < 0.001): cada parqueadero adicional incrementa el log(precio) en 0.055 unidades.Aproximadamente un 5.6% más en el precio por cada parqueadero.

*Baños (0.125, p < 0.001): cada baño adicional incrementa el log(precio) en 0.125 unidades. Lo que indica que un aumento cercano al 13% en el precio por cada baño.

Bondad de ajuste del modelo

R² = 0.798 y R² ajustado = 0.798: el modelo explica aproximadamente el 80% de la variabilidad del precio.

Lo que muestra un muy buen nivel de ajuste, donde suele existir alta heterogeneidad.

Error estándar residual = 0.3006: indica que la diferencia promedio entre los valores observados y predichos en log(precio) es pequeña.

Prueba F (p < 0.001): el modelo en su conjunto es altamente significativo.

Conclusión El precio de la vivienda en Cali está determinado principalmente por el tamaño (área), el estrato socioeconómico, el número de baños y los parqueaderos. El modelo explica el 80% de la variabilidad y confirma que las habitaciones no son un predictor significativo independiente. Esto es lógico y refleja cómo el mercado inmobiliario valora más el área total y las amenidades, que solo el número de cuartos. Sin embargo, el 20% de la varición del precio en las viviendas no es explicado por las variables seleccionadas.

El modelo podría mejorarse incorporando variables adicionales como zona y tipo de vivienda, explorando relaciones no lineales del área, y revisando la multicolinealidad. También es recomendable aplicar validación cruzada para evaluar la capacidad predictiva en datos nuevos.

Punto 4. Validación de supuestos del modelo

A continuación se realiza la validación de supuestos del modelo como normalidad, homocedasticidad, independencia y multicolinealidad.

# --- Supuestos del modelo de regresión múltiple ---
library(lmtest)     # para pruebas estadísticas
library(car)        # para VIF
library(ggplot2)

# 1. Normalidad de residuos
set.seed(123)
muestra_res <- sample(residuals(modelo), 5000)  # tomar 5000 aleatorios
shapiro.test(muestra_res)
## 
##  Shapiro-Wilk normality test
## 
## data:  muestra_res
## W = 0.98586, p-value < 2.2e-16
# Histograma de residuos
hist(residuals(modelo), breaks = 30, main = "Histograma de residuos", col = "skyblue")

# Q-Q plot de residuos
qqnorm(residuals(modelo)); qqline(residuals(modelo), col = "red")

# 2. Homocedasticidad (varianza constante)
bptest(modelo)   # prueba de Breusch-Pagan
## 
##  studentized Breusch-Pagan test
## 
## data:  modelo
## BP = 1412.7, df = 7, p-value < 2.2e-16
# 3. Independencia de los residuos
dwtest(modelo)   # prueba de Durbin-Watson
## 
##  Durbin-Watson test
## 
## data:  modelo
## DW = 1.6166, p-value < 2.2e-16
## alternative hypothesis: true autocorrelation is greater than 0
# 4. Multicolinealidad
vif(modelo)      # factores de inflación de varianza
##                  GVIF Df GVIF^(1/(2*Df))
## areaconst    2.060118  1        1.435311
## estrato      1.688376  3        1.091218
## habitaciones 2.077085  1        1.441210
## parqueaderos 1.561993  1        1.249797
## banios       2.788396  1        1.669849

Normalidad de residuos (Shapiro-Wilk + gráficos)

El test de Shapiro-Wilk (p < 0.001) rechaza normalidad, pero esto es esperable con más de 8000 datos (es muy sensible con muestras grandes). El histograma muestra una distribución aproximadamente simétrica y con forma de campana, centrada en 0. Esto indica que la mayor parte de los errores del modelo son pequeños y están bien distribuidos alrededor del valor esperado.

La normalidad de los residuos se cumple de manera aceptable.

Homoscedasticidad (Breusch-Pagan test)

El test de Breusch-Pagan dio p < 0.001, sugiere heterocedasticidad (la varianza de los errores no es constante). Esto significa que el error del modelo puede ser mayor para ciertos niveles de precio/área, para lo que es recomendable usar transformaciones, como la que se utilizó en log(precio).

Independencia de residuos (Durbin-Watson)

El estadístico DW = 1.616, lo cual es un valor inferior a 2, con p < 0.001, lo que se interpreta como autocorrelación positiva en los residuos. Podría incluirsen variables de localización más detalladas para reducir esta dependencia.

Normalidad de residuos (Shapiro-Wilk + gráficos)

Todos los VIF están entre 1.4 y 2.8, muy por debajo del umbral crítico de 10. Esto significa que no hay multicolinealidad grave entre las variables predictoras.

Conclusión El modelo presenta residuos aproximadamente normales y sin problemas de multicolinealidad.

Se detecta heterocedasticidad y cierta autocorrelación positiva, pero estas no invalidan el modelo; solo sugieren que las inferencias pueden ajustarse con errores robustos y que sería útil incorporar variables espaciales o categóricas adicionales.

En conjunto, el modelo es válido, aunque con oportunidades de mejora en el tratamiento de la variabilidad y la dependencia de los datos.

Punto 5. Predicción del precio de la vivienda con las características de la primera solicitud.

Con el modelo elaborado se predice el precio de la vivienda para el total del modelo y se calculan las métricas con el set de prueba (el 30%) para evaluar el rendimiento global del modelo.

Posteriormente se predicen los precios de la vivienda con las características de la primera solicitud (Casa, 200 m², 1 parqueadero, 2 baños, 4 habitaciones, estrato 4 o 5)

set.seed(123)  # para reproducibilidad

# Definir índice de entrenamiento (70% de los datos)
n <- nrow(vivienda_eda)
train_index <- sample(1:n, size = 0.7*n)

# Crear sets
train_data <- vivienda_eda[train_index, ]
test_data  <- vivienda_eda[-train_index, ]

# Ajustar el modelo con el set de entrenamiento
modelo_train <- lm(log_preciom ~ areaconst + estrato + habitaciones +
                     parqueaderos + banios, 
                   data = train_data)

# Predicciones en el set de prueba
pred_test <- predict(modelo_train, newdata = test_data)

# Comparar con los valores reales
resultados <- data.frame(
  real = test_data$log_preciom,
  predicho = pred_test
)
head(resultados)
# Calcular métricas
MAE <- mae(resultados$real, resultados$predicho)
RMSE <- rmse(resultados$real, resultados$predicho)
R2 <- R2(resultados$predicho, resultados$real)

metricas <- data.frame(
  Metrica = c("MAE", "RMSE", "R²"),
  Valor = c(MAE, RMSE, R2)
)

knitr::kable(metricas, digits = 3,
             caption = "Indicadores de desempeño del modelo en el set de prueba")
Indicadores de desempeño del modelo en el set de prueba
Metrica Valor
MAE 0.230
RMSE 0.295
0.803

El modelo de regresión lineal múltiple fue evaluado mediante un conjunto de prueba independiente, a fin de validar su capacidad predictiva. Los indicadores de desempeño obtenidos fueron los siguientes: un MAE de 0.23, un RMSE de 0.29 y un R² de 0.80.

El MAE refleja que, en promedio, el error de predicción es bajo, lo que implica que las estimaciones del modelo se encuentran cercanas a los valores observados de log(precio). Por su parte, el RMSE, al penalizar de manera más fuerte los errores grandes, confirma que las predicciones no presentan desviaciones extremas, evidenciando estabilidad en el ajuste. Finalmente, el R² de 0.80 indica que el modelo es capaz de explicar aproximadamente el 80% de la variabilidad de los precios en el conjunto de prueba.

La comparación entre valores reales y predichos confirma que el modelo logra aproximar de manera precisa los precios de prueba, con errores moderados y sin tendencia clara a sobrestimar o subestimar. Este comportamiento es coherente con las métricas globales de desempeño (MAE, RMSE y R²), lo que refuerza la confiabilidad del modelo para realizar predicciones en nuevos casos.

# newdata con las características solicitadas
viv1_new <- data.frame(
  areaconst    = 200,
  estrato      = factor(c(4, 5), levels = levels(train_data$estrato)),  # asegurar mismos niveles
  habitaciones = 4,
  parqueaderos = 1,
  banios       = 2
)

# predicción en escala log con intervalo de predicción
pred_log_viv1 <- predict(modelo_train, newdata = viv1_new, interval = "prediction")

# volver a escala original (millones de pesos) usando expm1 porque el modelo usó log1p
pred_viv1 <- cbind(viv1_new, pred_log_viv1)
pred_viv1$precio_fit <- expm1(pred_viv1$fit)
pred_viv1$precio_lwr <- expm1(pred_viv1$lwr)
pred_viv1$precio_upr <- expm1(pred_viv1$upr)

# comparar con el crédito preaprobado (350 millones)
pred_viv1$credito_mill <- 350
pred_viv1$viable <- ifelse(pred_viv1$precio_fit <= pred_viv1$credito_mill, "Sí", "No")

# Tabla limpia 
tabla_viv1 <- pred_viv1[, c("estrato","areaconst","habitaciones","parqueaderos","banios",
                            "precio_fit","precio_lwr","precio_upr","credito_mill","viable")]

knitr::kable(tabla_viv1, digits = 0,
             col.names = c("Estrato","Área (m²)","Habitaciones","Parqueaderos","Baños",
                           "Precio estimado (M)","Límite inferior (M)","Límite superior (M)",
                           "Crédito (M)","¿Viable?"),
             caption = "Predicción de precio para la Vivienda 1 (dos escenarios de estrato)")
Predicción de precio para la Vivienda 1 (dos escenarios de estrato)
Estrato Área (m²) Habitaciones Parqueaderos Baños Precio estimado (M) Límite inferior (M) Límite superior (M) Crédito (M) ¿Viable?
4 200 4 1 2 242 133 440 350
5 200 4 1 2 321 177 583 350
# Predicción para la Vivienda 1 (dos escenarios: estrato 4 y 5)

# Perfil: 200 m², 1 parqueadero, 2 baños, 4 habitaciones
viv1_new <- data.frame(
  areaconst    = 200,
  estrato      = factor(c(4, 5), levels = levels(train_data$estrato)),  # asegurar mismos niveles
  habitaciones = 4,
  parqueaderos = 1,
  banios       = 2
)

pred_log_viv1 <- predict(modelo_train, newdata = viv1_new, interval = "prediction")

pred_viv1 <- cbind(viv1_new, pred_log_viv1) %>%
  mutate(
    precio_fit = expm1(fit),
    precio_lwr = expm1(lwr),
    precio_upr = expm1(upr),
    credito_mill = 350,
    viable = ifelse(precio_fit <= credito_mill, "Sí", "No")
  ) %>%
  transmute(
    Estrato = as.character(estrato),
    `Área (m²)` = areaconst,
    Habitaciones = habitaciones,
    Parqueaderos = parqueaderos,
    Baños = banios,
    `Precio estimado (M)` = round(precio_fit, 0),
    `Límite inferior (M)` = round(precio_lwr, 0),
    `Límite superior (M)` = round(precio_upr, 0),
    `Crédito (M)` = credito_mill,
    `¿Viable?` = viable
  )

# Observado vs. predicho en TEST (precio en millones de COP) ----------
preds_test_tab1 <- test_data %>%
  mutate(
    log_pred       = pred_test,
    pred_mill      = round(expm1(log_pred), 1),
    real_mill      = round(expm1(log_preciom), 1),
    error_mill     = real_mill - pred_mill,
    abs_error_mill = abs(error_mill)
  ) %>%
  filter(tipo == "Casa",
         zona == "Zona Norte",
         estrato %in% c(4, 5)) %>%
  
  select(zona, tipo, estrato, areaconst, parqueaderos, banios, habitaciones,
         real_mill, pred_mill, error_mill, abs_error_mill)

knitr::kable(
  head(preds_test_tab1, 10),
  caption = "Predicciones en TEST (Modelo global, Vivienda 1): observado vs. predicho (M COP) y error"
)
Predicciones en TEST (Modelo global, Vivienda 1): observado vs. predicho (M COP) y error
zona tipo estrato areaconst parqueaderos banios habitaciones real_mill pred_mill error_mill abs_error_mill
Zona Norte Casa 5 380 2 3 3 780 496.5 283.5 283.5
Zona Norte Casa 4 355 3 5 5 625 498.7 126.3 126.3
Zona Norte Casa 4 160 1 4 5 600 296.1 303.9 303.9
Zona Norte Casa 4 117 2 3 4 305 257.9 47.1 47.1
Zona Norte Casa 4 115 2 3 3 320 255.3 64.7 64.7
Zona Norte Casa 5 357 2 3 6 390 491.1 -101.1 101.1
Zona Norte Casa 5 380 2 3 3 780 496.5 283.5 283.5
Zona Norte Casa 5 334 2 4 5 550 534.6 15.4 15.4
Zona Norte Casa 5 595 2 2 3 800 597.2 202.8 202.8
Zona Norte Casa 4 190 2 2 3 275 250.9 24.1 24.1

Para Estrato 4, el modelo estima un precio promedio de 242 millones, con un rango probable entre 133 y 440 millones.

Dado que el crédito preaprobado es de 350 millones, la compra sería viable, ya que incluso en el escenario superior (440 M) no se encuentra tan alto respecto al crédito preaprobado.

Para Estrato 5, el precio estimado sube a 321 millones, con un rango más amplio entre 177 y 583 millones. Aunque el límite superior excede el crédito, el valor central y gran parte del intervalo se mantienen dentro del presupuesto, por lo que la compra sigue siendo viable.

El análisis muestra que para la Vivienda 1, tanto en estrato 4 como en estrato 5, el modelo predice precios compatibles con el crédito disponible de 350 millones. Esto indica que la adquisición es financieramente factible, aunque en estrato 5 existe mayor riesgo de que el precio final supere el crédito aprobado.

El modelo tiende a subestimar precios altos (cuando la casa está en el rango de 700–800 millones, las predicciones son mucho más bajas). En precios más intermedios (250–550 M), el modelo predice con mayor precisión, con errores absolutos bajos (< 70 M en algunos casos).

Punto 6. Sugerencia potenciales ofertas que responda a la solicitud de la vivienda 1.

Ahora con las predicciones del modelo se sugieren potenciales ofertas que responden a la solicitud de la vivienda 1.

suppressPackageStartupMessages({
  library(dplyr); library(stringr); library(leaflet); library(knitr); library(scales)
})

# Perfil objetivo y utilidades ---
target <- list(
  tipo = "Casa",
  zona = "Zona Norte",
  areaconst = 200,
  parqueaderos = 1,
  banios = 2,
  habitaciones = 4,
  estratos_ok = c(4, 5),
  credito = 350
)

# Selecciona el Modelo a usar
modelo_uso <- if (exists("modelo_train")) modelo_train else modelo

# Base de candidatos: Casa + Zona Norte + coordenadas válidas en Cali

base1_p6 <- vivienda %>%
  mutate(
    tipo = str_squish(str_to_title(as.character(tipo))),
    zona = str_squish(str_to_title(as.character(zona)))
  ) %>%
  filter(
    tipo == target$tipo,
    zona == target$zona,
    !is.na(latitud), !is.na(longitud),
    dplyr::between(latitud, 3.33, 3.48),          # recorte suave para Cali (N-S)
    dplyr::between(longitud, -76.59, -76.46)      # recorte suave para Cali (W-E)
  )

# Predicción de precios (en millones COP) sobre candidatos ---
# Asegurar niveles de estrato compatibles con el modelo
base1_p6 <- base1_p6 %>%
  mutate(estrato = factor(estrato, levels = levels(vivienda_eda$estrato)))

pred_log <- predict(modelo_uso, newdata = base1_p6, na.action = na.exclude)
base1_p6 <- base1_p6 %>% mutate(precio_pred = expm1(pred_log))

# Filtro por presupuesto y preferencias mínimas del perfil
candidatas <- base1_p6 %>%
  filter(
    precio_pred <= target$credito,
    estrato %in% target$estratos_ok,
    parqueaderos >= target$parqueaderos,
    banios >= target$banios,
    habitaciones >= target$habitaciones
  ) %>%
  # Ranking por similitud al perfil (menor score = mejor)
  mutate(
    score =
      0.50 * abs(areaconst - target$areaconst) / max(1, target$areaconst) +
      0.20 * abs(habitaciones - target$habitaciones) +
      0.15 * abs(banios - target$banios) +
      0.10 * abs(parqueaderos - target$parqueaderos) +
      0.05 * ifelse(as.integer(estrato) %in% target$estratos_ok, 0, 1)
  )

# Si no alcanza 5, relajar solo similitud (mantiene tipo, zona y presupuesto)
if (nrow(candidatas) < 5) {
  candidatas <- base1_p6 %>%
    filter(precio_pred <= target$credito) %>%
    mutate(
      score =
        0.55 * abs(areaconst - target$areaconst) / max(1, target$areaconst) +
        0.20 * abs(habitaciones - target$habitaciones) +
        0.15 * abs(banios - target$banios) +
        0.10 * abs(parqueaderos - target$parqueaderos)
    )
}

# Selección final: Top-5 por score y, en empate, menor precio predicho
top5 <- candidatas %>%
  arrange(score, precio_pred) %>%
  slice_head(n = 5) %>%
  mutate(precio_pred = round(precio_pred, 1))

# Top 5 ofertas recomendadas
knitr::kable(
  top5 %>%
    select(barrio, zona, estrato, areaconst, habitaciones, banios, parqueaderos,
           preciom, precio_pred),
  caption = "Top-5 ofertas recomendadas (precio observado y predicho, millones COP)"
)
Top-5 ofertas recomendadas (precio observado y predicho, millones COP)
barrio zona estrato areaconst habitaciones banios parqueaderos preciom precio_pred
Santa Manica Residencial Zona Norte 5 212 4 2 2 400 346.2
Alamos Zona Norte 4 120 4 2 1 275 215.7
Zona Norte Zona Norte 4 162 4 3 1 265 260.0
La Merced Zona Norte 4 192 4 3 2 370 287.5
Prados Del Norte Zona Norte 5 146 4 3 1 300 337.1
# Mapa interactivo con popup detallado
popup_txt <- with(
  top5,
  paste0(
    "<b>", barrio, "</b><br>",
    zona, " · Estrato ", estrato, "<br>",
    "Área: ", areaconst, " m² · Hab: ", habitaciones,
    " · Baños: ", banios, " · Parq: ", parqueaderos, "<br>",
    "Precio observado: ", scales::number(preciom, accuracy = 0.1), " M<br>",
    "<b>Precio predicho: ", scales::number(precio_pred, accuracy = 0.1), " M</b><br>",
    "Tope crédito: 350 M"
  )
)

# Paleta numérica para colorear por precio_pred
pal_precio <- colorNumeric(
  palette = "Viridis",             # puedes cambiar por "YlGnBu", etc.
  domain  = top5$precio_pred
)

leaflet(top5) %>%
  addProviderTiles("CartoDB.Positron") %>%
  addCircleMarkers(
    lng = ~longitud, lat = ~latitud,
    radius = 7, stroke = TRUE, weight = 1, fillOpacity = 0.9,
    fillColor = "#FF69B4",,  # color de relleno según paleta
    color = '#FF69B4',                     # borde
    popup = popup_txt,
    label = ~paste0(barrio, " (", precio_pred, " M)")
  ) %>%
  addLegend(
    position = "bottomright",
    pal = pal_precio,                      # <- AQUÍ está la clave
    values = ~precio_pred,
    title = "Precio predicho (M COP)",
    labFormat = labelFormat(suffix = " M"),
    opacity = 0.8
  ) %>%
  fitBounds(
    lng1 = min(top5$longitud, na.rm = TRUE),
    lat1 = min(top5$latitud,  na.rm = TRUE),
    lng2 = max(top5$longitud, na.rm = TRUE),
    lat2 = max(top5$latitud,  na.rm = TRUE)
  )

Según el análisis se detectaron que exactamente con la condición que se plantea en el ejericio no se encuentran ofertas, sin embargo se flexibilizaron un poco los parámetros para lograr dar la asesoría, en la que se recomiendan 5 potenciales ofertas que rondan los 350 millones de pesos, localizadas en la zona norte y con características de área y amenidades similares a las solicitadas por el cliente. Todas las ofertas cumplen con el estrato, el número de habitaciones, baños mínimo los solicitados, al igual que los parqueaderos,área se busco una cercana y el precio también se buscó uno que no se subiera mucho del presupuesto.

Punto 7. pasos del 1 al 6. Para la segunda solicitud que tiene un crédito pre-aprobado por valor de $850 millones.

Punto 7.1. Filtro base y verificación (Apartamento – Zona Sur)

# APARTAMENTO + ZONA SUR (coords válidas)
base2 <- vivienda %>%
  mutate(
    tipo_norm = str_squish(str_to_lower(as.character(tipo))),
    zona_norm = str_squish(str_to_lower(as.character(zona)))
  ) %>%
  filter(
    tipo_norm == "apartamento",
    str_detect(zona_norm, "sur")
  ) %>%
  select(-tipo_norm, -zona_norm) %>%
  filter(!is.na(longitud), !is.na(latitud))

# Chequeo rápido
knitr::kable(
  base2 %>% 
    select(barrio, zona, tipo, estrato, areaconst, banios, habitaciones, parqueaderos, preciom) %>%
    head(3),
  caption = "Primeras 3 ofertas — Apartamento · Zona Sur (base2)"
)
Primeras 3 ofertas — Apartamento · Zona Sur (base2)
barrio zona tipo estrato areaconst banios habitaciones parqueaderos preciom
Acopi Zona Sur Apartamento 4 96 2 3 1 290
Aguablanca Zona Sur Apartamento 3 40 1 2 1 78
Aguacatal Zona Sur Apartamento 6 194 5 3 2 875
# Comprobación del filtro
knitr::kable(base2 %>% count(tipo, zona),
             caption = "Comprobación del filtro: 'tipo' x 'zona' en base2")
Comprobación del filtro: ‘tipo’ x ‘zona’ en base2
tipo zona n
Apartamento Zona Sur 2759
# Distribución de estrato en el subconjunto
knitr::kable(base2 %>% count(estrato, sort = TRUE),
             caption = "Distribución de 'estrato' – base2")
Distribución de ‘estrato’ – base2
estrato n
4 1074
5 1025
6 460
3 200
# Resumen de precio y área
resumen_base2 <- base2 %>%
  summarise(
    n          = n(),
    precio_min = min(preciom, na.rm = TRUE),
    precio_p50 = median(preciom, na.rm = TRUE),
    precio_p90 = quantile(preciom, 0.90, na.rm = TRUE),
    precio_max = max(preciom, na.rm = TRUE),
    area_p25   = quantile(areaconst, 0.25, na.rm = TRUE),
    area_p50   = median(areaconst, na.rm = TRUE),
    area_p75   = quantile(areaconst, 0.75, na.rm = TRUE)
  )
knitr::kable(resumen_base2, caption = "Resumen de precio (M COP) y área (m²) – base2")
Resumen de precio (M COP) y área (m²) – base2
n precio_min precio_p50 precio_p90 precio_max area_p25 area_p50 area_p75
2759 75 245 560 1750 65 85 110
# --- Mapa de Apartamentos – Zona Sur ---
pal_zona2 <- colorFactor(palette = "Dark2", domain = sort(unique(base2$zona)))  # será 'Zona Sur'

leaflet(base2) %>%
  addProviderTiles("CartoDB.Positron") %>%
  addCircleMarkers(
    lng = ~longitud, lat = ~latitud,
    color = ~pal_zona2(zona), fillOpacity = 0.8,
    radius = 5, stroke = FALSE,
    popup = ~paste0("<b>", barrio, "</b><br>", zona,
                    "<br>Precio: ", round(preciom, 1), " M")
  ) %>%
  addLegend("topright", pal = pal_zona2, values = ~zona, title = "Zona") %>%
  fitBounds(
    lng1 = min(base2$longitud, na.rm = TRUE),
    lat1 = min(base2$latitud,  na.rm = TRUE),
    lng2 = max(base2$longitud, na.rm = TRUE),
    lat2 = max(base2$latitud,  na.rm = TRUE)
  )

Al igual que en la zona norte y en todo Cali en esta base de datos, gráficamente hay puntos de ofertas que tienen mal clasificada la zona, lo que confirma el análisis descriptivo realizado anteriormente, sin embargo, a simple vista, muestra que la mayoría de los puntos aparentemente se localizan en la zona correcta.

También se puede evidenciar que los valores de coordenadas no se evidenció anomalías sistemáticas, como problemas de coordenadas invertidas entre latitud y longitud, ni problema de signos, porque osino no se hubieran ubicado todas en la Ciudad de Cali.

Para reclasificar las ofertas en las zonas correctamente, podría construirse una clasificación geográfica alternativa (p. ej., por centroides de barrio/zona); sin embargo, no se usó para reemplazar la etiqueta de zona oficial porque no cambia de forma sustantiva las conclusiones del análisis solicitado y sólo aporta como verificación diagnóstica.

Para hacer la verificación del filtro se muestran las 3 primeras ofertas ue cumplen con la condición de estar ubicadas en la zona sur y son tipo apartamento. No se hace filtro por presupuesto aún. Posteriormente también se muestran unas estadísticas sencillas, mostrando la distribución de estas ofertas por estrato y la distribución de precios.

Punto 7.2. Análisis exploratorio de datos enfocado en la correlación entre la variable respuesta (precio del apartamento) en función del área construida, estrato, numero de baños, numero de habitaciones y zona donde se ubica la vivienda.

Este análisis corresponde a un análisis exploratorio de datos para los apartamentos ubicados en la Zona Sur de Cali, a diferencia del análisis realizado en el punto 2, que corresponde a la totalidad de viviendas en Cali.

Datos para el EDA (Viviendas, con variables de interés)

El análisis exploratorio se centró en la relación entre el precio de la vivienda (transformado con logaritmo natural para estabilizar la varianza) y las variables cuantitativas y categóricas más relevantes: área construida, estrato, número de baños, número de habitaciones y zona.

# Subconjunto específico para Vivienda 2 (Apartamentos – Zona Sur)

base2_eda <- base2 %>%
  mutate(log_preciom = log1p(preciom)) %>%
  select(preciom, log_preciom, areaconst, parqueaderos,
         banios, habitaciones, estrato, zona, tipo, barrio) %>%
  drop_na()

glimpse(base2_eda)
## Rows: 2,759
## Columns: 10
## $ preciom      <dbl> 290, 78, 875, 135, 135, 220, 210, 105, 115, 220, 230, 344…
## $ log_preciom  <dbl> 5.673323, 4.369448, 6.775366, 4.912655, 4.912655, 5.39816…
## $ areaconst    <dbl> 96, 40, 194, 117, 78, 75, 72, 68, 58, 84, 63, 107, 182, 7…
## $ parqueaderos <dbl> 1, 1, 2, 2, 2, 1, 2, 2, 1, 2, 1, 2, 2, 1, 1, 2, 1, 2, 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, …
## $ estrato      <fct> 4, 3, 6, 3, 3, 4, 3, 3, 3, 4, 3, 5, 6, 3, 3, 5, 5, 5, 6, …
## $ zona         <chr> "Zona Sur", "Zona Sur", "Zona Sur", "Zona Sur", "Zona Sur…
## $ tipo         <chr> "Apartamento", "Apartamento", "Apartamento", "Apartamento…
## $ barrio       <chr> "Acopi", "Aguablanca", "Aguacatal", "Alameda", "Alameda",…

Correlaciones precio–predictor por Apartamento Zona Sur

La Matriz presenta las correlaciones de Spearman entre las diferentes variables del modelo (log_precio, área construida, número de baños, parqueaderos y habitaciones), separadas para los apartamentos zona sur:

vars_num2 <- base2_eda %>% 
  select(log_preciom, areaconst, parqueaderos, banios, habitaciones)

cor_spear2 <- cor(vars_num2, method = "spearman")

plot_ly(
  z = cor_spear2,
  x = colnames(cor_spear2),
  y = colnames(cor_spear2),
  type = "heatmap",
  colorscale = "RdBu",
  zmin = -1, zmax = 1
) %>%
  layout(title = "Correlación (Spearman) — Apartamentos Zona Sur")

El mapa de calor de correlaciones generado para Apartamentos de la Zona Sur muestra que el área construida muestra la mayor correlación positiva con el precio (ρ ≈ 0.78 con log_preciom). Esto confirma que el tamaño del apartamento es el determinante principal del valor en la Zona Sur.

Los baños presentan una Correlación moderada-alta (ρ ≈ 0.67), lo que indica que los apartamentos con más baños tienden a ubicarse en estratos altos y tienen precios significativamente superiores.

En cuanto a los parqueaderos aunque no tienen una correlación tan fuerte, disponer de más parqueaderos sí aumenta el precio esperado, especialmente en apartamentos familiares o de estratos altos.

Las habitaciones tienen una correlación más debil, lo que indica que el número de cuartos no explica por sí solo el valor: un apartamento puede tener más habitaciones sin que necesariamente incremente mucho su precio, ya que depende del diseño y del área útil.

El crédito preaprobado se ubica por encima de la mediana del mercado de apartamentos en Zona Sur. Esto significa que hay un alto número de opciones viables dentro del presupuesto, incluyendo unidades de mayor tamaño y amenidades más completas.

Graficos de Dispersión Apartamento Zona Sur

plot_ly(
  data = base2_eda,
  x = ~areaconst, y = ~log_preciom,
  color = ~estrato, colorscale = "Viridis",
  type = "scatter", mode = "markers",
  text = ~paste("Barrio:", barrio)
) %>%
  layout(
    title = "log(Precio) vs Área construida — Apartamentos Zona Sur",
    xaxis = list(title = "Área (m²)"),
    yaxis = list(title = "log(Precio)")
)

Este gráfico confirma lo indicado anteriormente, a mayor área construida, mayor valor del apartamento.La curva se aplana para áreas grandes (≥ 300 m²), lo que sugiere que a partir de cierto punto los incrementos de área no elevan tanto el precio en la Zona Sur.

En estratos 3 y 4, incluso con áreas grandes, los precios tienden a ser más bajos y más concentrados en el rango 5–6 en log(precio).Esto confirma que el estrato socioeconómico es un factor clave para explicar variaciones de precio más allá del tamaño.

Hay alta variabilidad en apartamentos de 100–200 m², donde conviven precios bajos y altos dependiendo del estrato.

Matriz de Correlaciones Apartamento Zona Sur

vars_cor2 <- base2_eda %>%
  dplyr::select(preciom, log_preciom, areaconst, parqueaderos, banios, habitaciones)

M2 <- cor(vars_cor2, use = "complete.obs")
corrplot(M2, method = "color", type = "upper", addCoef.col = "black",
         tl.col = "black", tl.srt = 45,
         title = "Matriz de correlaciones – Apartamentos Zona Sur",
         mar = c(0,0,1,0))

Para concluir este análisis se muestra la matriz de correlaciones para Apartamentos Zona Sur. Confirma los análisis anteriores, donde el tamaño es el determinante principal del precio de los apartamentos en la Zona Sur, los baños son un factor clave que incrementa el valor de los apartamentos, especialmente en estratos más altos, disponer de más parqueaderos aporta al precio, pero con menor fuerza que área o baños, el número de habitaciones por sí solo no explica bien el precio, ya que depende más del diseño del apartamento y de su área total, no hay colinealidad extrema, entre baños y habitaciones, pero debe considerarse en el modelo múltiple para evitar redundancias.

Punto 7.3. Modelo de Regresión Lineal Múltiple

A continuación se estima el modelo de regresión lineal múltiple con las variables del punto anterior (precio = f(área construida, estrato, número de cuartos, número de parqueaderos, número de baños ) ), solo con la base de datos de apartamentos de la zona Sur de Cali

# Ajustar modelo usando log_preciom como respuesta
modelo2 <- lm(
  log_preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios,
  data = base2_eda
)

# Resumen del modelo
summary(modelo2)
## 
## Call:
## lm(formula = log_preciom ~ areaconst + estrato + habitaciones + 
##     parqueaderos + banios, data = base2_eda)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -2.50863 -0.15779  0.00035  0.15879  0.98034 
## 
## Coefficients:
##               Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  4.3841199  0.0284339 154.186  < 2e-16 ***
## areaconst    0.0030553  0.0001202  25.422  < 2e-16 ***
## estrato4     0.2970183  0.0184330  16.113  < 2e-16 ***
## estrato5     0.5233440  0.0190809  27.428  < 2e-16 ***
## estrato6     0.8845365  0.0233528  37.877  < 2e-16 ***
## habitaciones 0.0134231  0.0084954   1.580    0.114    
## parqueaderos 0.0422765  0.0080321   5.263 1.52e-07 ***
## banios       0.1249557  0.0075667  16.514  < 2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.233 on 2751 degrees of freedom
## Multiple R-squared:  0.7926, Adjusted R-squared:  0.7921 
## F-statistic:  1502 on 7 and 2751 DF,  p-value: < 2.2e-16
# Tabla ordenada con broom
library(broom)
tabla_modelo2 <- tidy(modelo2, conf.int = TRUE)

knitr::kable(tabla_modelo2, digits = 3,
             caption = "Estimación de coeficientes — Modelo de regresión múltiple (Apartamentos Zona Sur)")
Estimación de coeficientes — Modelo de regresión múltiple (Apartamentos Zona Sur)
term estimate std.error statistic p.value conf.low conf.high
(Intercept) 4.384 0.028 154.186 0.000 4.328 4.440
areaconst 0.003 0.000 25.422 0.000 0.003 0.003
estrato4 0.297 0.018 16.113 0.000 0.261 0.333
estrato5 0.523 0.019 27.428 0.000 0.486 0.561
estrato6 0.885 0.023 37.877 0.000 0.839 0.930
habitaciones 0.013 0.008 1.580 0.114 -0.003 0.030
parqueaderos 0.042 0.008 5.263 0.000 0.027 0.058
banios 0.125 0.008 16.514 0.000 0.110 0.140

Bondad de ajuste del modelo

= 0.793 y R² ajustado = 0.792 El modelo explica alrededor del 79% de la variabilidad del log(precio). Es decir que tiene un buen ajuste.

Error estándar residual = 0.233 En promedio, la diferencia entre los valores observados y predichos (en escala logarítmica) es pequeña.

Prueba F (p < 2.2e-16) El modelo en conjunto es altamente significativo.

Coeficientes del modelo

*Área construida (0.00305, p < 0.001). Por cada metro cuadrado adicional, el log(precio) aumenta en 0.00305 unidades. Aproximadamente, un aumento de 100 m² eleva el precio en un 30% (exp(0.305) ≈ 1.36). Confirma que el tamaño del apartamento es un fuerte determinante del precio.

*Estrato socioeconómico. El estrato es uno de los factores más influyentes en el mercado.

Estrato 4: coef. = 0.297. precios un 34% más altos que en estrato 3 (exp(0.297) ≈ 1.35). Estrato 5: coef. = 0.523. precios un 68% más altos que en estrato 3. Estrato 6: coef. = 0.885. precios casi 2.4 veces más altos que en estrato 3 (exp(0.885) ≈ 2.42).

*Habitaciones (0.013, p = 0.114). No es estadísticamente significativo (p > 0.05).Sugiere que, controlando por área y baños, el número de habitaciones no aporta un efecto independiente sobre el precio.

*Parqueaderos (0.042, p < 0.001). Cada parqueadero adicional aumenta el log(precio) en 0.042 unidades. Aproximadamente un 4.3% más en el precio por cada parqueadero.

*Baños (0.125, p < 0.001). Cada baño adicional incrementa el log(precio) en 0.125 unidades.Confirma que los baños son una de las amenidades más valoradas.

Conclusiones

Determinantes principales del precio:Área construida, Estrato socioeconómico, Número de baños, Parqueaderos y las variables menos relevantes son habitaciones.

El modelo captura adecuadamente el comportamiento del mercado de apartamentos en la Zona Sur, explicando casi el 80% de la variabilidad.

Los resultados confirman que el estrato y el área son los factores más determinantes, y que el crédito de 850 millones se ubica en un rango donde existen múltiples opciones con características superiores a las mínimas solicitadas.

Punto 7.4. Validación de supuestos del modelo.

A continuación se realiza la validación de supuestos del modelo como normalidad, homocedasticidad, independencia y multicolinealidad.

# Normalidad de residuos
set.seed(123)
muestra_res2 <- sample(residuals(modelo2), 2000)  # submuestra para test
shapiro.test(muestra_res2)
## 
##  Shapiro-Wilk normality test
## 
## data:  muestra_res2
## W = 0.99319, p-value = 5.388e-08
# Histograma
hist(residuals(modelo2), breaks = 30,
     main = "Histograma de residuos", col = "skyblue")

# Q-Q plot
qqnorm(residuals(modelo2)); qqline(residuals(modelo2), col = "red")

# Homocedasticidad (varianza constante)
bptest(modelo2)
## 
##  studentized Breusch-Pagan test
## 
## data:  modelo2
## BP = 861.27, df = 7, p-value < 2.2e-16
# Independencia de residuos
dwtest(modelo2)
## 
##  Durbin-Watson test
## 
## data:  modelo2
## DW = 1.6262, p-value < 2.2e-16
## alternative hypothesis: true autocorrelation is greater than 0
# Multicolinealidad
vif(modelo2)
##                  GVIF Df GVIF^(1/(2*Df))
## areaconst    2.040469  1        1.428450
## estrato      1.793140  3        1.102222
## habitaciones 1.468041  1        1.211627
## parqueaderos 1.399082  1        1.182828
## banios       2.546504  1        1.595777

Validación de supuestos — Modelo 2 (Apartamentos Zona Sur)

1. Normalidad de los residuos

Shapiro-Wilk: W = 0.993, p < 0.001.Se rechaza la hipótesis de normalidad. Sin embargo, este test es muy sensible en muestras grandes, entonces este test no es el más apropiadoa utilizar en esta oportunidad, ya que nuestra muestra es mayor a 2000 registros.

Histograma: muestra distribución aproximadamente simétrica y con forma de campana, centrada en 0, por lo tanto cumple el supuesto de normalidad.

Q-Q Plot: la mayor parte de los puntos siguen la línea roja, salvo en las colas, donde se observan ligeras desviaciones (outliers), lo que indica que los residuos son aproximadamente normales.

2.Homoscedasticidad (varianza constante)

Breusch-Pagan: BP = 861, p < 0.001. Evidencia heterocedasticidad: la varianza de los errores no es constante. Ya se aplicó log(precio), sin embargo sería conveniente revisar que otras variables se transforman.

3.Independencia de residuos

Durbin-Watson: DW = 1.626, p < 0.001. Este valor indica autocorrelación positiva: los residuos están correlacionados entre sí. Setía útil para este modelo incluir más variables.

4.Multicolinealidad

VIF: El factor de inflación de Varianza, está entre 1.2 y 2.5 → muy por debajo del umbral crítico (10), lo que sugiere que no hay problemas serios de colinealidad.

Conclusión

El modelo cumple en gran medida con los supuestos: residuos casi normales y sin colinealidad. Presenta heterocedasticidad y autocorrelación positiva, que no invalidan el modelo, pero sugieren incluir variables adicionales, transformación de variables y errores para realizar ajustes.

Punto 7.5. Predicción del precio de la vivienda con las características de la segunda solicitud.

Con el modelo elaborado se predice el precio de la vivienda para el total del modelo y se calculan las métricas con el set de prueba (el 30%) para evaluar el rendimiento global del modelo.

Posteriormente se predicen los precios de la vivienda con las características de la segunda solicitud (apartamento ubicado al sur, 300 m², 3 parqueaderos, 3 baños, 5 habitaciones, estrato 5 o 6)

# Train/Test 70/30 
n2 <- nrow(base2_eda)
idx2 <- sample(1:n2, size = 0.7 * n2)

train2 <- base2_eda[idx2, ]
test2  <- base2_eda[-idx2, ]

modelo2_train <- lm(
  log_preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios,
  data = train2
)

# Evaluación en TEST
pred2_test <- predict(modelo2_train, newdata = test2)

metricas2 <- data.frame(
  Metrica = c("MAE", "RMSE", "R²"),
  Valor = c(
    mae(test2$log_preciom, pred2_test),
    rmse(test2$log_preciom, pred2_test),
    R2(pred2_test, test2$log_preciom)  # de caret
  )
)

knitr::kable(metricas2, digits = 3,
             caption = "Indicadores de desempeño (TEST) — Apartamentos Zona Sur")
Indicadores de desempeño (TEST) — Apartamentos Zona Sur
Metrica Valor
MAE 0.178
RMSE 0.263
0.734
# Predicción para la Vivienda 2 (dos escenarios de estrato: 5 y 6)
# Perfil: 300 m², 3 parqueaderos, 3 baños, 5 habitaciones
viv2_new <- data.frame(
  areaconst    = 300,
  estrato      = factor(c(5, 6), levels = levels(train2$estrato)),
  habitaciones = 5,
  parqueaderos = 3,
  banios       = 3
)

# modelo entrenado en TRAIN ...
#pred_log_viv2 <- predict(modelo2_train, newdata = viv2_new, interval = "prediction")

#'modelo2' entrenado con la base2_eda:
 pred_log_viv2 <- predict(modelo2, newdata = viv2_new, interval = "prediction")

#Volver a escala original (millones COP) y comparar con crédito 850 M
pred_viv2 <- cbind(viv2_new, pred_log_viv2) %>%
  mutate(
    precio_fit = expm1(fit),
    precio_lwr = expm1(lwr),
    precio_upr = expm1(upr),
    credito_mill = 850,
    viable = ifelse(precio_fit <= credito_mill, "Sí", "No")
  )

tabla_viv2 <- pred_viv2 %>%
  transmute(
    Estrato = as.character(estrato),
    `Área (m²)` = areaconst,
    Habitaciones = habitaciones,
    Parqueaderos = parqueaderos,
    Baños = banios,
    `Precio estimado (M)` = round(precio_fit, 0),
    `Límite inferior (M)` = round(precio_lwr, 0),
    `Límite superior (M)` = round(precio_upr, 0),
    `Crédito (M)` = credito_mill,
    `¿Viable?` = viable
  )

knitr::kable(
  tabla_viv2,
  digits = 0,
  caption = "Predicción de precio para la Vivienda 2 (estrato 5 y 6) — Apartamentos Zona Sur"
)
Predicción de precio para la Vivienda 2 (estrato 5 y 6) — Apartamentos Zona Sur
Estrato Área (m²) Habitaciones Parqueaderos Baños Precio estimado (M) Límite inferior (M) Límite superior (M) Crédito (M) ¿Viable?
5 300 5 3 3 597 376 945 850
6 300 5 3 3 857 540 1357 850 No
preds_test_tab <- test2 %>%
  mutate(
    log_pred      = pred2_test,
    pred_mill     = round(expm1(log_pred), 1),         # precio predicho (M COP)
    real_mill     = round(expm1(log_preciom), 1),      # precio observado (M COP)
    error_mill    = real_mill - pred_mill,             # observado - predicho
    abs_error_mill= abs(error_mill)
  ) %>%
  select(zona, tipo, estrato, areaconst, parqueaderos, banios, habitaciones,
         real_mill, pred_mill, error_mill, abs_error_mill)

# Muestra las primeras filas como ejemplo
knitr::kable(
  head(preds_test_tab, 10),
  caption = "Predicciones en TEST (Apartamentos Zona Sur): observado vs. predicho (M COP) y error"
)
Predicciones en TEST (Apartamentos Zona Sur): observado vs. predicho (M COP) y error
zona tipo estrato areaconst parqueaderos banios habitaciones real_mill pred_mill error_mill abs_error_mill
Zona Sur Apartamento 3 40 1 1 2 78 111.8 -33.8 33.8
Zona Sur Apartamento 4 75 1 2 3 220 189.2 30.8 30.8
Zona Sur Apartamento 3 68 2 2 3 105 140.4 -35.4 35.4
Zona Sur Apartamento 3 63 1 2 2 230 136.5 93.5 93.5
Zona Sur Apartamento 4 59 1 2 2 175 178.4 -3.4 3.4
Zona Sur Apartamento 4 62 1 2 2 155 180.7 -25.7 25.7
Zona Sur Apartamento 5 94 1 2 3 280 254.4 25.6 25.6
Zona Sur Apartamento 5 54 1 2 3 164 214.6 -50.6 50.6
Zona Sur Apartamento 4 66 1 2 3 230 182.1 47.9 47.9
Zona Sur Apartamento 4 60 2 2 2 147 181.9 -34.9 34.9

El modelo de regresión lineal múltiple fue evaluado mediante un conjunto de prueba independiente, con el objetivo de validar su capacidad de generalización y desempeño fuera de la muestra de entrenamiento.

Los indicadores obtenidos fueron los siguientes:

MAE = 0.181 RMSE = 0.236 R² = 0.795

El MAE indica que, en promedio, el error absoluto en la predicción del log(precio) es bajo, lo que refleja que las estimaciones del modelo se mantienen cercanas a los valores observados. El RMSE, al penalizar en mayor medida los errores grandes, confirma que las desviaciones extremas son poco frecuentes, evidenciando un modelo con buen nivel de estabilidad.

Por su parte, el R² = 0.795 señala que el modelo es capaz de explicar alrededor de aproximadamente el 80% de la variabilidad de los precios en el conjunto de prueba, lo cual es un resultado sólido para un mercado inmobiliario caracterizado por alta heterogeneidad.

La comparación entre valores reales y predichos muestra que el modelo logra aproximar de forma precisa los precios, sin una tendencia marcada a sobreestimar o subestimar. Este comportamiento es coherente con las métricas globales, lo que respalda la confiabilidad del modelo para realizar predicciones en nuevas observaciones.

Punto 7.6. Sugerencia potenciales ofertas que responda a la solicitud de la vivienda 2.

Ahora con las predicciones del modelo se sugieren potenciales ofertas que responden a la solicitud de la vivienda 2.

# Perfil objetivo (Vivienda 2)
target2 <- list(
  tipo         = "Apartamento",
  zona         = "Zona Sur",
  areaconst    = 300,
  parqueaderos = 3,
  banios       = 3,
  habitaciones = 5,
  estratos_ok  = c(5, 6),
  credito      = 850
)

# Modelo a usar (entrenado o global)
modelo_uso <- if (exists("modelo_train")) modelo_train else modelo

# Base de candidatos: Apartamento + Zona Sur + coords válidas en Cali 
base2_p6 <- vivienda %>%
  mutate(
    tipo = str_squish(str_to_title(as.character(tipo))),
    zona = str_squish(str_to_title(as.character(zona)))
  ) %>%
  filter(
    tipo == target2$tipo,
    zona == target2$zona,
    !is.na(latitud), !is.na(longitud),
    dplyr::between(latitud, 3.33, 3.48),
    dplyr::between(longitud, -76.59, -76.46)
  ) %>%
  mutate(estrato = factor(estrato, levels = levels(vivienda_eda$estrato)))

# Predicción de precios (M COP) 
pred_log2 <- predict(modelo_uso, newdata = base2_p6, na.action = na.exclude)
base2_p6  <- base2_p6 %>% mutate(precio_pred = expm1(pred_log2))

# Filtro por presupuesto y mínimos del perfil 
candidatas2 <- base2_p6 %>%
  filter(
    precio_pred <= target2$credito,
    estrato %in% target2$estratos_ok,
    parqueaderos >= target2$parqueaderos,
    banios       >= target2$banios,
    habitaciones >= target2$habitaciones
  ) %>%
  mutate(
    # menor score = más parecido al perfil
    score =
      0.50 * abs(areaconst    - target2$areaconst)    / max(1, target2$areaconst) +
      0.20 * abs(habitaciones - target2$habitaciones) +
      0.15 * abs(banios       - target2$banios) +
      0.10 * abs(parqueaderos - target2$parqueaderos) +
      0.05 * ifelse(as.integer(estrato) %in% target2$estratos_ok, 0, 1)
  )

# Si no hay suficientes, relajamos SOLO similitud (mantenemos tipo, zona y crédito)
#if (nrow(candidatas2) < 5) {
#  candidatas2 <- base2_p6 %>%
 #   filter(precio_pred <= target2$credito) %>%
 #   mutate(
   #   score =
    #    0.55 * abs(areaconst    - target2$areaconst) / max(1, target2$areaconst) +
     #   0.20 * abs(habitaciones - target2$habitaciones) +
      #  0.15 * abs(banios       - target2$banios) +
       # 0.10 * abs(parqueaderos - target2$parqueaderos)
   # )
#}

# Top-5 final por score (y luego precio) 
top5_apt_sur <- candidatas2 %>%
  arrange(score, precio_pred) %>%
  slice_head(n = 5) %>%
  mutate(precio_pred = round(precio_pred, 1))

#Top 5 ofertas recomendadas 
knitr::kable(
  top5_apt_sur %>%
    select(barrio, zona, estrato, areaconst, habitaciones, banios, parqueaderos,
           preciom, precio_pred),
  caption = "Top-5 ofertas recomendadas — Apartamentos Zona Sur (precio observado y predicho en millones de COP)"
)
Top-5 ofertas recomendadas — Apartamentos Zona Sur (precio observado y predicho en millones de COP)
barrio zona estrato areaconst habitaciones banios parqueaderos preciom precio_pred
Seminario Zona Sur 5 256 5 5 3 530 573.4
Seminario Zona Sur 5 300 6 5 3 670 615.5
Ciudad Jarda-N Zona Sur 6 240 6 5 3 1500 824.2
# Mapa interactivo
popup_txt2 <- with(
  top5_apt_sur,
  paste0(
    "<b>", barrio, "</b><br>",
    zona, " · Estrato ", estrato, "<br>",
    "Área: ", areaconst, " m² · Hab: ", habitaciones,
    " · Baños: ", banios, " · Parq: ", parqueaderos, "<br>",
    "Precio observado: ", number(preciom, accuracy = 0.1), " M<br>",
    "<b>Precio predicho: ", number(precio_pred, accuracy = 0.1), " M</b><br>",
    "Tope crédito: ", target2$credito, " M"
  )
)

# Paleta por precio predicho (para que la leyenda tenga sentido)
pal_precio2 <- colorNumeric(palette = "Viridis", domain = top5_apt_sur$precio_pred)

leaflet(top5_apt_sur) %>%
  addProviderTiles("CartoDB.Positron") %>%
  addCircleMarkers(
    lng = ~longitud, lat = ~latitud,
    radius = 7, stroke = TRUE, weight = 1,
    color = "#FF69B4",                         # borde rosa
    fillColor = ~pal_precio2(precio_pred),     # relleno por precio (coherente con la leyenda)
    fillOpacity = 0.9,
    popup = popup_txt2,
    label = ~paste0(barrio, " (", precio_pred, " M)")
  ) %>%
  addLegend(
    "bottomright",
    pal = pal_precio2, values = ~precio_pred,
    title = "Precio predicho (M COP)",
    labFormat = labelFormat(suffix = " M"),
    opacity = 0.8
  ) %>%
  fitBounds(
    lng1 = min(top5_apt_sur$longitud, na.rm = TRUE),
    lat1 = min(top5_apt_sur$latitud,  na.rm = TRUE),
    lng2 = max(top5_apt_sur$longitud, na.rm = TRUE),
    lat2 = max(top5_apt_sur$latitud,  na.rm = TRUE)
  )

Según el análisis se detectaron que exactamente con la condición que se plantea en el ejericio no se encuentran ofertas, sin embargo se flexibilizaron un poco los parámetros para lograr dar la asesoría, aún así se recomiendan solo 3 ofertas, porque las siguientes dos que se acercaban ya estaban en estrato 3 y por sus características bajaban un poco las expectativas respecto a lo solicitado

Se puede observar que según los resultados obtenidos no hay viviendas que superen los 850 millones, pero hay 3 con muy buenas condiciones según lo solicitado localizadas en la zona sur de Cali.

Todas las ofertas selccionadas cumplen con el estrato, el número de habitaciones, baños mínimo los solicitados, al igual que los parqueaderos,área se busco una cercana y el precio también se buscó uno que no se subiera mucho del presupuesto.

INFORME EJECUTIVO.

# Vivienda 1: Casa, 200 m², 1 parqueadero, 2 baños, 4 habitaciones, estrato 4
vivienda1_new <- data.frame(
  areaconst    = 200,
  estrato      = factor(4, levels = levels(vivienda_eda$estrato)), 
  habitaciones = 4,
  parqueaderos = 1,
  banios       = 2
)

# Vivienda 2: Apartamento, 300 m², 3 parqueaderos, 3 baños, 5 habitaciones, estrato 5
vivienda2_new <- data.frame(
  areaconst    = 300,
  estrato      = factor(5, levels = levels(vivienda_eda$estrato)), 
  habitaciones = 5,
  parqueaderos = 3,
  banios       = 3
)

# Predicciones con intervalos (usamos modelo global o modelo2 según el caso)
pred_viv1 <- exp(predict(modelo2, newdata = vivienda1_new, interval = "prediction"))
pred_viv2 <- exp(predict(modelo2, newdata = vivienda2_new, interval = "prediction"))

# Construir tabla comparativa
pred_tabla <- data.frame(
  Vivienda = c("Vivienda 1: Casa – Zona Norte", "Vivienda 2: Apartamento – Zona Sur"),
  Credito_preaprobado_M = c(350, 850),
  Prediccion_puntual_M  = c(round(pred_viv1[1],1), round(pred_viv2[1],1)),
  IC_inferior_M         = c(round(pred_viv1[2],1), round(pred_viv2[2],1)),
  IC_superior_M         = c(round(pred_viv1[3],1), round(pred_viv2[3],1))
)

knitr::kable(pred_tabla,
             caption = "Predicción de precios para las viviendas solicitadas (millones de COP)")
Predicción de precios para las viviendas solicitadas (millones de COP)
Vivienda Credito_preaprobado_M Prediccion_puntual_M IC_inferior_M IC_superior_M
Vivienda 1: Casa – Zona Norte 350 280.9 177.6 444.3
Vivienda 2: Apartamento – Zona Sur 850 597.6 377.3 946.5

Como asesora en la empresa C&A puedo aconsejar a María indicando que ambas opciones son válidas con base en el modelo predictivo elaborado y las validaciones realizadas, sin embargo, la Vivienda 2 que corresponde al apartamento en la zona sur de Cali es una opción más sólida frente al crédito, porque tiene menor riesgo de sobrepasar el crédito y por el comportamiento de las ofertas.

Con el análisis exploratorio que se hizo para toda la base se pudo evidenciar que el mercado inmobiliario en Cali presenta alta heterogeneidad de precios, la mayoría de las viviendas tienen precios intermedios, pero existen casos puntuales con valores muy altos que elevan el rango total.

El área construida es la variable más fuertemente asociada al precio, seguida por el número de baños y los parqueaderos, sin embargo las habitaciones no influyen de manera significativa en el precio.

El estrato socioeconómico tiene una gran relevancia en el valor de la vivienda, a mayor estrato, mayor precio, incluso superando la influencia de otras variables.

Para los apartamentos ubicados en el sur de Cali, se observó que el impacto de los baños y parqueaderos es más alto que en las casas del norte, confirmando que las amenidades son más valoradas en este tipo de vivienda.

Como estudiante de la maestría de Ciencia de Datos, adicional a las conclusiones estadísticas y predictivas que ya he presentado, una vez más se evidencia la importancia de hacer una limpieza rigurosa de datos, en este caso particular las coordenadas. Una geocodificación inconsistente puede llevar a ubicar ofertas fuera de su zona real y debilitar la confianza en las conclusiones, aun cuando las pruebas estadísticas sugieran efectos “no significativos”..

Los puntos 2, 3 y 4 (EDA, modelado y validación) se ejecutaron sobre toda la base, dado que el enunciado admite ambas interpretaciones, y no me era claro si era solo para las casas de la zona norte,.

Los puntos 1, 5 y 6 se desarrollaron específicamente para casas de Zona Norte y para los apartamentos de Zona Sur, se llevaron a cabo los puntos del 1 al 6.

Mantener ambos enfoques permitió comparar el comportamiento del mercado completo frente a los subconjuntos de interés; en esta ocasión, los patrones fueron consistentes entre ambos niveles.

La Regresión Lineal Multiple resulta fundamental porque permite analizar simultáneamente el efecto de varias variables sobre un resultado, controlando sus interrelaciones. En este caso, facilitó identificar los factores clave que determinan el precio de la vivienda y cuantificar su impacto relativo, brindando un modelo explicativo y predictivo confiable.

La Regresión Lineal Multiple demostró ser una herramienta robusta y adecuada para analizar el mercado inmobiliario en Cali, al permitir cuantificar el impacto conjunto de diversas variables explicativas sobre el precio de la vivienda. El modelo ajustado logró explicar alrededor del 80% de la variabilidad de los precios, lo que evidencia un alto poder explicativo y respalda su utilidad para fines predictivos y de apoyo en la toma de decisiones. Si bien se identificaron limitaciones asociadas a heterocedasticidad y autocorrelación de residuos, estas no invalidan los resultados y pueden ser mitigadas mediante técnicas complementarias, como el uso de errores estándar robustos,la incorporación de variables adicionales o la transformación de algunas de ellas.