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 |
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"
Para empezar se carga la base de datos desde el paquete MODELOS, suministrada en el ejercicio:
##
## ── 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'
##
##
## 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")| 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")| 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 |
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")| 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.
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.
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.
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
}
}
}## [1] 0
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
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
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|
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")| 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.
# 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
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'")| tipo | n |
|---|---|
| Apartamento | 5061 |
| Casa | 3200 |
| zona | n |
|---|---|
| Zona Sur | 4684 |
| Zona Norte | 1908 |
| Zona Oeste | 1195 |
| Zona Oriente | 350 |
| Zona Centro | 124 |
| 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 |
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)")| 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)")| ns_etq | Zona Norte | Zona Sur |
|---|---|---|
| Zona Norte | 1659 | 249 |
| Zona Sur | 460 | 4224 |
| 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%
## 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")| 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)")| 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.
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.
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
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)"
)| 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")| 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")| 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")| 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.
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.
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.
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_corEl á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.
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")| 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.
# 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_facetLos 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.
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_estratofig_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_zonaEn 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.
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_baniosExiste 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.
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.
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")| 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.
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")##
## studentized Breusch-Pagan test
##
## data: modelo
## BP = 1412.7, df = 7, p-value < 2.2e-16
##
## Durbin-Watson test
##
## data: modelo
## DW = 1.6166, p-value < 2.2e-16
## alternative hypothesis: true autocorrelation is greater than 0
## 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.
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")| Metrica | Valor |
|---|---|
| MAE | 0.230 |
| RMSE | 0.295 |
| R² | 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)")| 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 | Sí |
| 5 | 200 | 4 | 1 | 2 | 321 | 177 | 583 | 350 | Sí |
# 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"
)| 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).
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)"
)| 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.
# 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)"
)| 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")| 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")| 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")| 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.
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.
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",…
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.
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.
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.
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)")| 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
R² = 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.
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")##
## studentized Breusch-Pagan test
##
## data: modelo2
## BP = 861.27, df = 7, p-value < 2.2e-16
##
## Durbin-Watson test
##
## data: modelo2
## DW = 1.6262, p-value < 2.2e-16
## alternative hypothesis: true autocorrelation is greater than 0
## 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.
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")| Metrica | Valor |
|---|---|
| MAE | 0.178 |
| RMSE | 0.263 |
| R² | 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"
)| 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 | Sí |
| 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"
)| 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.
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)"
)| 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.
# 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)")| 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.