Se inicia la actividad cargando el dataset vivienda desde el paquete paqueteMODELOS, el cual contiene información de viviendas en la ciudad de Cali, incluyendo ubicación, características físicas y precio de venta en millones de pesos.
También se cargan los paquetes necesarios para realizar el análisis exploratorio, modelado estadístico, visualizaciones interactivas y validación de supuestos.
Como siguiente paso se valida la estructura de la base de datos para identificar los tipos de variables disponibles.
library(dplyr)
library(plotly)
library(leaflet)
library(kableExtra)
library(tidyr)
library(naniar)
library(lmtest)
Cargar Datos
# Cargar datos
devtools::install_github("centromagis/paqueteMODELOS", force =TRUE)
── R CMD build ─────────────────────────────────────────────────────────────────
✔ checking for file 'C:\Users\User\AppData\Local\Temp\RtmpSgvWZB\remotes618410987368\Centromagis-paqueteMODELOS-3b06257/DESCRIPTION'
─ preparing 'paqueteMODELOS':
checking DESCRIPTION meta-information ... ✔ checking DESCRIPTION meta-information
─ checking for LF line-endings in source and make files and shell scripts (404ms)
─ checking for empty or unneeded directories
─ building 'paqueteMODELOS_0.1.0.tar.gz'
library(paqueteMODELOS)
data("vivienda")
str(vivienda) # Estructura del data.frame base de R
spc_tbl_ [8,322 × 13] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
$ id : num [1:8322] 1147 1169 1350 5992 1212 ...
$ zona : chr [1:8322] "Zona Oriente" "Zona Oriente" "Zona Oriente" "Zona Sur" ...
$ piso : chr [1:8322] NA NA NA "02" ...
$ estrato : num [1:8322] 3 3 3 4 5 5 4 5 5 5 ...
$ preciom : num [1:8322] 250 320 350 400 260 240 220 310 320 780 ...
$ areaconst : num [1:8322] 70 120 220 280 90 87 52 137 150 380 ...
$ parqueaderos: num [1:8322] 1 1 2 3 1 1 2 2 2 2 ...
$ banios : num [1:8322] 3 2 2 5 2 3 2 3 4 3 ...
$ habitaciones: num [1:8322] 6 3 4 3 3 3 3 4 6 3 ...
$ tipo : chr [1:8322] "Casa" "Casa" "Casa" "Casa" ...
$ barrio : chr [1:8322] "20 de julio" "20 de julio" "20 de julio" "3 de julio" ...
$ longitud : num [1:8322] -76.5 -76.5 -76.5 -76.5 -76.5 ...
$ latitud : num [1:8322] 3.43 3.43 3.44 3.44 3.46 ...
- attr(*, "spec")=List of 3
..$ cols :List of 13
.. ..$ id : list()
.. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
.. ..$ zona : list()
.. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
.. ..$ piso : list()
.. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
.. ..$ estrato : list()
.. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
.. ..$ preciom : list()
.. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
.. ..$ areaconst : list()
.. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
.. ..$ parqueaderos: list()
.. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
.. ..$ banios : list()
.. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
.. ..$ habitaciones: list()
.. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
.. ..$ tipo : list()
.. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
.. ..$ barrio : list()
.. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
.. ..$ longitud : list()
.. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
.. ..$ latitud : list()
.. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
..$ default: list()
.. ..- attr(*, "class")= chr [1:2] "collector_guess" "collector"
..$ delim : chr ";"
..- attr(*, "class")= chr "col_spec"
- attr(*, "problems")=<externalptr>
summary(vivienda) # Resumen estadístico de todas las columnas
id zona piso estrato
Min. : 1 Length:8322 Length:8322 Min. :3.000
1st Qu.:2080 Class :character Class :character 1st Qu.:4.000
Median :4160 Mode :character Mode :character Median :5.000
Mean :4160 Mean :4.634
3rd Qu.:6240 3rd Qu.:5.000
Max. :8319 Max. :6.000
NA's :3 NA's :3
preciom areaconst parqueaderos banios
Min. : 58.0 Min. : 30.0 Min. : 1.000 Min. : 0.000
1st Qu.: 220.0 1st Qu.: 80.0 1st Qu.: 1.000 1st Qu.: 2.000
Median : 330.0 Median : 123.0 Median : 2.000 Median : 3.000
Mean : 433.9 Mean : 174.9 Mean : 1.835 Mean : 3.111
3rd Qu.: 540.0 3rd Qu.: 229.0 3rd Qu.: 2.000 3rd Qu.: 4.000
Max. :1999.0 Max. :1745.0 Max. :10.000 Max. :10.000
NA's :2 NA's :3 NA's :1605 NA's :3
habitaciones tipo barrio longitud
Min. : 0.000 Length:8322 Length:8322 Min. :-76.59
1st Qu.: 3.000 Class :character Class :character 1st Qu.:-76.54
Median : 3.000 Mode :character Mode :character Median :-76.53
Mean : 3.605 Mean :-76.53
3rd Qu.: 4.000 3rd Qu.:-76.52
Max. :10.000 Max. :-76.46
NA's :3 NA's :3
latitud
Min. :3.333
1st Qu.:3.381
Median :3.416
Mean :3.418
3rd Qu.:3.452
Max. :3.498
NA's :3
glimpse(vivienda)
Rows: 8,322
Columns: 13
$ id <dbl> 1147, 1169, 1350, 5992, 1212, 1724, 2326, 4386, 1209, 159…
$ zona <chr> "Zona Oriente", "Zona Oriente", "Zona Oriente", "Zona Sur…
$ piso <chr> NA, NA, NA, "02", "01", "01", "01", "01", "02", "02", "02…
$ estrato <dbl> 3, 3, 3, 4, 5, 5, 4, 5, 5, 5, 6, 4, 5, 6, 4, 5, 5, 4, 5, …
$ preciom <dbl> 250, 320, 350, 400, 260, 240, 220, 310, 320, 780, 750, 62…
$ areaconst <dbl> 70, 120, 220, 280, 90, 87, 52, 137, 150, 380, 445, 355, 2…
$ parqueaderos <dbl> 1, 1, 2, 3, 1, 1, 2, 2, 2, 2, NA, 3, 2, 2, 1, 4, 2, 2, 2,…
$ banios <dbl> 3, 2, 2, 5, 2, 3, 2, 3, 4, 3, 7, 5, 6, 2, 4, 4, 4, 3, 2, …
$ habitaciones <dbl> 6, 3, 4, 3, 3, 3, 3, 4, 6, 3, 6, 5, 6, 2, 5, 5, 4, 3, 3, …
$ tipo <chr> "Casa", "Casa", "Casa", "Casa", "Apartamento", "Apartamen…
$ barrio <chr> "20 de julio", "20 de julio", "20 de julio", "3 de julio"…
$ longitud <dbl> -76.51168, -76.51237, -76.51537, -76.54000, -76.51350, -7…
$ latitud <dbl> 3.43382, 3.43369, 3.43566, 3.43500, 3.45891, 3.36971, 3.4…
Se filtran de la base original solo las viviendas de tipo “Casa” ubicadas en la “Zona Norte” de Cali, obteniendo así un df de trabajo para el análisis de la primera solicitud de compra.
# Creamos subconjunto para Vivienda 1
base1 <- vivienda %>%
filter(tipo == "Casa", zona == "Zona Norte")
No. de viviendas en la base Vivienda 1 Zona Norte
# Verificamos cuántas viviendas hay en esta base
nrow(base1)
[1] 722
3 Primeros registros relevantees
# Mostramos los primeros 3 registros relevantes
base1 %>%
select(id, barrio, estrato, preciom, areaconst, parqueaderos, banios, habitaciones) %>%
head(3) %>%
kable("html", caption = "Primeros 3 registros: Casas en Zona Norte") %>%
kable_styling(full_width = FALSE, bootstrap_options = c("striped", "hover"))
| id | barrio | estrato | preciom | areaconst | parqueaderos | banios | habitaciones |
|---|---|---|---|---|---|---|---|
| 1209 | acopi | 5 | 320 | 150 | 2 | 4 | 6 |
| 1592 | acopi | 5 | 780 | 380 | 2 | 3 | 3 |
| 4057 | acopi | 6 | 750 | 445 | NA | 7 | 6 |
Las primeras observaciones muestran variabilidad en precios, áreas, y estratos,esto se debe tener en cuenta para el análisis.
Las tablas resumen permiten identificarque las casas en esta zona están distribuidas principalmente entre estratos 4, 5 y 6.
# Tabla: Conteo por estrato
base1 %>%
count(estrato) %>%
kable("html", col.names = c("Estrato", "Cantidad"), caption = "Cantidad de casas por estrato") %>%
kable_styling(full_width = FALSE)
| Estrato | Cantidad |
|---|---|
| 3 | 235 |
| 4 | 161 |
| 5 | 271 |
| 6 | 55 |
Conteo por barrio
# Tabla: Conteo por barrio
base1 %>%
count(barrio) %>%
arrange(desc(n)) %>%
kable("html", col.names = c("Barrio", "Cantidad"), caption = "Cantidad de casas por barrio en Zona Norte") %>%
kable_styling(full_width = FALSE)
| Barrio | Cantidad |
|---|---|
| la flora | 99 |
| acopi | 70 |
| villa del prado | 40 |
| el bosque | 37 |
| prados del norte | 31 |
| san vicente | 31 |
| vipasa | 30 |
| la merced | 24 |
| urbanización la flora | 23 |
| brisas de los | 22 |
| salomia | 20 |
| zona norte | 19 |
| santa monica | 16 |
| versalles | 16 |
| santa mónica residencial | 15 |
| Cali | 13 |
| los andes | 13 |
| villa del sol | 12 |
| ciudad los álamos | 11 |
| juanamb√∫ | 11 |
| granada | 10 |
| los guaduales | 10 |
| la rivera | 9 |
| villas de veracruz | 7 |
| calima | 6 |
| chipichape | 5 |
| popular | 5 |
| santa monica residencial | 5 |
| flora industrial | 4 |
| la campiña | 4 |
| urbanización la merced | 4 |
| villa de veracruz | 4 |
| alamos | 3 |
| barranquilla | 3 |
| calimio norte | 3 |
| centenario | 3 |
| floralia | 3 |
| las delicias | 3 |
| base aérea | 2 |
| la floresta | 2 |
| la rivera ii | 2 |
| las ceibas | 2 |
| los guayacanes | 2 |
| menga | 2 |
| pacara | 2 |
| paseo de los | 2 |
| paso del comercio | 2 |
| porvenir | 2 |
| san luis | 2 |
| torres de comfandi | 2 |
| urbanización barranquilla | 2 |
| La Flora | 1 |
| Santa Monica | 1 |
| Villa Del Prado | 1 |
| Villas De Veracruz | 1 |
| alameda del río | 1 |
| atanasio girardot | 1 |
| barrio tranquilo y | 1 |
| berlin | 1 |
| brisas del guabito | 1 |
| calibella | 1 |
| cambulos | 1 |
| chapinero | 1 |
| colinas del bosque | 1 |
| cristales | 1 |
| el cedro | 1 |
| el gran limonar | 1 |
| el guabito | 1 |
| el sena | 1 |
| el trébol | 1 |
| evaristo garcía | 1 |
| gaitan | 1 |
| jorge eliecer gaitán | 1 |
| la base | 1 |
| la esmeralda | 1 |
| la rivera i | 1 |
| la riviera | 1 |
| la villa del | 1 |
| las acacias | 1 |
| las américas | 1 |
| las granjas | 1 |
| manzanares | 1 |
| metropolitano del norte | 1 |
| nueva tequendama | 1 |
| oasis de comfandi | 1 |
| occidente | 1 |
| parque residencial el | 1 |
| poblado campestre | 1 |
| portada de comfandi | 1 |
| portales de comfandi | 1 |
| quintas de salomia | 1 |
| rozo la torre | 1 |
| san luís | 1 |
| santa bárbara | 1 |
| santa monica norte | 1 |
| santa mónica | 1 |
| santander | 1 |
| tejares de san | 1 |
| unión de vivienda | 1 |
| urbanización la nueva | 1 |
| valle del lili | 1 |
| villa colombia | 1 |
| zona oriente | 1 |
MAPA LEAFLET
leaflet(base1) %>%
addTiles() %>%
addCircleMarkers(
lng = ~longitud,
lat = ~latitud,
popup = ~paste("<b>Barrio:</b>", barrio,
"<br><b>Precio:</b>", preciom, "millones",
"<br><b>Área:</b>", areaconst, "m²",
"<br><b>Estrato:</b>", estrato),
color = "blue",
radius = 6,
stroke = FALSE,
fillOpacity = 0.8
) %>%
setView(lng = -76.52, lat = 3.45, zoom = 12)
En el mapa interactivo se muestran las ubicaciones geográficas de las viviendas disponibles. La mayoría están bien localizadas en el norte de la ciudad.
EDA Base1 vivienda Zona Norte
En esta fase se exploró la estructura de los datos y se identificaron valores faltantes o inconsistencias potenciales. Se observan algunos registros con precios muy bajos o áreas construidas extremadamente pequeñas, así como valores N` en variables clave como baños y parqueaderos.
Se decide eliminar únicamente aquellos registros que representen errores evidentes de codificación o información faltante en variables fundamentales para el análisis.
Esta limpieza garantiza que el modelo posterior no se vea afectado por datos irreales o poco confiables.
# Estructura general
glimpse(base1)
Rows: 722
Columns: 13
$ id <dbl> 1209, 1592, 4057, 4460, 6081, 7824, 7987, 3495, 141, 243,…
$ zona <chr> "Zona Norte", "Zona Norte", "Zona Norte", "Zona Norte", "…
$ piso <chr> "02", "02", "02", "02", "02", "02", "02", "03", NA, NA, N…
$ estrato <dbl> 5, 5, 6, 4, 5, 4, 5, 5, 3, 3, 3, 3, 5, 3, 4, 4, 5, 5, 4, …
$ preciom <dbl> 320, 780, 750, 625, 750, 600, 420, 490, 230, 190, 180, 50…
$ areaconst <dbl> 150, 380, 445, 355, 237, 160, 200, 118, 160, 435, 120, 21…
$ parqueaderos <dbl> 2, 2, NA, 3, 2, 1, 4, 2, NA, NA, NA, NA, NA, NA, NA, NA, …
$ banios <dbl> 4, 3, 7, 5, 6, 4, 4, 4, 2, 0, 3, 6, 5, 5, 3, 3, 4, 5, 5, …
$ habitaciones <dbl> 6, 3, 6, 5, 6, 5, 5, 4, 3, 0, 3, 6, 4, 8, 4, 3, 4, 6, 4, …
$ tipo <chr> "Casa", "Casa", "Casa", "Casa", "Casa", "Casa", "Casa", "…
$ barrio <chr> "acopi", "acopi", "acopi", "acopi", "acopi", "acopi", "ac…
$ longitud <dbl> -76.51341, -76.51674, -76.52950, -76.53179, -76.54044, -7…
$ latitud <dbl> 3.47968, 3.48721, 3.38527, 3.40590, 3.36862, 3.42125, 3.4…
summary(base1)
id zona piso estrato
Min. : 58.0 Length:722 Length:722 Min. :3.000
1st Qu.: 766.2 Class :character Class :character 1st Qu.:3.000
Median :2257.0 Mode :character Mode :character Median :4.000
Mean :2574.6 Mean :4.202
3rd Qu.:4225.0 3rd Qu.:5.000
Max. :8319.0 Max. :6.000
preciom areaconst parqueaderos banios
Min. : 89.0 Min. : 30.0 Min. : 1.000 Min. : 0.000
1st Qu.: 261.2 1st Qu.: 140.0 1st Qu.: 1.000 1st Qu.: 2.000
Median : 390.0 Median : 240.0 Median : 2.000 Median : 3.000
Mean : 445.9 Mean : 264.9 Mean : 2.182 Mean : 3.555
3rd Qu.: 550.0 3rd Qu.: 336.8 3rd Qu.: 3.000 3rd Qu.: 4.000
Max. :1940.0 Max. :1440.0 Max. :10.000 Max. :10.000
NA's :287
habitaciones tipo barrio longitud
Min. : 0.000 Length:722 Length:722 Min. :-76.59
1st Qu.: 3.000 Class :character Class :character 1st Qu.:-76.53
Median : 4.000 Mode :character Mode :character Median :-76.52
Mean : 4.507 Mean :-76.52
3rd Qu.: 5.000 3rd Qu.:-76.50
Max. :10.000 Max. :-76.47
latitud
Min. :3.333
1st Qu.:3.452
Median :3.468
Mean :3.460
3rd Qu.:3.482
Max. :3.496
# Conteo de NAs por variable
sapply(base1, function(x) sum(is.na(x)))
id zona piso estrato preciom areaconst
0 0 372 0 0 0
parqueaderos banios habitaciones tipo barrio longitud
287 0 0 0 0 0
latitud
0
# Revisión de valores únicos en variables clave
table(base1$estrato)
3 4 5 6
235 161 271 55
table(base1$banios)
0 1 2 3 4 5 6 7 8 9 10
10 17 165 187 171 101 46 11 11 1 2
table(base1$parqueaderos)
1 2 3 4 5 6 7 8 9 10
161 158 49 40 11 8 5 1 1 1
table(base1$habitaciones)
0 1 2 3 4 5 6 7 8 9 10
20 2 12 171 222 137 60 42 29 14 13
table(base1$areaconst == 0)
FALSE
722
table(base1$preciom == 0)
FALSE
722
# Histograma de precio para ver distribución general
ggplot(base1, aes(x = preciom)) +
geom_histogram(bins = 50, fill = "#69b3a2", color = "white") +
labs(title = "Distribución del precio de casas en Zona Norte",
x = "Precio (millones)", y = "Frecuencia") +
theme_minimal()
- Conteo de NA
# 1) Conteo y % de NA por variable
na_audit <- base1 %>%
summarise(across(everything(), ~ sum(is.na(.)))) %>%
pivot_longer(everything(), names_to = "variable", values_to = "na_count") %>%
mutate(
total_rows = nrow(base1),
na_pct = round(100 * na_count / total_rows, 2)
) %>%
arrange(desc(na_pct))
na_audit %>%
kable("html", caption = "Auditoría de faltantes en base1 (casas, Zona Norte)",
col.names = c("Variable", "NA (conteo)", "Total filas", "% NA")) %>%
kable_styling(full_width = FALSE, bootstrap_options = c("striped","hover"))
| Variable | NA (conteo) | Total filas | % NA |
|---|---|---|---|
| piso | 372 | 722 | 51.52 |
| parqueaderos | 287 | 722 | 39.75 |
| id | 0 | 722 | 0.00 |
| zona | 0 | 722 | 0.00 |
| estrato | 0 | 722 | 0.00 |
| preciom | 0 | 722 | 0.00 |
| areaconst | 0 | 722 | 0.00 |
| banios | 0 | 722 | 0.00 |
| habitaciones | 0 | 722 | 0.00 |
| tipo | 0 | 722 | 0.00 |
| barrio | 0 | 722 | 0.00 |
| longitud | 0 | 722 | 0.00 |
| latitud | 0 | 722 | 0.00 |
gg_miss_var(base1) # barras de % NA por variable
# Revisamos las filas donde parqueaderos es NA
base1 %>%
filter(is.na(parqueaderos)) %>%
select(barrio, estrato, areaconst, preciom, parqueaderos) %>%
head(10)
# A tibble: 10 × 5
barrio estrato areaconst preciom parqueaderos
<chr> <dbl> <dbl> <dbl> <dbl>
1 acopi 6 445 750 NA
2 acopi 3 160 230 NA
3 acopi 3 435 190 NA
4 acopi 3 120 180 NA
5 acopi 3 210 500 NA
6 acopi 5 455 520 NA
7 acopi 3 300 380 NA
8 acopi 4 117 305 NA
9 acopi 4 118 350 NA
10 acopi 5 165 395 NA
En la validación de valores faltantes se evidencia que piso tiene más del 50% de datos ausentes y parqueaderos casi el 40%.
Dado que el análisis para la vivienda 1 se centra en casas, la variable piso se descarta. Para parqueaderos,se opta por imputar los NA, pues esta variable es predictora clave del precio y su eliminación arbitraria podría distorsionar el modelo. Las demás variables clave (preciom, areaconst, estrato, banios, habitaciones) no presentan valores faltantes-
Se identificó que aproximadamente el 40% de los registros carecen de información en parqueaderos. Dado que esta variable aporta valor predictivo, eliminar tantos registros reduciría el tamaño de la muestra y la representatividad, se decide imputar los valores faltantes con la mediana del número de parqueaderos dentro de cada estrato socioeconómico. Esta estrategia preserva la coherencia de la base de datos y minimiza el sesgo, al relacionar la imputación con el nivel socioeconómico.
# Imputar parqueaderos NA con la mediana por estrato
base1_model <- base1 %>%
group_by(estrato) %>%
mutate(parqueaderos = ifelse(is.na(parqueaderos),
median(parqueaderos, na.rm = TRUE),
parqueaderos)) %>%
ungroup()
# Verificamos que ya no hay NA
sum(is.na(base1_model$parqueaderos))
[1] 0
- Detección de Outliers
# Selección de variables numéricas
vars_numericas <- base1_model %>%
select(preciom, areaconst, estrato, banios, parqueaderos, habitaciones)
# 1) Boxplots para visualizar
vars_numericas %>%
pivot_longer(cols = everything(), names_to = "variable", values_to = "valor") %>%
ggplot(aes(x = variable, y = valor)) +
geom_boxplot(fill = "#69b3a2", color = "black", outlier.colour = "red") +
labs(title = "Boxplots de variables numéricas (outliers en rojo)",
x = "Variable", y = "Valor") +
theme_minimal()
# 2) Función para calcular límites IQR
calc_outliers <- function(x) {
Q1 <- quantile(x, 0.25, na.rm = TRUE)
Q3 <- quantile(x, 0.75, na.rm = TRUE)
IQR_val <- Q3 - Q1
lower <- Q1 - 1.5 * IQR_val
upper <- Q3 + 1.5 * IQR_val
return(data.frame(Q1=Q1, Q3=Q3, IQR=IQR_val, Lower=lower, Upper=upper))
}
# 3) Revisar outliers para cada variable
outlier_summary <- vars_numericas %>%
summarise(across(everything(), ~list(calc_outliers(.x)))) %>%
pivot_longer(cols = everything(), names_to = "variable", values_to = "stats") %>%
unnest(cols = c(stats))
outlier_summary
# A tibble: 6 × 6
variable Q1 Q3 IQR Lower Upper
<chr> <dbl> <dbl> <dbl> <dbl> <dbl>
1 preciom 261. 550 289. -172. 983.
2 areaconst 140 337. 197. -155. 632.
3 estrato 3 5 2 0 8
4 banios 2 4 2 -1 7
5 parqueaderos 1 2 1 -0.5 3.5
6 habitaciones 3 5 2 0 8
El análisis de valores atípicos mediante boxplots e IQR muestra que hay de viviendas con precios y áreas significativamente superiores al promedio que pueden ser de un segmento de viviendas de mas alto nivel. Dado que esta si es una casuistica presente, estos valores si aportan al entendimiento del mercado, por lo que se mantienen en el conjunto de datos. No se identificaron ceros en variables clave tras la imputación, lo que confirma la consistencia general de la base base1_model.
EDA completo con graficos
library(GGally)
library(plotly)
# Subconjunto de variables numéricas
eda_data <- base1_model %>%
select(preciom, areaconst, estrato, banios, parqueaderos, habitaciones)
# 1) Matriz de correlaciones con GGally
ggpairs(eda_data,
lower = list(continuous = wrap("points", alpha = 0.4)),
diag = list(continuous = wrap("barDiag")),
upper = list(continuous = wrap("cor", size = 3))) +
ggtitle("Matriz de correlaciones entre variables numéricas")
# 2) Precio vs Área construida
plot_ly(base1_model, x = ~areaconst, y = ~preciom, type = "scatter", mode = "markers",
marker = list(color = 'blue'),
text = ~paste("Estrato:", estrato, "<br>Baños:", banios)) %>%
layout(title = "Precio vs Área construida",
xaxis = list(title = "Área construida (m²)"),
yaxis = list(title = "Precio (millones)"))
# 3) Precio vs Estrato
plot_ly(base1_model, x = ~factor(estrato), y = ~preciom, type = "box", color = ~factor(estrato)) %>%
layout(title = "Distribución de precios por estrato",
xaxis = list(title = "Estrato"), yaxis = list(title = "Precio (millones)"))
# 4) Precio vs Baños
plot_ly(base1_model, x = ~banios, y = ~preciom, type = "scatter", mode = "markers",
marker = list(color = 'orange'),
text = ~paste("Estrato:", estrato, "<br>Área:", areaconst)) %>%
layout(title = "Precio vs Baños",
xaxis = list(title = "Baños"), yaxis = list(title = "Precio (millones)"))
# 5) Precio vs Parqueaderos
plot_ly(base1_model, x = ~parqueaderos, y = ~preciom, type = "scatter", mode = "markers",
marker = list(color = 'green'),
text = ~paste("Estrato:", estrato, "<br>Área:", areaconst)) %>%
layout(title = "Precio vs Parqueaderos",
xaxis = list(title = "Parqueaderos"), yaxis = list(title = "Precio (millones)"))
# 6) Precio vs Habitaciones
plot_ly(base1_model, x = ~habitaciones, y = ~preciom, type = "scatter", mode = "markers",
marker = list(color = 'purple'),
text = ~paste("Estrato:", estrato, "<br>Área:", areaconst)) %>%
layout(title = "Precio vs Habitaciones",
xaxis = list(title = "Habitaciones"), yaxis = list(title = "Precio (millones)"))
El análisis exploratorio muestra:
Estos hallazgos validan el uso de estas variables como predictoras en el modelo de regresión múltiple.
Construir un modelo lm() que estime preciom (precio en millones) en función de:
# Ajuste del modelo de regresión múltiple
modelo1 <- lm(preciom ~ areaconst + estrato + banios + parqueaderos + habitaciones,
data = base1_model)
# Resumen del modelo
summary(modelo1)
Call:
lm(formula = preciom ~ areaconst + estrato + banios + parqueaderos +
habitaciones, data = base1_model)
Residuals:
Min 1Q Median 3Q Max
-930.04 -78.24 -17.21 45.51 1078.18
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) -226.5815 29.6390 -7.645 6.75e-14 ***
areaconst 0.8082 0.0436 18.536 < 2e-16 ***
estrato 79.7443 7.4434 10.713 < 2e-16 ***
banios 24.2979 5.3650 4.529 6.94e-06 ***
parqueaderos 17.0956 5.7275 2.985 0.00293 **
habitaciones 0.9366 4.1044 0.228 0.81956
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 158.2 on 716 degrees of freedom
Multiple R-squared: 0.655, Adjusted R-squared: 0.6526
F-statistic: 271.9 on 5 and 716 DF, p-value: < 2.2e-16
El modelo de regresión lineal múltiple para casas en Zona Norte muestra que:
El R² ajustado indica que el modelo explica aproximadamente 52.66% de la variabilidad en los precios. Este nivel de ajuste es razonable para datos inmobiliarios, donde existen factores externos como ubicación específica, acabados, antiguedad de construccion del inmueble, entre otros, que influyen en el precio.
library(car)
library(performance)
# 1. Diagnóstico básico de residuos
par(mfrow = c(2,2))
plot(modelo1)
par(mfrow = c(1,1))
# 2. Normalidad de residuos (QQ-Plot + prueba Shapiro-Wilk)
qqPlot(modelo1, main="QQ Plot de residuos")
[1] 208 632
shapiro.test(residuals(modelo1)) # p > 0.05 indica normalidad
Shapiro-Wilk normality test
data: residuals(modelo1)
W = 0.83323, p-value < 2.2e-16
# 3. Homocedasticidad (prueba de Breusch-Pagan)
library(lmtest)
bptest(modelo1) # p > 0.05 indica homocedasticidad
studentized Breusch-Pagan test
data: modelo1
BP = 132.34, df = 5, p-value < 2.2e-16
# 4. Multicolinealidad (VIF)
vif(modelo1) # Valores > 5 indican colinealidad alta
areaconst estrato banios parqueaderos habitaciones
1.530728 1.542322 1.926194 1.345919 1.621726
# 5. Resumen completo de supuestos
check_model(modelo1)
La validación de supuestos muestra que:
El modelo cumple con los supuestos básicos de regresión lineal y es adecuado para análisis e inferencia.
# Crear un nuevo dataframe con los valores de la Vivienda 1
nueva_v1 <- data.frame(
areaconst = 200,
estrato = 4, # probaremos con estrato 4 y 5
banios = 2,
parqueaderos = 1,
habitaciones = 4
)
# Predicción con intervalo de confianza
pred_v1_e4 <- predict(modelo1, newdata = nueva_v1, interval = "prediction")
pred_v1_e4
fit lwr upr
1 323.464 12.37273 634.5552
# Con estrato 5
nueva_v1$estrato <- 5
pred_v1_e5 <- predict(modelo1, newdata = nueva_v1, interval = "prediction")
pred_v1_e5
fit lwr upr
1 403.2083 91.53602 714.8805
La Vivienda 1 cuenta con las siguientes características:
Se estimaron los precios usando el modelo de regresión lineal múltiple:
| Estrato | Precio estimado (millones) | Intervalo de predicción 95% (millones) |
|---|---|---|
| 4 | 323.46 | 12.37 – 634.55 |
| 5 | 403.21 | 91.53 – 714.88 |
# --- Perfil objetivo Vivienda 1 ---
target <- list(areaconst = 200, banios = 2, parqueaderos = 1, habitaciones = 4)
# Base para recomendaciones: casas, zona norte (ya la tienes como base1).
# Reglas: precio <= 350; estrato 4 o 5; specs cercanas al objetivo;
# ventana de área ±20% (160–240 m²) para asegurar al menos 5 opciones.
candidatas <- base1 %>%
filter(
preciom <= 350,
estrato %in% c(4,5),
!is.na(longitud), !is.na(latitud), # necesarias para mapa
!is.na(parqueaderos), !is.na(banios), !is.na(habitaciones), !is.na(areaconst)
) %>%
filter(
between(areaconst, 0.8*target$areaconst, 1.2*target$areaconst),
banios >= target$banios, # al menos como lo solicitado
parqueaderos >= target$parqueaderos,
habitaciones >= target$habitaciones
) %>%
# Score de similitud (menor es mejor). Penalizamos estrato 5 un poquito por presupuesto.
mutate(
score = abs(areaconst - target$areaconst)/target$areaconst +
0.5*abs(banios - target$banios) +
0.5*abs(parqueaderos - target$parqueaderos) +
0.25*abs(habitaciones - target$habitaciones) +
ifelse(estrato == 5, 0.3, 0) # preferimos estrato 4 por costo
) %>%
arrange(score, preciom)
# Si hay menos de 5, bajamos ventana de área a ±30% (opcional):
if(nrow(candidatas) < 5){
candidatas <- base1 %>%
filter(preciom <= 350, estrato %in% c(4,5),
!is.na(longitud), !is.na(latitud),
!is.na(parqueaderos), !is.na(banios), !is.na(habitaciones), !is.na(areaconst)) %>%
filter(
between(areaconst, 0.7*target$areaconst, 1.3*target$areaconst),
banios >= target$banios,
parqueaderos >= target$parqueaderos,
habitaciones >= target$habitaciones
) %>%
mutate(
score = abs(areaconst - target$areaconst)/target$areaconst +
0.5*abs(banios - target$banios) +
0.5*abs(parqueaderos - target$parqueaderos) +
0.25*abs(habitaciones - target$habitaciones) +
ifelse(estrato == 5, 0.3, 0)
) %>%
arrange(score, preciom)
}
# Tomamos las 5 mejores
top5_v1 <- candidatas %>%
select(id, barrio, estrato, areaconst, banios, parqueaderos, habitaciones, preciom, longitud, latitud, score) %>%
head(5)
# --- Tabla bonita ---
top5_v1 %>%
select(id, barrio, estrato, areaconst, banios, parqueaderos, habitaciones, preciom) %>%
kable("html",
caption = "Top 5 ofertas para Vivienda 1 (casa, zona norte, ≤ 350M)",
col.names = c("ID","Barrio","Estrato","Área (m²)","Baños","Parq.","Habs.","Precio (M)")) %>%
kable_styling(full_width = FALSE, bootstrap_options = c("striped","hover"))
| ID | Barrio | Estrato | Área (m²) | Baños | Parq. | Habs. | Precio (M) |
|---|---|---|---|---|---|---|---|
| 94 | zona norte | 4 | 162 | 3 | 1 | 4 | 265 |
| 1163 | la merced | 5 | 216 | 2 | 2 | 4 | 350 |
| 1376 | la flora | 5 | 160 | 3 | 1 | 4 | 320 |
| 1270 | el bosque | 5 | 203 | 2 | 2 | 5 | 350 |
| 464 | el bosque | 4 | 165 | 4 | 1 | 4 | 330 |
# --- Mapa interactivo ---
leaflet(top5_v1) %>%
addTiles() %>%
addCircleMarkers(
lng = ~longitud, lat = ~latitud,
radius = 7, stroke = FALSE, fillOpacity = 0.85,
popup = ~paste0(
"<b>ID: </b>", id,
"<br><b>Barrio: </b>", barrio,
"<br><b>Estrato: </b>", estrato,
"<br><b>Área: </b>", areaconst, " m²",
"<br><b>Baños: </b>", banios,
"<br><b>Parqueaderos: </b>", parqueaderos,
"<br><b>Habitaciones: </b>", habitaciones,
"<br><b>Precio: </b>", round(preciom,1), " millones"
)
) %>%
setView(lng = -76.52, lat = 3.45, zoom = 12)
A partir del perfil objetivo (200 m², 2 baños, 1 parqueadero, 4 habitaciones, estrato 4–5, presupuesto ≤ 350 M), se filtraron las viviendas y se construyó un índice de similitud que pondera cercanía en área y comodidades, penalizando el estrato 5 por el presupuesto.
La tabla presenta las 5 mejores coincidencias y el mapa muestra su ubicación. Las opciones priorizadas cumplen con el presupuesto, mantienen especificaciones iguales o superiores a las solicitadas y se ubican en barrios de la Zona Norte. Estas alternativas representan candidatos viables para visita y negociación.
Filtramos el dataset por vivienda por Zona sur tipo Apartamentos
Se filtró la base para conservar únicamente apartamentos ubicados en la Zona Sur de Cali. Se presentan los tres primeros registros, así como tablas de comprobación (tipo y zona) y de descripción (estrato, barrios con mayor oferta y resumen de variables cuantitativas). El mapa interactivo ubica geográficamente todas las ofertas de la base; si se observan puntos aparentemente fuera de la zona, pueden deberse a coordenadas mal registradas o a límites administrativos distintos a la clasificación de “Zona Sur” en la base. Para decisiones operativas se recomienda contrastar con cartografía oficial.
# Filtrar apartamentos en Zona Sur
base2 <- vivienda %>%
filter(tipo == "Apartamento", zona == "Zona Sur")
# Verificamos primeros registros
head(base2, 3)
# A tibble: 3 × 13
id zona piso estrato preciom areaconst parqueaderos banios habitaciones
<dbl> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 5098 Zona S… 05 4 290 96 1 2 3
2 698 Zona S… 02 3 78 40 1 1 2
3 8199 Zona S… <NA> 6 875 194 2 5 3
# ℹ 4 more variables: tipo <chr>, barrio <chr>, longitud <dbl>, latitud <dbl>
# Tabla resumen por estrato
table(base2$estrato)
3 4 5 6
201 1091 1033 462
# Conteo total de filas
nrow(base2)
[1] 2787
# Primeros 3 registros (bonito)
library(knitr); library(kableExtra); library(dplyr)
base2 %>%
select(id, barrio, piso, estrato, areaconst, parqueaderos, banios, habitaciones, preciom) %>%
head(3) %>%
kable("html", caption = "Primeros 3 registros: Apartamentos en Zona Sur") %>%
kable_styling(full_width = FALSE, bootstrap_options = c("striped","hover"))
| id | barrio | piso | estrato | areaconst | parqueaderos | banios | habitaciones | preciom |
|---|---|---|---|---|---|---|---|---|
| 5098 | acopi | 05 | 4 | 96 | 1 | 2 | 3 | 290 |
| 698 | aguablanca | 02 | 3 | 40 | 1 | 1 | 2 | 78 |
| 8199 | aguacatal | NA | 6 | 194 | 2 | 5 | 3 | 875 |
# 3) Tablas que comprueban el filtro y describen la base
# 3.1 confirmación del filtro
base2 %>% count(tipo) %>% kable("html", caption = "Comprobación: tipo") %>% kable_styling(full_width = FALSE)
| tipo | n |
|---|---|
| Apartamento | 2787 |
base2 %>% count(zona) %>% kable("html", caption = "Comprobación: zona") %>% kable_styling(full_width = FALSE)
| zona | n |
|---|---|
| Zona Sur | 2787 |
# 3.2 distribución por estrato
base2 %>%
count(estrato) %>%
arrange(estrato) %>%
kable("html", col.names = c("Estrato","Cantidad"),
caption = "Cantidad de apartamentos por estrato (Zona Sur)") %>%
kable_styling(full_width = FALSE)
| Estrato | Cantidad |
|---|---|
| 3 | 201 |
| 4 | 1091 |
| 5 | 1033 |
| 6 | 462 |
# 3.3 barrios con mayor oferta (top 10)
base2 %>%
count(barrio) %>%
arrange(desc(n)) %>%
head(10) %>%
kable("html", col.names = c("Barrio","Cantidad"),
caption = "Top 10 barrios con oferta de apartamentos (Zona Sur)") %>%
kable_styling(full_width = FALSE)
| Barrio | Cantidad |
|---|---|
| valle del lili | 837 |
| ciudad jardín | 218 |
| pance | 205 |
| el ingenio | 128 |
| el caney | 124 |
| la hacienda | 108 |
| el refugio | 77 |
| el limonar | 59 |
| caney | 58 |
| quintas de don | 58 |
# 3.4 resumen de variables cuantitativas
library(tibble)
base2 %>%
select(preciom, areaconst, parqueaderos, banios, habitaciones) %>%
summary() %>%
as.data.frame() %>%
rownames_to_column("Medida") %>%
kable("html", caption = "Resumen estadístico de variables cuantitativas") %>%
kable_styling(full_width = FALSE)
| Medida | Var1 | Var2 | Freq |
|---|---|---|---|
| 1 | preciom | Min. : 75.0 | |
| 2 | preciom | 1st Qu.: 175.0 | |
| 3 | preciom | Median : 245.0 | |
| 4 | preciom | Mean : 297.3 | |
| 5 | preciom | 3rd Qu.: 335.0 | |
| 6 | preciom | Max. :1750.0 | |
| 7 | preciom | NA | |
| 8 | areaconst | Min. : 40.00 | |
| 9 | areaconst | 1st Qu.: 65.00 | |
| 10 | areaconst | Median : 85.00 | |
| 11 | areaconst | Mean : 97.47 | |
| 12 | areaconst | 3rd Qu.:110.00 | |
| 13 | areaconst | Max. :932.00 | |
| 14 | areaconst | NA | |
| 15 | parqueaderos | Min. : 1.000 | |
| 16 | parqueaderos | 1st Qu.: 1.000 | |
| 17 | parqueaderos | Median : 1.000 | |
| 18 | parqueaderos | Mean : 1.415 | |
| 19 | parqueaderos | 3rd Qu.: 2.000 | |
| 20 | parqueaderos | Max. :10.000 | |
| 21 | parqueaderos | NA’s :406 | |
| 22 | banios | Min. :0.000 | |
| 23 | banios | 1st Qu.:2.000 | |
| 24 | banios | Median :2.000 | |
| 25 | banios | Mean :2.488 | |
| 26 | banios | 3rd Qu.:3.000 | |
| 27 | banios | Max. :8.000 | |
| 28 | banios | NA | |
| 29 | habitaciones | Min. :0.000 | |
| 30 | habitaciones | 1st Qu.:3.000 | |
| 31 | habitaciones | Median :3.000 | |
| 32 | habitaciones | Mean :2.966 | |
| 33 | habitaciones | 3rd Qu.:3.000 | |
| 34 | habitaciones | Max. :6.000 | |
| 35 | habitaciones | NA |
MAPA INTERACTIVO
# Mapa interactivo de la base
library(leaflet)
centro_lon <- mean(base2$longitud, na.rm = TRUE)
centro_lat <- mean(base2$latitud, na.rm = TRUE)
leaflet(base2 %>% filter(!is.na(longitud), !is.na(latitud))) %>%
addTiles() %>%
addCircleMarkers(
lng = ~longitud, lat = ~latitud,
popup = ~paste0("<b>ID: </b>", id,
"<br><b>Barrio: </b>", barrio,
"<br><b>Piso: </b>", piso,
"<br><b>Estrato: </b>", estrato,
"<br><b>Área: </b>", areaconst, " m²",
"<br><b>Baños: </b>", banios,
"<br><b>Parqueaderos: </b>", parqueaderos,
"<br><b>Habitaciones: </b>", habitaciones,
"<br><b>Precio: </b>", round(preciom,1), " millones"),
radius = 6, stroke = FALSE, fillOpacity = 0.85
) %>%
setView(lng = centro_lon, lat = centro_lat, zoom = 12)
EDA
Revisamos la base2 para vivienda, buscamos rangos raros (áreas muy pequeñas/grandes, precios imposibles), ceros en baños o habitaciones.
# Estructura de la base filtrada (apartamentos, Zona Sur)
glimpse(base2)
Rows: 2,787
Columns: 13
$ id <dbl> 5098, 698, 8199, 1241, 5370, 6975, 5615, 6262, 7396, 6949…
$ zona <chr> "Zona Sur", "Zona Sur", "Zona Sur", "Zona Sur", "Zona Sur…
$ piso <chr> "05", "02", NA, NA, NA, "06", "08", NA, NA, NA, "10", "05…
$ estrato <dbl> 4, 3, 6, 3, 3, 4, 3, 3, 3, 4, 3, 5, 6, 3, 3, 5, 5, 5, 6, …
$ preciom <dbl> 290, 78, 875, 135, 135, 220, 210, 105, 115, 220, 230, 344…
$ areaconst <dbl> 96, 40, 194, 117, 78, 75, 72, 68, 58, 84, 63, 107, 182, 7…
$ parqueaderos <dbl> 1, 1, 2, NA, NA, 1, 2, NA, 1, NA, 1, 2, 2, 1, 1, 2, 1, 2,…
$ banios <dbl> 2, 1, 5, 2, 1, 2, 2, 2, 2, 2, 2, 2, 4, 2, 2, 4, 2, 4, 3, …
$ habitaciones <dbl> 3, 2, 3, 3, 3, 3, 3, 3, 2, 3, 2, 3, 3, 3, 3, 3, 2, 3, 3, …
$ tipo <chr> "Apartamento", "Apartamento", "Apartamento", "Apartamento…
$ barrio <chr> "acopi", "aguablanca", "aguacatal", "alameda", "alameda",…
$ longitud <dbl> -76.53464, -76.50100, -76.55700, -76.51400, -76.53600, -7…
$ latitud <dbl> 3.44987, 3.40000, 3.45900, 3.44100, 3.43600, 3.39109, 3.4…
# Resumen de variables numéricas clave
summary(select(base2, preciom, areaconst, parqueaderos, banios, habitaciones))
preciom areaconst parqueaderos banios
Min. : 75.0 Min. : 40.00 Min. : 1.000 Min. :0.000
1st Qu.: 175.0 1st Qu.: 65.00 1st Qu.: 1.000 1st Qu.:2.000
Median : 245.0 Median : 85.00 Median : 1.000 Median :2.000
Mean : 297.3 Mean : 97.47 Mean : 1.415 Mean :2.488
3rd Qu.: 335.0 3rd Qu.:110.00 3rd Qu.: 2.000 3rd Qu.:3.000
Max. :1750.0 Max. :932.00 Max. :10.000 Max. :8.000
NA's :406
habitaciones
Min. :0.000
1st Qu.:3.000
Median :3.000
Mean :2.966
3rd Qu.:3.000
Max. :6.000
na_audit2 <- base2 %>%
summarise(across(everything(), ~ sum(is.na(.)))) %>%
pivot_longer(everything(), names_to = "Variable", values_to = "NA_count") %>%
mutate(
Total_filas = nrow(base2),
NA_pct = round(100 * NA_count / Total_filas, 2)
) %>%
arrange(desc(NA_pct))
na_audit2 %>%
kable("html",
caption = "Auditoría de faltantes – Vivienda 2 (aptos, Zona Sur)",
col.names = c("Variable","NA (conteo)","Total filas","% NA")) %>%
kable_styling(full_width = FALSE, bootstrap_options = c("striped","hover"))
| Variable | NA (conteo) | Total filas | % NA |
|---|---|---|---|
| piso | 622 | 2787 | 22.32 |
| parqueaderos | 406 | 2787 | 14.57 |
| id | 0 | 2787 | 0.00 |
| zona | 0 | 2787 | 0.00 |
| estrato | 0 | 2787 | 0.00 |
| preciom | 0 | 2787 | 0.00 |
| areaconst | 0 | 2787 | 0.00 |
| banios | 0 | 2787 | 0.00 |
| habitaciones | 0 | 2787 | 0.00 |
| tipo | 0 | 2787 | 0.00 |
| barrio | 0 | 2787 | 0.00 |
| longitud | 0 | 2787 | 0.00 |
| latitud | 0 | 2787 | 0.00 |
library(dplyr)
base2 %>%
summarise(
banios_ceros = sum(banios == 0, na.rm = TRUE),
hab_ceros = sum(habitaciones == 0, na.rm = TRUE),
parqueaderos_NA = sum(is.na(parqueaderos)),
piso_NA = sum(is.na(piso))
)
# A tibble: 1 × 4
banios_ceros hab_ceros parqueaderos_NA piso_NA
<int> <int> <int> <int>
1 6 8 406 622
Imputación de datos faltantes:
library(dplyr)
# Quitamos solo los 0 implausibles (sin tocar parqueaderos aún)
base2_clean_bh <- base2 %>%
filter(banios != 0, habitaciones != 0)
# Chequeo: cuántas filas eliminamos
data.frame(
antes = nrow(base2),
despues = nrow(base2_clean_bh),
eliminadas = nrow(base2) - nrow(base2_clean_bh)
)
antes despues eliminadas
1 2787 2777 10
base2_imp <- base2_clean_bh %>%
group_by(estrato) %>%
mutate(
parqueaderos = if_else(is.na(parqueaderos),
round(median(parqueaderos, na.rm = TRUE)),
parqueaderos)
) %>%
ungroup()
# Verificación: ya no debería haber NA en parqueaderos
sum(is.na(base2_imp$parqueaderos))
[1] 0
Los faltantes en parqueaderos que suman el 14.6% se imputaron con la mediana por estrato, lo que mantiene el tamaño muestral y respeta la estructura socioeconómica.Con lo anterior se construyó base2_model (sin faltantes en variables críticas) y base2_map (con coordenadas válidas).
# Para el MODELO: sin NA en variables críticas
base2_model <- base2_imp %>%
filter(estrato >= 1 & estrato <= 6) %>%
drop_na(preciom, areaconst, estrato, banios, parqueaderos, habitaciones)
# Para el MAPA: solo coordenadas válidas
base2_map <- base2_imp %>%
drop_na(longitud, latitud)
# Resumen de tamaños (para el informe)
data.frame(
dataset = c("base2 (filtrada)", "base2_model (modelo)", "base2_map (mapa)"),
filas = c(nrow(base2), nrow(base2_model), nrow(base2_map))
)
dataset filas
1 base2 (filtrada) 2787
2 base2_model (modelo) 2777
3 base2_map (mapa) 2777
En apartamentos Zona Sur se identifican outliers en preciom y areaconst asociados a unidades de alto valor. Dado que representan un segmento real del mercado, se mantienen. No se detectaron valores 0 en las demás variables tras la limpieza.
library(dplyr)
library(tidyr)
library(ggplot2)
# Variables numéricas del modelo
num2 <- base2_model %>%
select(preciom, areaconst, estrato, banios, parqueaderos, habitaciones)
# (A) Boxplots para ver outliers visualmente
num2 %>%
pivot_longer(everything(), names_to = "variable", values_to = "valor") %>%
ggplot(aes(variable, valor)) +
geom_boxplot(fill = "#69b3a2", color = "black", outlier.colour = "red") +
labs(title = "Vivienda 2 – Outliers por variable", x = "", y = "") +
theme_minimal()
# (B) Tabla con límites IQR y conteo de outliers por variable
calc_iqr <- function(x){
Q1 <- quantile(x, .25, na.rm=TRUE); Q3 <- quantile(x, .75, na.rm=TRUE)
I <- Q3 - Q1; lower <- Q1 - 1.5*I; upper <- Q3 + 1.5*I
c(Q1=Q1, Q3=Q3, IQR=I, Lower=lower, Upper=upper,
n_below=sum(x < lower, na.rm=TRUE),
n_above=sum(x > upper, na.rm=TRUE))
}
outliers2 <- sapply(num2, calc_iqr) %>% t() %>% as.data.frame()
round(outliers2, 2)
Q1.25% Q3.75% IQR.75% Lower.25% Upper.75% n_below n_above
preciom 175 335 160 -65.0 575.0 0 260
areaconst 65 110 45 -2.5 177.5 0 175
estrato 4 5 1 2.5 6.5 0 0
banios 2 3 1 0.5 4.5 0 135
parqueaderos 1 2 1 -0.5 3.5 0 32
habitaciones 3 3 0 3.0 3.0 482 394
EDA correlaciones y graficos
eda2 <- base2_model %>%
select(preciom, areaconst, estrato, banios, parqueaderos, habitaciones)
# (A) Matriz de correlaciones / dispersión
ggpairs(eda2,
lower = list(continuous = wrap("points", alpha = 0.35, size = 0.9)),
diag = list(continuous = wrap("barDiag")),
upper = list(continuous = wrap("cor", size = 3))) +
ggtitle("Vivienda 2 – Correlaciones y dispersión")
# (B) Gráficos interactivos Plotly
# Precio vs Área
plot_ly(base2_model, x = ~areaconst, y = ~preciom,
type="scatter", mode="markers",
text=~paste("Estrato:", estrato,
"<br>Baños:", banios,
"<br>Parq:", parqueaderos)) %>%
layout(title="Precio vs Área construida (V2)",
xaxis=list(title="Área (m²)"),
yaxis=list(title="Precio (M)"))
# Precio por Estrato (cajas)
plot_ly(base2_model, x = ~factor(estrato), y = ~preciom,
type="box", color=~factor(estrato)) %>%
layout(title="Precio por estrato (V2)",
xaxis=list(title="Estrato"), yaxis=list(title="Precio (M)"))
# Precio vs Baños
plot_ly(base2_model, x = ~banios, y = ~preciom,
type="scatter", mode="markers") %>%
layout(title="Precio vs Baños (V2)",
xaxis=list(title="Baños"), yaxis=list(title="Precio (M)"))
# Precio vs Parqueaderos
plot_ly(base2_model, x = ~parqueaderos, y = ~preciom,
type="scatter", mode="markers") %>%
layout(title="Precio vs Parqueaderos (V2)",
xaxis=list(title="Parqueaderos"), yaxis=list(title="Precio (M)"))
# Precio vs Habitaciones
plot_ly(base2_model, x = ~habitaciones, y = ~preciom,
type="scatter", mode="markers") %>%
layout(title="Precio vs Habitaciones (V2)",
xaxis=list(title="Habitaciones"), yaxis=list(title="Precio (M)"))
El EDA de apartamentos en Zona Sur evidencia una relación positiva fuerte entre areaconst y preciom, y diferencias claras de precio por estrato (mayores en 5–6).
banios y parqueaderos presentan asociaciones positivas moderadas, habitaciones aporta información adicional más limitada.
Estos hallazgos respaldan el uso de estas variables en el modelo de regresión múltiple.
# Modelo de regresión lineal múltiple
modelo2 <- lm(preciom ~ areaconst + estrato + banios + parqueaderos + habitaciones,
data = base2_model)
# Resumen del modelo (coeficientes, R2, significancia)
summary(modelo2)
Call:
lm(formula = preciom ~ areaconst + estrato + banios + parqueaderos +
habitaciones, data = base2_model)
Residuals:
Min 1Q Median 3Q Max
-1120.55 -38.38 -2.81 38.27 922.91
Coefficients:
Estimate Std. Error t value Pr(>|t|)
(Intercept) -259.27683 13.16926 -19.688 < 2e-16 ***
areaconst 1.32548 0.05008 26.466 < 2e-16 ***
estrato 57.44579 2.68557 21.391 < 2e-16 ***
banios 45.71708 3.10613 14.718 < 2e-16 ***
parqueaderos 77.66743 3.90002 19.915 < 2e-16 ***
habitaciones -19.52241 3.45457 -5.651 1.76e-08 ***
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Residual standard error: 92.83 on 2771 degrees of freedom
Multiple R-squared: 0.7639, Adjusted R-squared: 0.7635
F-statistic: 1793 on 5 and 2771 DF, p-value: < 2.2e-16
# 4.1 Diagnóstico básico de residuos
par(mfrow = c(2,2)); plot(modelo2); par(mfrow = c(1,1))
# 4.2 Normalidad (QQ-Plot) y Homocedasticidad (Breusch–Pagan)
library(car)
qqPlot(modelo2, main = "QQ-Plot de residuos (modelo2)") # visual
[1] 1524 2373
library(lmtest)
bptest(modelo2) # p > 0.05 ≈ homocedasticidad
studentized Breusch-Pagan test
data: modelo2
BP = 898.6, df = 5, p-value < 2.2e-16
# 4.3 Colinealidad (VIF)
library(car)
vif(modelo2) # < 5 ideal, < 10 aceptable
areaconst estrato banios parqueaderos habitaciones
2.177763 1.646232 2.678270 1.857722 1.440328
# 4.4 Chequeo integral (opcional)
library(performance)
check_model(modelo2)
El modelo cumple razonablemente con los supuestos clave: hay linealidad, independencia y una colinealidad baja. Aunque se observan algunas desviaciones en la normalidad y una leve heterocedasticidad en precios altos, esto es normal en mercados con tanta variabilidad como el inmobiliario. A pesar de lo anterior, el modelo es totalmente funcional tanto para inferencia como para predicción.
# Estrato 5
nuevo_v2 <- data.frame(
areaconst = 300,
estrato = 5,
banios = 3,
parqueaderos = 3,
habitaciones = 5
)
pred_v2_e5 <- predict(modelo2, newdata = nuevo_v2, interval = "prediction")
pred_v2_e5
fit lwr upr
1 698.1389 514.9294 881.3484
# Estrato 6
nuevo_v2$estrato <- 6
pred_v2_e6 <- predict(modelo2, newdata = nuevo_v2, interval = "prediction")
pred_v2_e6
fit lwr upr
1 755.5847 572.3567 938.8127
Características objetivo: 300 m², 3 baños, 3 parqueaderos, 5
habitaciones, estrato 5–6.
Crédito preaprobado: 850 millones.
| Estrato | Precio estimado (M) | Intervalo de predicción 95% (M) |
|---|---|---|
| 5 | 698.14 | 514.93 – 881.35 |
| 6 | 755.58 | 572.36 – 938.81 |
# -------- Perfil objetivo Vivienda 2 --------
target_v2 <- list(areaconst = 300, banios = 3, parqueaderos = 3, habitaciones = 5)
# 1) Candidatas (apartamentos, Zona Sur, estrato 5–6, ≤ 850M, specs mínimas)
candidatas_v2 <- base2_imp %>%
filter(
tipo == "Apartamento",
zona == "Zona Sur",
estrato %in% c(5, 6),
preciom <= 850,
!is.na(longitud), !is.na(latitud),
!is.na(areaconst), !is.na(banios), !is.na(parqueaderos), !is.na(habitaciones)
) %>%
filter(
between(areaconst, 0.8 * target_v2$areaconst, 1.2 * target_v2$areaconst), # 240–360 m²
banios >= target_v2$banios,
parqueaderos >= target_v2$parqueaderos,
habitaciones >= target_v2$habitaciones
) %>%
mutate(
# Score de similitud (menor = mejor). Ligera penalización a estrato 6 (presupuesto).
score = abs(areaconst - target_v2$areaconst) / target_v2$areaconst +
0.5 * abs(banios - target_v2$banios) +
0.5 * abs(parqueaderos - target_v2$parqueaderos) +
0.25 * abs(habitaciones - target_v2$habitaciones) +
ifelse(estrato == 6, 0.15, 0)
) %>%
arrange(score, preciom)
# 2) Si hay pocas, relajamos ventana de área a ±30%
if (nrow(candidatas_v2) < 5) {
candidatas_v2 <- base2_imp %>%
filter(
tipo == "Apartamento", zona == "Zona Sur", estrato %in% c(5, 6),
preciom <= 850, !is.na(longitud), !is.na(latitud),
!is.na(areaconst), !is.na(banios), !is.na(parqueaderos), !is.na(habitaciones)
) %>%
filter(
between(areaconst, 0.7 * target_v2$areaconst, 1.3 * target_v2$areaconst), # 210–390 m²
banios >= target_v2$banios, parqueaderos >= target_v2$parqueaderos, habitaciones >= target_v2$habitaciones
) %>%
mutate(
score = abs(areaconst - target_v2$areaconst) / target_v2$areaconst +
0.5 * abs(banios - target_v2$banios) +
0.5 * abs(parqueaderos - target_v2$parqueaderos) +
0.25 * abs(habitaciones - target_v2$habitaciones) +
ifelse(estrato == 6, 0.15, 0)
) %>%
arrange(score, preciom)
}
# 3) Top 5
top5_v2 <- candidatas_v2 %>%
select(id, barrio, estrato, areaconst, banios, parqueaderos, habitaciones, preciom,
longitud, latitud, score) %>%
head(5)
# 4) Tabla bonita
top5_v2 %>%
select(id, barrio, estrato, areaconst, banios, parqueaderos, habitaciones, preciom) %>%
kable("html",
caption = "Top 5 apartamentos recomendados (Zona Sur, ≤ 850M)",
col.names = c("ID","Barrio","Estrato","Área (m²)","Baños","Parq.","Habs.","Precio (M)")) %>%
kable_styling(full_width = FALSE, bootstrap_options = c("striped","hover"))
| ID | Barrio | Estrato | Área (m²) | Baños | Parq. | Habs. | Precio (M) |
|---|---|---|---|---|---|---|---|
| 8036 | seminario | 5 | 256 | 5 | 3 | 5 | 530 |
| 7512 | seminario | 5 | 300 | 5 | 3 | 6 | 670 |
# 5) Mapa interactivo
leaflet(top5_v2) %>%
addTiles() %>%
addCircleMarkers(
lng = ~longitud, lat = ~latitud,
radius = 7, stroke = FALSE, fillOpacity = 0.85,
popup = ~paste0(
"<b>ID: </b>", id,
"<br><b>Barrio: </b>", barrio,
"<br><b>Estrato: </b>", estrato,
"<br><b>Área: </b>", areaconst, " m²",
"<br><b>Baños: </b>", banios,
"<br><b>Parqueaderos: </b>", parqueaderos,
"<br><b>Habitaciones: </b>", habitaciones,
"<br><b>Precio: </b>", round(preciom,1), " M"
)
) %>%
setView(lng = mean(top5_v2$longitud), lat = mean(top5_v2$latitud), zoom = 12)
Selección de ofertas – Vivienda 2:
Se filtraron apartamentos en Zona Sur, estratos 5–6, con precio ≤ 850 millones y especificaciones iguales o superiores a las solicitadas (≥ 300 m² ± tolerancia, ≥ 3 baños, ≥ 3 parqueaderos, ≥ 5 habitaciones). Se definió un índice de similitud que pondera cercanía en área y comodidades, penalizando el estrato 6 para priorizar el presupuesto. Se presentan las 5 mejores coincidencias en tabla y mapa interactivo. Estas ofertas son candidatos idóneos para visita y negociación, coherentes con la predicción del modelo (698 millones en estrato 5 y 756 millones en estrato 6).
Evaluación con set de prueba (80/20)
# --- Rendimiento con set de prueba (ambos modelos) ---
set.seed(42)
split1 <- sample.int(nrow(base1_model), size = floor(0.8*nrow(base1_model)))
train1 <- base1_model[split1,]; test1 <- base1_model[-split1,]
m1 <- lm(preciom ~ areaconst + estrato + banios + parqueaderos + habitaciones, data=train1)
pred1 <- predict(m1, newdata=test1)
RMSE1 <- sqrt(mean((test1$preciom - pred1)^2))
MAE1 <- mean(abs(test1$preciom - pred1))
set.seed(42)
split2 <- sample.int(nrow(base2_model), size = floor(0.8*nrow(base2_model)))
train2 <- base2_model[split2,]; test2 <- base2_model[-split2,]
m2 <- lm(preciom ~ areaconst + estrato + banios + parqueaderos + habitaciones, data=train2)
pred2 <- predict(m2, newdata=test2)
RMSE2 <- sqrt(mean((test2$preciom - pred2)^2))
MAE2 <- mean(abs(test2$preciom - pred2))
data.frame(
Modelo = c("Casas Norte", "Aptos Sur"),
RMSE = c(RMSE1, RMSE2),
MAE = c(MAE1, MAE2)
)
Modelo RMSE MAE
1 Casas Norte 176.69646 110.36794
2 Aptos Sur 73.59518 52.10209
# Promedios y porcentajes
mean1 <- mean(test1$preciom)
MAPE1 <- mean(abs((test1$preciom - pred1) / test1$preciom)) * 100
pctMAE1 <- 100 * MAE1 / mean1
mean2 <- mean(test2$preciom)
MAPE2 <- mean(abs((test2$preciom - pred2) / test2$preciom)) * 100
pctMAE2 <- 100 * MAE2 / mean2
# Tabla de métricas (nombres sin %)
tabla_metricas <- data.frame(
Modelo = c("Casas Norte", "Aptos Sur"),
RMSE = c(RMSE1, RMSE2),
MAE = c(MAE1, MAE2),
Precio_medio_test = c(mean1, mean2),
MAPE_pct = c(MAPE1, MAPE2),
MAE_sobre_media_pct = c(pctMAE1, pctMAE2)
)
# (opcional) redondear y mostrar bonito
library(dplyr); library(knitr); library(kableExtra)
tabla_metricas %>%
mutate(across(where(is.numeric), ~round(., 2))) %>%
kable("html", caption = "Rendimiento con set de prueba (80/20)") %>%
kable_styling(full_width = FALSE, bootstrap_options = c("striped","hover"))
| Modelo | RMSE | MAE | Precio_medio_test | MAPE_pct | MAE_sobre_media_pct |
|---|---|---|---|---|---|
| Casas Norte | 176.7 | 110.37 | 462.21 | 24.47 | 23.88 |
| Aptos Sur | 73.6 | 52.10 | 300.49 | 19.03 | 17.34 |
Para Casas en Zona Norte, el modelo tiene un RMSE de 176.7 millones y un MAE de unos 110.4 millones en el set de prueba. Eso indica que los errores son algo dispersos, lo cual era esperable por la alta variabilidad en los precios de casas en esa zona (hay mucha diferencia entre unas y otras).
En cambio, para Apartamentos en Zona Sur, el rendimiento es bastante mejor: RMSE = 73.6 M y MAE = 52.1 M, lo que muestra que el modelo logra predecir con más precisión. Esto tiene sentido ya que los apartamentos en esa zona tienden a tener precios más homogéneos.
Los resultados encajan bien con lo que vimos en los diagnósticos: linealidad razonable, algo de heterocedasticidad en los precios más altos, y colas pesadas en la distribución. Por eso, si se va a hacer inferencia, se recomienda usar errores estándar robustos. Aun con sus detalles, ambos modelos son útiles para priorizar y predecir dentro del rango observado.
C&A recibió dos solicitudes de vivienda en Cali con perfiles y presupuestos específicos. Se analizó la base vivienda (paquete paqueteMODELOS) aplicando filtrado por zona Y tipo, limpieza, imputación razonada, EDA, regresión lineal múltiple y verificación de supuestos. Se generaron predicciones y se priorizaron 5 ofertas para cada caso con mapas interactivos.
Vivienda 1
Casa, Zona Norte, crédito ≤ 350 M
Perfil: 200 m², 1 parqueadero, 2 baños, 4 habitaciones, estrato 4–5.
Modelo (casas, Zona Norte): preciom - areaconst + estrato + banios + parqueaderos + habitaciones.
Ajuste: R² adj = 0.653. Predictores significativos: areaconst, estrato, banios, parqueaderos; habitaciones no aporta de forma estadística una vez se controlan las demás.
Predicción:
Estrato 4: 323.5 M (IC95% 12.4–634.6 M) dentro del presupuesto.
Estrato 5: 403.2 M (IC95% 91.5–714.9 M) supera el tope estimado.
Recomendación: concentrar la búsqueda en estrato 4. Se presentan 5 casas ≤ 350 millones que cumplen o superan el perfil, con mapa para priorizar visitas.
Vivienda 2
Apartamento, Zona Sur, crédito ≤ 850 M
Perfil: 300 m², 3 parqueaderos, 3 baños, 5 habitaciones, estrato 5–6.
Modelo (apartamentos, Zona Sur): misma fórmula.
Ajuste: (reporta el R² adj de tu summary(modelo2)).
Predicción:
Estrato 5: 698.1 M (IC95% 514.9–881.3 M) se encuentra en el en rango, con disponible.
Estrato 6: 755.6 M (IC95% 572.4–938.8 M) se encuentra en el rango sin embargo el extremo superior puede exceder 850 millones
Recomendación: priorizar estrato 5 y opciones de estrato 6 con margen de negociación. Se listan 5 apartamentos ≤ 850 millones con mapa.
Área construida y estrato son los principales impulsores del precio, baños y parqueaderos tienen efecto positivo moderado.
Se observan propiedades de alto valor (outliers altos) que reflejan la heterogeneidad real del mercado, se mantuvieron.
Conclusiónes: