knitr::opts_chunk$set(
warning = FALSE, # oculta warnings
message = FALSE # oculta mensajes (p.ej. de library)
)
# Luego instala el paquete desde GitHub
devtools::install_github("centromagis/paqueteMODELOS")
# Y finalmente cárgalo
library(paqueteMODELOS)
# Cargar dataset
data("vivienda")
# Ver estructura y primeras filas
str(vivienda)
## spc_tbl_ [8,322 × 13] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
## $ id : num [1:8322] 1147 1169 1350 5992 1212 ...
## $ zona : chr [1:8322] "Zona Oriente" "Zona Oriente" "Zona Oriente" "Zona Sur" ...
## $ piso : chr [1:8322] NA NA NA "02" ...
## $ estrato : num [1:8322] 3 3 3 4 5 5 4 5 5 5 ...
## $ preciom : num [1:8322] 250 320 350 400 260 240 220 310 320 780 ...
## $ areaconst : num [1:8322] 70 120 220 280 90 87 52 137 150 380 ...
## $ parqueaderos: num [1:8322] 1 1 2 3 1 1 2 2 2 2 ...
## $ banios : num [1:8322] 3 2 2 5 2 3 2 3 4 3 ...
## $ habitaciones: num [1:8322] 6 3 4 3 3 3 3 4 6 3 ...
## $ tipo : chr [1:8322] "Casa" "Casa" "Casa" "Casa" ...
## $ barrio : chr [1:8322] "20 de julio" "20 de julio" "20 de julio" "3 de julio" ...
## $ longitud : num [1:8322] -76.5 -76.5 -76.5 -76.5 -76.5 ...
## $ latitud : num [1:8322] 3.43 3.43 3.44 3.44 3.46 ...
## - attr(*, "spec")=List of 3
## ..$ cols :List of 13
## .. ..$ id : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ zona : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
## .. ..$ piso : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
## .. ..$ estrato : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ preciom : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ areaconst : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ parqueaderos: list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ banios : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ habitaciones: list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ tipo : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
## .. ..$ barrio : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
## .. ..$ longitud : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## .. ..$ latitud : list()
## .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
## ..$ default: list()
## .. ..- attr(*, "class")= chr [1:2] "collector_guess" "collector"
## ..$ delim : chr ";"
## ..- attr(*, "class")= chr "col_spec"
## - attr(*, "problems")=<externalptr>
head(vivienda)
## # A tibble: 6 × 13
## id zona piso estrato preciom areaconst parqueaderos banios habitaciones
## <dbl> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 1147 Zona O… <NA> 3 250 70 1 3 6
## 2 1169 Zona O… <NA> 3 320 120 1 2 3
## 3 1350 Zona O… <NA> 3 350 220 2 2 4
## 4 5992 Zona S… 02 4 400 280 3 5 3
## 5 1212 Zona N… 01 5 260 90 1 2 3
## 6 1724 Zona N… 01 5 240 87 1 3 3
## # ℹ 4 more variables: tipo <chr>, barrio <chr>, longitud <dbl>, latitud <dbl>
summary(vivienda)
## 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
library(tidyverse)
library(janitor)
# 1) Copia de trabajo con nombres estandarizados
df <- vivienda %>% clean_names()
# 2) Cuántas filas/columnas y nombres
nrow(df); ncol(df); names(df)
## [1] 8322
## [1] 13
## [1] "id" "zona" "piso" "estrato" "preciom"
## [6] "areaconst" "parqueaderos" "banios" "habitaciones" "tipo"
## [11] "barrio" "longitud" "latitud"
# 3) Faltantes por columna
colSums(is.na(df))
## id zona piso estrato preciom areaconst
## 3 3 2638 3 2 3
## parqueaderos banios habitaciones tipo barrio longitud
## 1605 3 3 3 3 3
## latitud
## 3
# 4) ¿IDs repetidos?
sum(duplicated(df$id))
## [1] 2
# Quitar duplicados por id
df <- df %>% distinct(id, .keep_all = TRUE)
# Revisar tamaño final
nrow(df)
## [1] 8320
library(tidyverse)
library(janitor)
# 1) Partimos del dataset original y construimos la base maestra
df_master <- vivienda %>%
clean_names() %>% # nombres en snake_case
filter(!is.na(id)) %>% # sin filas sin id
distinct(id, .keep_all = TRUE) # sin duplicados por id
# 2) Revisión de NA en variables clave (numéricas y categóricas que usaremos)
vars_clave <- c("estrato","preciom","areaconst","parqueaderos","banios","habitaciones",
"tipo","zona","barrio","piso","longitud","latitud")
na_overview <- sapply(df_master[, vars_clave], function(x) sum(is.na(x)))
na_overview
## estrato preciom areaconst parqueaderos banios habitaciones
## 0 0 0 1602 0 0
## tipo zona barrio piso longitud latitud
## 0 0 0 2635 0 0
El objetivo del análisis es determinar si estos NA representan realmente propiedades sin parqueadero o son errores/omisiones en el registro, y definir la mejor estrategia para su imputación.
### Revisar casos con NA en parqueaderos ###
df_master %>%
filter(is.na(parqueaderos)) %>%
select(parqueaderos, tipo, estrato, areaconst, preciom) %>%
head(20)
## # A tibble: 20 × 5
## parqueaderos tipo estrato areaconst preciom
## <dbl> <chr> <dbl> <dbl> <dbl>
## 1 NA Casa 6 445 750
## 2 NA Apartamento 3 49 100
## 3 NA Casa 3 160 230
## 4 NA Apartamento 5 105 430
## 5 NA Casa 3 435 190
## 6 NA Casa 3 120 180
## 7 NA Casa 3 210 500
## 8 NA Apartamento 3 176 199
## 9 NA Apartamento 5 235 320
## 10 NA Casa 5 455 520
## 11 NA Apartamento 5 85 345
## 12 NA Apartamento 3 61 130
## 13 NA Apartamento 5 74 185
## 14 NA Apartamento 3 72 98
## 15 NA Apartamento 3 96 130
## 16 NA Apartamento 3 60 92
## 17 NA Apartamento 3 68 98
## 18 NA Casa 3 300 380
## 19 NA Apartamento 4 82 220
## 20 NA Apartamento 3 60 120
df_master %>%
filter(is.na(parqueaderos)) %>%
group_by(tipo, estrato) %>%
summarise(cantidad = n()) %>%
arrange(desc(cantidad))
## # A tibble: 8 × 3
## # Groups: tipo [2]
## tipo estrato cantidad
## <chr> <dbl> <int>
## 1 Casa 3 426
## 2 Apartamento 4 357
## 3 Apartamento 3 343
## 4 Casa 4 131
## 5 Casa 5 121
## 6 Apartamento 5 107
## 7 Apartamento 6 62
## 8 Casa 6 55
library(ggplot2)
ggplot(df_master %>% filter(is.na(parqueaderos)), aes(x = areaconst)) +
geom_histogram(binwidth = 5, fill = "steelblue") +
labs(title = "Distribución de área en inmuebles con NA en parqueaderos")
df_master %>%
group_by(is.na(parqueaderos)) %>%
summarise(
promedio_precio = mean(preciom, na.rm = TRUE),
promedio_area = mean(areaconst, na.rm = TRUE),
promedio_habit = mean(habitaciones, na.rm = TRUE)
)
## # A tibble: 2 × 4
## `is.na(parqueaderos)` promedio_precio promedio_area promedio_habit
## <lgl> <dbl> <dbl> <dbl>
## 1 FALSE 469. 181. 3.61
## 2 TRUE 287. 149. 3.58
df_master %>%
filter(is.na(parqueaderos)) %>%
count(zona, sort = TRUE)
## # A tibble: 5 × 2
## zona n
## <chr> <int>
## 1 Zona Norte 633
## 2 Zona Sur 621
## 3 Zona Oriente 188
## 4 Zona Oeste 100
## 5 Zona Centro 60
df_master %>%
filter(is.na(parqueaderos)) %>%
count(tipo, sort = TRUE)
## # A tibble: 2 × 2
## tipo n
## <chr> <int>
## 1 Apartamento 869
## 2 Casa 733
ggplot(df_master, aes(x = areaconst, y = preciom, color = is.na(parqueaderos))) +
geom_point(alpha = 0.5) +
labs(title = "Parqueaderos NA vs No NA")
df_master %>%
filter(is.na(parqueaderos)) %>%
count(estrato, sort = TRUE)
## # A tibble: 4 × 2
## estrato n
## <dbl> <int>
## 1 3 769
## 2 4 488
## 3 5 228
## 4 6 117
df_master %>%
mutate(precio_m2 = preciom / areaconst) %>%
group_by(is.na(parqueaderos)) %>%
summarise(
promedio_precio_m2 = mean(precio_m2, na.rm = TRUE),
n = n()
)
## # A tibble: 2 × 3
## `is.na(parqueaderos)` promedio_precio_m2 n
## <lgl> <dbl> <int>
## 1 FALSE 2.86 6717
## 2 TRUE 2.16 1602
df_master %>%
filter(tipo == "Casa") %>%
count(parqueaderos) %>%
arrange(parqueaderos)
## # A tibble: 11 × 2
## parqueaderos n
## <dbl> <int>
## 1 1 857
## 2 2 891
## 3 3 268
## 4 4 296
## 5 5 64
## 6 6 66
## 7 7 17
## 8 8 17
## 9 9 4
## 10 10 6
## 11 NA 733
df_master %>%
filter(tipo == "Apartamento") %>%
count(parqueaderos) %>%
arrange(parqueaderos)
## # A tibble: 9 × 2
## parqueaderos n
## <dbl> <int>
## 1 1 2298
## 2 2 1584
## 3 3 252
## 4 4 88
## 5 5 4
## 6 6 2
## 7 7 1
## 8 10 2
## 9 NA 869
df_master %>%
group_by(tipo, estrato, is.na(parqueaderos)) %>%
summarise(
promedio_area = mean(areaconst, na.rm = TRUE),
mediana_area = median(areaconst, na.rm = TRUE),
n = n()
) %>%
arrange(tipo, estrato, `is.na(parqueaderos)`)
## # A tibble: 16 × 6
## # Groups: tipo, estrato [8]
## tipo estrato `is.na(parqueaderos)` promedio_area mediana_area n
## <chr> <dbl> <lgl> <dbl> <dbl> <int>
## 1 Apartamento 3 FALSE 69.1 60 296
## 2 Apartamento 3 TRUE 65.2 60 343
## 3 Apartamento 4 FALSE 81.4 75 1047
## 4 Apartamento 4 TRUE 68.3 60 357
## 5 Apartamento 5 FALSE 111. 98 1659
## 6 Apartamento 5 TRUE 106. 90 107
## 7 Apartamento 6 FALSE 177. 157 1229
## 8 Apartamento 6 TRUE 181. 142. 62
## 9 Casa 3 FALSE 216. 187 388
## 10 Casa 3 TRUE 186. 154. 426
## 11 Casa 4 FALSE 227. 203 594
## 12 Casa 4 TRUE 264. 235 131
## 13 Casa 5 FALSE 287. 250 863
## 14 Casa 5 TRUE 282. 250 121
## 15 Casa 6 FALSE 381. 330 641
## 16 Casa 6 TRUE 391. 350 55
ggplot(df_master, aes(x = factor(estrato), y = areaconst,
fill = is.na(parqueaderos))) +
geom_boxplot() +
facet_wrap(~ tipo) +
labs(
title = "Área construida por estrato y tipo de vivienda",
x = "Estrato",
y = "Área construida"
)
df_master %>%
mutate(na_parqueaderos = ifelse(is.na(parqueaderos), "NA", "No NA")) %>%
count(tipo, estrato, na_parqueaderos) %>%
ggplot(aes(x = factor(estrato), y = tipo, fill = n)) +
geom_tile(color = "white") +
geom_text(aes(label = n), color = "black") +
facet_wrap(~ na_parqueaderos) +
labs(title = "Distribución por estrato, tipo de vivienda y NA en parqueaderos",
x = "Estrato", y = "Tipo de vivienda")
Los NA se distribuyen por estrato y tipo, con valores de área y precio similares a propiedades con 1 parqueadero.
Los NA no están distribuidos aleatoriamente:
En casas, hay NA en todos los estratos, pero son más frecuentes en estratos 3 y 4.
En apartamentos, los NA son frecuentes en estratos 3 y 4, y mucho menos en 5 y 6.
En la mayoría de estratos y tipos de vivienda, los NA tienen un área construida ligeramente menor que los no NA, pero no es una diferencia considerable para representar el patron de no parqueadero
El patrón sugiere omisión de registro y no ausencia real de parqueadero.
Con base en el análisis, lo más coherente sería imputar los NA usando la mediana de parqueaderos agrupada por tipo de vivienda y estrato. Esto preserva el patrón real, evita introducir valores inexistentes (no hay ninguna vivienda con parqueadero 0) y respeta la segmentación socioeconómica del dataset.
library(dplyr)
# Calcular la mediana por grupo (tipo + estrato) si tener en cuenta NA
medianas_parqueaderos <- df_master %>%
group_by(tipo, estrato) %>%
summarise(mediana_parq = median(parqueaderos, na.rm = TRUE)) %>%
mutate(mediana_parq = round(mediana_parq)) # redondear por si acaso
# Unir y reemplazar NA con la mediana del grupo
df_master <- df_master %>%
left_join(medianas_parqueaderos, by = c("tipo", "estrato")) %>%
mutate(parqueaderos = ifelse(is.na(parqueaderos), mediana_parq, parqueaderos)) %>%
select(-mediana_parq) # limpiar columna auxiliar
# Debe dar 0
sum(is.na(df_master$parqueaderos))
## [1] 0
# Cantidad de ceros
sum(df_master$parqueaderos == 0, na.rm = TRUE)
## [1] 0
# ¿Hay valores con decimales?
any(df_master$parqueaderos %% 1 != 0)
## [1] FALSE
# Conteo de NA por tipo de vivienda y estrato
df_master %>%
mutate(na_piso = is.na(piso)) %>%
count(tipo, estrato, na_piso) %>%
arrange(desc(n))
## # A tibble: 16 × 4
## tipo estrato na_piso n
## <chr> <dbl> <lgl> <int>
## 1 Apartamento 5 FALSE 1328
## 2 Apartamento 4 FALSE 1056
## 3 Apartamento 6 FALSE 894
## 4 Casa 5 FALSE 630
## 5 Casa 4 FALSE 460
## 6 Apartamento 3 FALSE 441
## 7 Apartamento 5 TRUE 438
## 8 Casa 6 FALSE 438
## 9 Casa 3 FALSE 437
## 10 Apartamento 6 TRUE 397
## 11 Casa 3 TRUE 377
## 12 Casa 5 TRUE 354
## 13 Apartamento 4 TRUE 348
## 14 Casa 4 TRUE 265
## 15 Casa 6 TRUE 258
## 16 Apartamento 3 TRUE 198
df_master %>%
mutate(na_piso = is.na(piso)) %>%
group_by(tipo, estrato, na_piso) %>%
summarise(
promedio_area = mean(areaconst, na.rm = TRUE),
mediana_area = median(areaconst, na.rm = TRUE),
n = n()
) %>%
arrange(tipo, estrato, na_piso)
## # A tibble: 16 × 6
## # Groups: tipo, estrato [8]
## tipo estrato na_piso promedio_area mediana_area n
## <chr> <dbl> <lgl> <dbl> <dbl> <int>
## 1 Apartamento 3 FALSE 66.2 60 441
## 2 Apartamento 3 TRUE 68.8 60 198
## 3 Apartamento 4 FALSE 77.4 71 1056
## 4 Apartamento 4 TRUE 80.1 71 348
## 5 Apartamento 5 FALSE 109. 96 1328
## 6 Apartamento 5 TRUE 114. 97 438
## 7 Apartamento 6 FALSE 175. 153 894
## 8 Apartamento 6 TRUE 181. 165 397
## 9 Casa 3 FALSE 199. 170 437
## 10 Casa 3 TRUE 202. 166 377
## 11 Casa 4 FALSE 238. 220 460
## 12 Casa 4 TRUE 227. 200 265
## 13 Casa 5 FALSE 287. 250 630
## 14 Casa 5 TRUE 285. 254. 354
## 15 Casa 6 FALSE 377. 326 438
## 16 Casa 6 TRUE 389. 349 258
# Para Casas
df_master %>%
filter(tipo == "Casa", !is.na(piso)) %>%
count(piso) %>%
arrange(piso)
## # A tibble: 8 × 2
## piso n
## <chr> <int>
## 1 01 430
## 2 02 938
## 3 03 524
## 4 04 62
## 5 05 3
## 6 06 2
## 7 07 4
## 8 10 2
Lo anterior sugiere que en casas, la variable piso probablemente no representa un nivel de edificio, sino el número de pisos que tiene la casa por lo cual no se debe imputar por 1.
Para Apartamentos, la variable piso casi siempre representa el nivel dentro de un edificio. Por lo cual se va a validar la distribución para determinar los pisos mas comunes y por estrato para encontrar algun patrón
df_master %>%
filter(tipo == "Apartamento", !is.na(piso)) %>%
count(estrato, piso) %>%
arrange(estrato, piso)
## # A tibble: 46 × 3
## estrato piso n
## <dbl> <chr> <int>
## 1 3 01 68
## 2 3 02 76
## 3 3 03 82
## 4 3 04 79
## 5 3 05 121
## 6 3 06 2
## 7 3 07 2
## 8 3 08 7
## 9 3 10 2
## 10 3 11 2
## # ℹ 36 more rows
library(dplyr)
library(ggplot2)
# Tabla por estrato y piso
tabla_pisos_apto <- df_master %>%
filter(tipo == "Apartamento", !is.na(piso)) %>%
count(estrato, piso) %>%
arrange(estrato, piso)
# Gráfico de distribución
df_master %>%
filter(tipo == "Apartamento", !is.na(piso)) %>%
ggplot(aes(x = as.numeric(piso), fill = as.factor(estrato))) +
geom_histogram(binwidth = 1, position = "dodge") +
labs(title = "Distribución de pisos en Apartamentos por estrato",
x = "Piso", y = "Cantidad", fill = "Estrato") +
theme_minimal()
Con el analisis por tipo de vivienda anterior, podemos elegir la tecnica de imputación:
# Guardar copia base antes de imputar
df_base <- df_master
# Calcular moda por tipo y estrato
moda_pisos <- df_master %>%
group_by(tipo, estrato) %>%
summarise(moda_piso = names(sort(table(piso), decreasing = TRUE))[1],
.groups = "drop")
# Calcular moda global para casas
moda_casa <- df_master %>%
filter(tipo == "Casa", !is.na(piso)) %>%
summarise(moda_piso_casa = names(sort(table(piso), decreasing = TRUE))[1]) %>%
pull(moda_piso_casa)
# Unir y reemplazar
df_master <- df_master %>%
left_join(moda_pisos, by = c("tipo", "estrato")) %>%
mutate(
piso = ifelse(tipo == "Casa" & is.na(piso), moda_casa,
ifelse(tipo == "Apartamento" & is.na(piso), moda_piso, piso))
) %>%
select(-moda_piso)
# Verificar que no haya NA en piso
sum(is.na(df_master$piso))
## [1] 0
vars_clave <- c("estrato","preciom","areaconst","parqueaderos","banios",
"habitaciones","piso","tipo","zona","barrio","longitud","latitud")
colSums(is.na(df_master[, vars_clave]))
## estrato preciom areaconst parqueaderos banios habitaciones
## 0 0 0 0 0 0
## piso tipo zona barrio longitud latitud
## 0 0 0 0 0 0
# Chequeos rápidos
str(df_master$piso)
## chr [1:8319] "02" "02" "02" "02" "01" "01" "01" "01" "02" "02" "02" "02" ...
summary(df_master$piso)
## Length Class Mode
## 8319 character character
df_master <- df_master %>%
mutate(
piso = as.integer(as.character(piso)),
parqueaderos = as.integer(parqueaderos)
)
# Revisar
str(df_master$piso)
## int [1:8319] 2 2 2 2 1 1 1 1 2 2 ...
summary(df_master$piso)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 1.000 2.000 3.000 3.507 5.000 12.000
Despues de limpiar el Dataset e imputar los datos NA guardamos una versión base antes de validar los outliers que nos sirvirá para trabajar para cada análisis requerido en el ejercicio. Este esun df limpio sin datos duplicados, Sin NA.
# Guardar dataset maestro limpio
df_master_base <- df_master
# guardar en archivo para no perderlo
save(df_master_base, file = "df_master_base.RData")
write.csv(df_master_base, "df_master_base.csv", row.names = FALSE)
# 1Crear copia del maestro limpio
df_outliers <- df_master_base
# Función para detectar outliers usando IQR
detectar_outliers <- function(x) {
if (is.numeric(x)) {
Q1 <- quantile(x, 0.25, na.rm = TRUE)
Q3 <- quantile(x, 0.75, na.rm = TRUE)
IQR_val <- Q3 - Q1
lim_inf <- Q1 - 1.5 * IQR_val
lim_sup <- Q3 + 1.5 * IQR_val
return(ifelse(x < lim_inf | x > lim_sup, TRUE, FALSE))
} else {
return(rep(FALSE, length(x)))
}
}
# Aplicar detección a todas las variables numéricas
outlier_flags <- as.data.frame(lapply(df_outliers, detectar_outliers))
# Contar outliers por variable
conteo_outliers <- colSums(outlier_flags)
conteo_outliers <- sort(conteo_outliers, decreasing = TRUE)
# Ver resumen
conteo_outliers
## habitaciones parqueaderos preciom areaconst piso longitud
## 888 567 552 382 297 130
## banios id zona estrato tipo barrio
## 72 0 0 0 0 0
## latitud
## 0
library(ggplot2)
library(tidyr)
# Filtrar variables con outliers, excluyendo longitud y latitud
variables_con_outliers <- names(conteo_outliers[conteo_outliers > 0])
variables_con_outliers <- setdiff(variables_con_outliers, c("longitud", "latitud"))
# Filtrar dataset para esas variables
df_outliers_plot <- df_outliers[ , variables_con_outliers]
# Pasar a formato largo para ggplot
df_outliers_long <- df_outliers_plot %>%
pivot_longer(cols = everything(), names_to = "Variable", values_to = "Valor")
# Graficar boxplots
ggplot(df_outliers_long, aes(x = Variable, y = Valor)) +
geom_boxplot(outlier.colour = "red", outlier.shape = 16) +
theme_minimal() +
labs(title = "Distribución y Outliers por Variable",
y = "Valor", x = "Variable") +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
library(ggplot2)
library(tidyr)
# Filtrar variables con outliers, excluyendo longitud y latitud
variables_con_outliers <- names(conteo_outliers[conteo_outliers > 0])
variables_con_outliers <- setdiff(variables_con_outliers, c("longitud", "latitud"))
# Filtrar dataset para esas variables
df_outliers_plot <- df_outliers[ , variables_con_outliers]
# Pasar a formato largo para ggplot
df_outliers_long <- df_outliers_plot %>%
pivot_longer(cols = everything(), names_to = "Variable", values_to = "Valor")
# Graficar boxplots pequeños por variable
ggplot(df_outliers_long, aes(x = "", y = Valor)) +
geom_boxplot(outlier.colour = "red", outlier.shape = 16) +
facet_wrap(~Variable, scales = "free", ncol = 4) +
theme_minimal() +
labs(title = "Outliers por Variable",
y = "Valor", x = "") +
theme(axis.text.x = element_blank(),
axis.ticks.x = element_blank())
Creamos una columna adicional en el df para identificar los outliers y no generar nuevos calculos ya que los outliers se deben tratar de forma difernte dependiendo el escenario.
Mostrramos la cantidad de outliers por variable
library(dplyr)
# 1) Partimos del maestro limpio
df_outliers <- df_master_base
# 2) Variables numéricas a evaluar (sin coordenadas)
vars <- c("estrato","preciom","areaconst","parqueaderos","banios","habitaciones","piso")
# 3) Función IQR para marcar outliers (TRUE/FALSE)
detect_outliers_iqr <- function(x){
q1 <- quantile(x, 0.25, na.rm = TRUE)
q3 <- quantile(x, 0.75, na.rm = TRUE)
iqr <- q3 - q1
li <- q1 - 1.5 * iqr
ls <- q3 + 1.5 * iqr
(x < li) | (x > ls)
}
# 4) Matriz de flags por variable
flag_mat <- sapply(vars, function(v) detect_outliers_iqr(df_outliers[[v]]))
flag_df <- as.data.frame(flag_mat)
names(flag_df) <- paste0(vars, "_out")
# 5) Agregar flags y el conteo por fila
df_outliers <- bind_cols(df_outliers, flag_df)
df_outliers$outlier_count <- rowSums(flag_df)
# 6) Chequeos rápidos
table(df_outliers$outlier_count) # distribución de # de outliers por fila
##
## 0 1 2 3 4 5
## 6380 1390 343 152 44 10
colSums(flag_df) # cuántos outliers por variable
## estrato_out preciom_out areaconst_out parqueaderos_out
## 0 552 382 567
## banios_out habitaciones_out piso_out
## 72 888 297
bad_idx <- with(df_master_base,
which(!is.finite(preciom) | preciom <= 0 |
!is.finite(areaconst) | areaconst <= 0))
length(bad_idx) # ¿cuántos?
## [1] 0
# vistazo
head(df_master_base[bad_idx, c("id","tipo","estrato","zona","preciom","areaconst")])
## # A tibble: 0 × 6
## # ℹ 6 variables: id <dbl>, tipo <chr>, estrato <dbl>, zona <chr>,
## # preciom <dbl>, areaconst <dbl>
Realiazamos los siguientes pasos:
Se preparan los datos para el PCA (Análisis de Componentes Principales) de manera que los resultados no se distorsionen por valores extremos o escalas diferentes.
Se realiza esta limpieza con dos tecnicas dependiendo de las variables:
Usamos Winsorizing con IQR:
Calculamos el rango intercuartílico (IQR) y establecemos límites inferior/superior. Los valores fuera de ese rango se recortaron al límite más cercano, esto para evitar valores exageradamente altos que suban la media.
Usamos log1p() para comprimir la escala de valores grandes, pero sin afectar tanto los valores pequeños, con esto evitamos que el precio o area de “domine” la varianza sobre uno menor o mayor ya que estas variables tienden a estar sesgadas.
Usamos scale() para que cada variable tenga: Media = 0, Desviación estándar = 1. Esto ya que el PCA se basa en varianzas y se deben estandarizar para que variables con valores mas grandes no dominen a las que tienen rangos mas pequeños.
# Reiniciar SIEMPRE desde el maestro limpio
df_pca <- df_master_base[, c("estrato","preciom","areaconst",
"parqueaderos","banios","habitaciones")]
# --- Winsor para conteos (IQR) ---
winsor_iqr <- function(x){
q1 <- quantile(x, 0.25, na.rm = TRUE); q3 <- quantile(x, 0.75, na.rm = TRUE)
iqr <- q3 - q1; li <- q1 - 1.5*iqr; ls <- q3 + 1.5*iqr
pmin(pmax(x, li), ls)
}
df_pca$parqueaderos <- winsor_iqr(df_pca$parqueaderos)
df_pca$banios <- winsor_iqr(df_pca$banios)
df_pca$habitaciones <- winsor_iqr(df_pca$habitaciones)
# (estrato no se toca aquí)
# --- Log a variables sesgadas ---
df_pca$preciom <- log1p(df_pca$preciom) # log(1 + x)
df_pca$areaconst <- log1p(df_pca$areaconst)
# --- Escalar todo (media 0, sd 1) ---
df_pca[] <- scale(df_pca)
pca_res <- prcomp(df_pca, center = FALSE, scale. = FALSE)
# Tabla compacta
var_exp <- pca_res$sdev^2
prop <- var_exp / sum(var_exp)
cumprop <- cumsum(prop)
round(rbind(StdDev = pca_res$sdev,
PropVar = prop,
CumVar = cumprop), 4)
## [,1] [,2] [,3] [,4] [,5] [,6]
## StdDev 1.9571 1.0834 0.6160 0.5318 0.4907 0.3051
## PropVar 0.6383 0.1956 0.0632 0.0471 0.0401 0.0155
## CumVar 0.6383 0.8340 0.8972 0.9444 0.9845 1.0000
# Scree (usa factoextra si lo tienes)
# install.packages("factoextra") # si falta
library(factoextra)
fviz_eig(pca_res, addlabels = TRUE)
PC1 = 63.8%, PC2 = 19.6%, con estos dos componentes ya explicamos el 83.4 de variación total en los precios.
loadings <- pca_res$rotation
# Top aportantes en PC1 y PC2
sort(loadings[,1], decreasing = TRUE) # PC1
## preciom areaconst banios parqueaderos estrato habitaciones
## 0.4734284 0.4519489 0.4465318 0.4131081 0.3250900 0.3096373
sort(loadings[,2], decreasing = TRUE) # PC2
## habitaciones areaconst banios preciom parqueaderos estrato
## 0.6623422 0.2284640 0.1784501 -0.1912170 -0.2301237 -0.6226891
# (opcional) contribución visual por eje
fviz_contrib(pca_res, choice = "var", axes = 1, top = 6)
fviz_contrib(pca_res, choice = "var", axes = 2, top = 6)
PC1 está armado sobre todo por precio, área, baños y parqueaderos PC2 está dominado por Habitaciones y estrato
# Solo variables (más limpio)
fviz_pca_var(pca_res, repel = TRUE)
# Variables + individuos (si no se satura tu plot)
fviz_pca_biplot(pca_res, repel = TRUE, alpha.ind = 0.2)
Tratamiento de outliers y escalar:
Tratamos los outliers en parqueaderos, banios, habitacionescon winsor IQR
A preciom y areaconst les aplicamos log1p() para comprimir proque hay pocos muy grandes.
Estandarizamos todo con scale() para que todas queden en la misma escala
library(dplyr)
# Partimos del maestro
df_for_clust <- df_master_base %>%
mutate(row_id = row_number()) %>%
select(row_id, id, tipo, zona, barrio,
estrato, preciom, areaconst, parqueaderos, banios, habitaciones)
# Función winsor IQR (suaviza extremos en conteos)
winsor_iqr <- function(x){
q1 <- quantile(x, 0.25, na.rm = TRUE); q3 <- quantile(x, 0.75, na.rm = TRUE)
iqr <- q3 - q1; li <- q1 - 1.5*iqr; ls <- q3 + 1.5*iqr
pmin(pmax(x, li), ls)
}
# Variables numéricas para k-means (mismas que en PCA)
X <- df_for_clust %>%
select(estrato, preciom, areaconst, parqueaderos, banios, habitaciones)
# Tratamiento suave de outliers + log a sesgadas
X$parqueaderos <- winsor_iqr(X$parqueaderos)
X$banios <- winsor_iqr(X$banios)
X$habitaciones <- winsor_iqr(X$habitaciones)
X$preciom <- log1p(X$preciom)
X$areaconst <- log1p(X$areaconst)
# Escalar (guardamos atributos para luego des-escalar centros)
X_mat <- scale(X) # matriz con atributos
X_sc <- as.data.frame(X_mat) # data frame para usar en funciones
# Chequeos
sapply(X_sc, function(x) sum(!is.finite(x))) # debe ser todo 0
## estrato preciom areaconst parqueaderos banios habitaciones
## 0 0 0 0 0 0
Se realiza el paso anterior ya que en el escenario de clustering si una variable tiene números grandes, dominaría el resultado.
Elegimos K pro metodo codo y Silhouette
library(factoextra)
# --- Método del codo (WSS) ---
set.seed(123)
fviz_nbclust(X_sc, kmeans, method = "wss") +
labs(title = "Método del codo (WSS)")
# --- Método de la silueta ---
set.seed(123)
fviz_nbclust(X_sc, kmeans, method = "silhouette") +
labs(title = "Silhouette promedio")
Probamos con k=3 y K=4 ya que k=2 podria ser poco detallado yt esconder subconjuntos en él.
library(factoextra)
library(cluster)
set.seed(123)
k <- 3
km3 <- kmeans(X_sc, centers = k, nstart = 50, iter.max = 100)
# Silhouette del modelo elegido
sil3 <- silhouette(km3$cluster, dist(X_sc))
mean_sil3 <- mean(sil3[, "sil_width"]); mean_sil3
## [1] 0.3449765
# Mapa de clusters
fviz_cluster(km3, data = X_sc, geom = "point", ellipse.type = "norm",
ggtheme = theme_minimal(), alpha = 0.6)
comp_k <- function(k){
set.seed(123)
fit <- kmeans(X_sc, centers = k, nstart = 50)
ms <- mean(silhouette(fit$cluster, dist(X_sc))[, "sil_width"])
sizes <- fit$size
list(k=k, mean_sil=round(ms,3), sizes=sizes, model=fit)
}
res2 <- comp_k(2); res3 <- comp_k(3); res4 <- comp_k(4)
res2[c("k","mean_sil","sizes")]
## $k
## [1] 2
##
## $mean_sil
## [1] 0.388
##
## $sizes
## [1] 4805 3514
res3[c("k","mean_sil","sizes")]
## $k
## [1] 3
##
## $mean_sil
## [1] 0.345
##
## $sizes
## [1] 4345 1448 2526
res4[c("k","mean_sil","sizes")]
## $k
## [1] 4
##
## $mean_sil
## [1] 0.319
##
## $sizes
## [1] 3251 1454 2430 1184
# 1) Añadir etiqueta de cluster al dataset original
df_clusters <- df_for_clust %>%
mutate(cluster = factor(km3$cluster))
# 2) Resumen numérico por cluster (medianas en unidades originales)
perfil_num <- df_clusters %>%
group_by(cluster) %>%
summarise(
n = n(),
precio_med = median(preciom, na.rm = TRUE),
area_med = median(areaconst, na.rm = TRUE),
estrato_med = median(estrato, na.rm = TRUE),
banios_med = median(banios, na.rm = TRUE),
parq_med = median(parqueaderos, na.rm = TRUE),
hab_med = median(habitaciones, na.rm = TRUE),
.groups = "drop"
) %>%
arrange(desc(area_med))
perfil_num
## # A tibble: 3 × 8
## cluster n precio_med area_med estrato_med banios_med parq_med hab_med
## <fct> <int> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 3 2526 690 234 6 4 2 4
## 2 2 1448 360 225 4 4 1 5
## 3 1 4345 230 82 4 2 1 3
# (Opcional) centros del k-means en unidades reales (promedios)
cent_scaled <- km3$centers
cent_unscaled <- sweep(cent_scaled, 2, attr(X_mat, "scaled:scale"), "*")
cent_unscaled <- sweep(cent_unscaled, 2, attr(X_mat, "scaled:center"), "+")
cent_unscaled[,"preciom"] <- expm1(cent_unscaled[,"preciom"])
cent_unscaled[,"areaconst"] <- expm1(cent_unscaled[,"areaconst"])
round(cent_unscaled, 2)
## estrato preciom areaconst parqueaderos banios habitaciones
## 1 4.26 217.30 83.58 1.18 2.13 2.84
## 2 3.96 367.42 224.01 1.42 3.75 4.74
## 3 5.66 730.17 244.59 2.52 4.38 3.82
# 3) Distribución por tipo y zona (porcentajes dentro de cada cluster)
dist_tipo <- df_clusters %>%
count(cluster, tipo) %>%
group_by(cluster) %>% mutate(pct = round(100*n/sum(n),1)) %>% ungroup()
dist_tipo
## # A tibble: 6 × 4
## cluster tipo n pct
## <fct> <chr> <int> <dbl>
## 1 1 Apartamento 3664 84.3
## 2 1 Casa 681 15.7
## 3 2 Apartamento 184 12.7
## 4 2 Casa 1264 87.3
## 5 3 Apartamento 1252 49.6
## 6 3 Casa 1274 50.4
dist_zona <- df_clusters %>%
count(cluster, zona) %>%
group_by(cluster) %>% mutate(pct = round(100*n/sum(n),1)) %>% ungroup()
dist_zona
## # A tibble: 15 × 4
## cluster zona n pct
## <fct> <chr> <int> <dbl>
## 1 1 Zona Centro 50 1.2
## 2 1 Zona Norte 1200 27.6
## 3 1 Zona Oeste 352 8.1
## 4 1 Zona Oriente 160 3.7
## 5 1 Zona Sur 2583 59.4
## 6 2 Zona Centro 72 5
## 7 2 Zona Norte 344 23.8
## 8 2 Zona Oeste 77 5.3
## 9 2 Zona Oriente 190 13.1
## 10 2 Zona Sur 765 52.8
## 11 3 Zona Centro 2 0.1
## 12 3 Zona Norte 376 14.9
## 13 3 Zona Oeste 769 30.4
## 14 3 Zona Oriente 1 0
## 15 3 Zona Sur 1378 54.6
library(ggplot2)
ggplot(dist_tipo, aes(x = cluster, y = pct, fill = tipo)) +
geom_col(position = "fill") +
scale_y_continuous(labels = scales::percent_format(scale = 1)) +
labs(title = "Composición por TIPO dentro de cada cluster", x = "Cluster", y = "%") +
theme_minimal()
ggplot(dist_zona, aes(x = cluster, y = pct, fill = zona)) +
geom_col(position = "fill") +
scale_y_continuous(labels = scales::percent_format(scale = 1)) +
labs(title = "Composición por ZONA dentro de cada cluster", x = "Cluster", y = "%") +
theme_minimal()
El mapa de clusters muestra buena separación (silhouette = 0.345 es aceptable).
Cluster 1 se ubica a la izquierda del plano (menor PC1 = más pequeño/menor precio).
Cluster 3 a la derecha (mayor PC1 = grande/caro y mayor estrato).
Cluster 2 queda intermedio en precio pero alto en numero de habitaciones (familiar).
Explicación de clusters:
Cluster 1 pequeño
Tamaño del grupo: 4,345 52%.
Producto típico: 84% apartamentos.
Perfil numérico: área 84 m², precio 217 Millones, estrato 4, 2 baños, 1 parqueadero, 3 habitaciones.
Zonas más frecuentes: Sur 59% y Norte 28%.
Este cluster es de unidades pequeñas y de ticket bajo/medio, compradores sensibles al precio.Se sugieren paquetes de financiación y cierre rápid, les interesa entre 2–3 habitaciones y 1 parqueadero. Ideal generar campañas masivas en Zona Sur y Norte, estas viviendas son de rotación alta.
Cluster 2 — Familiar amplio (mayoría casas)
Tamaño del grupo: 1,448 17%.
Producto típico: 87% casas.
Perfil numérico: área 224 m², precio 360 Millones, estrato 4, 4 baños, 1 o 2 parqueaderos, 5 habitaciones.
Zonas más frecuentes: Sur 53%, Norte 24%, Oriente 13%.
Este cluster sugiere una vivienda familiar grande, ticket medio, patio y más dormitorios pesan mucho en la decisión. Se enfatiza en el numero de habitaciones y espacios sociale. Mejora en cocina y patio. Se puede llegar con campañas dirigidas a familias en Sur y Norte
Cluster 3 — Premium alto estrato (grande y caro)
Tamaño del grupo: 2,526 30%.
Producto típico: mixto 50% apartamento y 50% casa.
Perfil numérico: área 235–245 m², precio 690–730 M, estrato 6, 4 baños, 2–3 parqueaderos, 4 habitaciones.
Zonas más frecuentes: Sur 55%, Oeste 30% y algo de Norte 15%.
Portafolio premium: alto valor agregado y menor elasticidad al precio. Se destacan mas espacios, seguridad, acabados.
# % de estrato dentro de cada cluster
dist_estrato <- df_clusters %>%
count(cluster, estrato) %>%
group_by(cluster) %>%
mutate(pct = round(100*n/sum(n),1)) %>%
ungroup()
dist_estrato
## # A tibble: 12 × 4
## cluster estrato n pct
## <fct> <dbl> <int> <dbl>
## 1 1 3 941 21.7
## 2 1 4 1567 36.1
## 3 1 5 1593 36.7
## 4 1 6 244 5.6
## 5 2 3 510 35.2
## 6 2 4 498 34.4
## 7 2 5 429 29.6
## 8 2 6 11 0.8
## 9 3 3 2 0.1
## 10 3 4 64 2.5
## 11 3 5 728 28.8
## 12 3 6 1732 68.6
# (opcional) gráfico apilado
library(ggplot2)
ggplot(dist_estrato, aes(x = cluster, y = pct, fill = factor(estrato))) +
geom_col(position = "fill") +
scale_y_continuous(labels = scales::percent_format(scale = 1)) +
labs(title = "Composición por ESTRATO en cada cluster", x = "Cluster", y = "%", fill = "Estrato") +
theme_minimal()
# pruebas no paramétricas por variable clave
vars_test <- c("preciom","areaconst","estrato","banios","parqueaderos","habitaciones")
kw <- lapply(vars_test, function(v){
f <- as.formula(paste(v, "~ cluster"))
list(var=v, test=kruskal.test(f, data = df_clusters))
})
# ver p-values
sapply(kw, function(x) paste0(x$var, ": p=", signif(x$test$p.value, 3)))
## [1] "preciom: p=0" "areaconst: p=0" "estrato: p=0"
## [4] "banios: p=0" "parqueaderos: p=0" "habitaciones: p=0"
Los tres clusters son estadisticamente diferentes, representan patrones reales y representan perfiles diferentes.. Los perfiles tienen sentido, son de tamaño razonable y funcionan para segmentar y tomar decisiones sobre que ofrecer y donde.
Limpiamos el dataframe master sin outliers y solo con las varaibles categoricas que vamos a utilziar
library(dplyr)
# 0.1) Nos quedamos con las variables categóricas necesarias
df_ca <- df_master_base %>%
select(tipo, zona, barrio) %>%
filter(!is.na(tipo), !is.na(zona), !is.na(barrio))
# 0.2) Agrupamos barrios con muy pocos casos a "Otros" para que el mapa sea legible
umbral <- 30 # ajusta si salen demasiadas categorías
barrios_ok <- df_ca %>% count(barrio, sort = TRUE) %>% filter(n >= umbral) %>% pull(barrio)
df_ca <- df_ca %>%
mutate(barrio2 = if_else(barrio %in% barrios_ok, barrio, "Otros"))
library(ggplot2)
# 1.1) Tabla de frecuencias tipo × zona
tab_tz <- table(df_ca$tipo, df_ca$zona)
tab_tz
##
## Zona Centro Zona Norte Zona Oeste Zona Oriente Zona Sur
## Apartamento 24 1198 1029 62 2787
## Casa 100 722 169 289 1939
# 1.2) Prueba de independencia chi-cuadrado
chi_tz <- chisq.test(tab_tz)
chi_tz$p.value # p < 0.05 => hay asociación (no independencia)
## [1] 3.207561e-148
# 1.3) Heatmap de residuales estandarizados (más/menos de lo esperado)
res_tz <- as.data.frame(as.table(chi_tz$stdres))
names(res_tz) <- c("tipo","zona","stdres")
ggplot(res_tz, aes(x = zona, y = tipo, fill = stdres)) +
geom_tile(color = "white") +
scale_fill_gradient2(low = "#d73027", mid = "white", high = "#1a9850", midpoint = 0) +
labs(title = "Residuales estandarizados (χ²) — Tipo × Zona",
x = "Zona", y = "Tipo", fill = "Std. resid") +
theme_minimal()
Se evidencia relación entre tipo de vivienda y zona (no son independientes). Existe una asociación muy significativa entre tipo y zona. Los apartamentos se concentran más en Zona Oeste y menos en Centro,Oriente, Sur. Las casas aparecen más de lo esperado en Centro,Oriente,Sur y menos en Oeste.
# --- CA: Tipo × Zona (1 dimensión) ---
library(dplyr)
library(ggplot2)
library(FactoMineR)
# 1) Tabla de contingencia
tab_tz <- with(df_master, table(tipo, zona))
print(tab_tz)
## zona
## tipo Zona Centro Zona Norte Zona Oeste Zona Oriente Zona Sur
## Apartamento 24 1198 1029 62 2787
## Casa 100 722 169 289 1939
# 2) CA (sin gráficos automáticos)
ca_tz <- CA(tab_tz, graph = FALSE)
ca_tz$eig # ver inercia; habrá solo "Dim 1"
## eigenvalue percentage of variance cumulative percentage of variance
## dim 1 0.08305442 100 100
# 3) Filas (TIPO) - coord 1D robusto
row_coord <- ca_tz$row$coord # puede ser vector o matriz
row_lab <- if (!is.null(rownames(row_coord))) rownames(row_coord) else names(row_coord)
row_df <- data.frame(
tipo = row_lab,
Dim1 = as.numeric(drop(row_coord)), # <- aquí la magia
row.names = NULL
)
ggplot(row_df, aes(x = Dim1, y = tipo, color = tipo, label = tipo)) +
geom_point(size = 3) +
geom_vline(xintercept = 0, linetype = 3) +
geom_text(nudge_x = 0.03, show.legend = FALSE) +
scale_color_manual(values = rep("#2E86AB", nrow(row_df))) +
labs(title = "CA Tipo × Zona — Filas (TIPO) en Dim 1", x = "Dim 1", y = NULL) +
theme_minimal()
# 4) Columnas (ZONA) - coord 1D robusto
col_coord <- ca_tz$col$coord
col_lab <- if (!is.null(rownames(col_coord))) rownames(col_coord) else names(col_coord)
col_df <- data.frame(
zona = col_lab,
Dim1 = as.numeric(drop(col_coord)),
row.names = NULL
)
ggplot(col_df, aes(x = Dim1, y = zona, color = zona, label = zona)) +
geom_point(size = 3) +
geom_vline(xintercept = 0, linetype = 3) +
geom_text(nudge_x = 0.03, show.legend = FALSE) +
labs(title = "CA Tipo × Zona — Columnas (ZONA) en Dim 1", x = "Dim 1", y = NULL) +
theme_minimal()
# 5) (Opcional) Mapa 1D conjunto
rows <- row_df %>% transmute(etiqueta = tipo, Dim1, grupo = "Tipo")
cols <- col_df %>% transmute(etiqueta = zona, Dim1, grupo = "Zona")
both <- bind_rows(rows, cols)
ggplot(both, aes(x = Dim1, y = 0, color = grupo, label = etiqueta)) +
geom_point(size = 3) +
geom_vline(xintercept = 0, linetype = 3) +
geom_text(nudge_y = 0.05, show.legend = FALSE) +
scale_color_manual(values = c("Tipo" = "#2E86AB", "Zona" = "#E67E22")) +
labs(title = "CA Tipo × Zona — Mapa 1D (Dim 1)", x = "Dim 1", y = NULL) +
theme_minimal() +
theme(axis.text.y = element_blank(),
axis.ticks.y = element_blank(),
panel.grid.major.y = element_blank())
Análisis:
# Tipo x Barrio por ZONA (Top-k)
# ===============================
library(dplyr)
library(ggplot2)
library(forcats)
library(scales)
library(ragg) # salida nítida y segura en UTF-8
# Parámetros
thr <- 50 # mínimo de registros por barrio
top_k <- 15 # top-k barrios por zona por mayor |desviación|
# Base limpia
base <- df_master %>%
mutate(
barrio = trimws(barrio),
barrio = if_else(is.na(barrio) | barrio == "", "Sin barrio", barrio)
) %>%
filter(!is.na(zona), !is.na(tipo))
# Conteos por zona-barrio-tipo
cont <- base %>% count(zona, barrio, tipo, name = "n")
# Totales por barrio
tot_barrio <- cont %>%
group_by(zona, barrio) %>%
summarise(n_total = sum(n), .groups = "drop")
# % de Casas por barrio (solo barrios con n_total >= thr)
p_casa_barrio <- cont %>%
filter(tipo == "Casa") %>%
right_join(tot_barrio, by = c("zona", "barrio")) %>%
mutate(
n = replace_na(n, 0L),
p_casa = n / n_total
) %>%
filter(n_total >= thr)
# % de Casas promedio ponderado por zona
p_casa_zona <- p_casa_barrio %>%
group_by(zona) %>%
summarise(p_casa_zona = weighted.mean(p_casa, w = n_total), .groups = "drop")
# Desviación y Top-k por zona
plot_df <- p_casa_barrio %>%
left_join(p_casa_zona, by = "zona") %>%
mutate(
dev = p_casa - p_casa_zona,
# <- SIN ACENTOS para evitar el crash del viewer
quien = if_else(dev >= 0, "Mas Casa", "Mas Apartamento")
) %>%
group_by(zona) %>%
slice_max(order_by = abs(dev), n = top_k, with_ties = FALSE) %>%
mutate(barrio = fct_reorder(barrio, dev)) %>%
ungroup()
# Gráfico tipo lollipop por zona
p <- ggplot(plot_df, aes(x = dev, y = barrio, color = quien)) +
geom_segment(aes(x = 0, xend = dev, yend = barrio), linewidth = 1, alpha = 0.9) +
geom_point(size = 2.6) +
facet_wrap(~ zona, scales = "free_y") +
geom_vline(xintercept = 0, linetype = 3) +
scale_color_manual(values = c("Mas Casa" = "#2ecc71", "Mas Apartamento" = "#e74c3c")) +
scale_x_continuous(labels = percent_format(accuracy = 1)) +
labs(
title = "Tipo × Barrio por zona — desviación de % Casas vs promedio de la zona",
subtitle = paste0("Se muestran los ", top_k,
" barrios con mayor |desviación| por zona (n ≥ ", thr, ")."),
x = "Desviación de % Casas (barrio − media zonal)",
y = "Barrio",
color = NULL
) +
theme_minimal(base_size = 11) +
theme(
legend.position = "bottom",
panel.grid.major.y = element_blank(),
panel.grid.minor = element_blank()
)
# ==== OPCIÓN A (recomendada): solo guardar con ragg (sin imprimir al viewer) ====
ragg::agg_png("tipo_x_barrio_por_zona.png", width = 1400, height = 900, res = 150)
print(p)
dev.off()
## png
## 2
# ==== OPCIÓN B (si quieres ver en pantalla) ====
# Como ya quitamos acentos, esto suele funcionar:
print(p)
Zona Norte
Foco Casas (verde fuerte): Villa del Prado (muy por encima del promedio), seguido por Acopi. Un buen lugar paraproyectos de casas y campañas con mensajes de “casas con patio y mas espacio para la familia”.
Foco Apartamentos (rojo): Torres de Comfandi, Versalles, Prados del Norte, La Flora. Zonas con mayor demanda por apartamentos: torres, con precios competitivo.
Zona Oeste
Bajo perfil para Casas: Cristales, Los Cristales, Aguacatal.
Bajo perfil para Apartamentos: El Peñón, Santa Teresita, Normandía.
Para esta zona conviene segmentar por estrato y precio además del tipo, porque la preferencia por producto no es tan extrema como en Norte o Sur.
Zona Sur
-Foco Casas: Ciudad 2000, Parcelaciones Pance, Ciudad Jardín, Pance, Nuevo Tequendama, El Limonar. -Se puede generar oferta con tickets altos para Pance y Ciudad Jardín.
El comportamiento del mercado se explica sobre todo por tamaño/valor de la vivienda (área, precio, baños, parqueaderos) y, en segundo lugar, por habitaciones/estrato.
Se identificaron tres segmentos consistentes y útiles para el análisis.
Hay zonas y barrios donde se favorece claramente la venta de casas y otras donde predominan los apartamentos. Alinear producto y geografía mejora velocidad de venta y margen.
PCA
Componente 1 (63.8% de la varianza): representa tamaño/valor. Aporta más precio, área, baños y parqueaderos.
Componente 2 (19.6%): diferencia composición/estrato, con mayor peso de habitaciones y estrato.
Lo que más separa las viviendas es su tamaño/valor, después, su perfil familiar y estrato.
Clusterización
Apartamentos medianos (Cluster 1): tamaño: 83–90 m², No de habitaciones: 2-3, baños: 2, 1 parqueadero, estrato 4, precio medio 220 a 370. Ruerte presencia en zona Sur 59% y Norte 28%.
Casas familiares (Cluster 2): Tamaño: 220–225 m², No de habitaciones: 4–5, 4 baños, 1 o 2 parqueaderos, estrato 4, precio alto 360.Mayor presencia en Sur 53%, en el Norte 24% y Oriente 13%.
Alto valor (Cluster 3): Tamaño: 240–250 m²,4 baños, mas de 2 parqueaderos, estrato alto 5–6, precio alto promedio o superior a 730. 50% casas y 50% apartamentos, fuerte en Sur 55% y Oeste 30%.
Tipo x Zona (Chi Cuadrado)
La probabilidad de encontrar más casas o más apartamentos depende de la zona.
Tipo x barro
Casas: Villa del Prado, Acopi.
Apartamentos: Torres de Comfandi, Versalles, Prados del Norte.
Casas: Cristales, Los Cristales, Aguacatal.
Apartamentos: El Peñón, Santa Teresita, Normandía.
Casas: Ciudad 2000, Parcelaciones Pance, Ciudad Jardín, Pance, El Limonar.
Apartamentos: Valle del Lili, Caney, Meléndez, Capri, La Hacienda.
Para organizar el protafolio es importante priorizar zona por tipo de vivienda y estrato. Si son casas priorizar zona sur en barrios como pance, ciudad jardin, limonar y ciudad 2000 o en el Norte barrios como villa del prado y acopi y en la zona Oeste barrio cristales
Si son apartamentos, tambien se prioriza en zona sur pero en barrios como Valle de lili, caney, melendez, capri. en el norte barrios como Torres de comfandi, Versalles, ¨Prados norte. En el Oeste barrios como El peñon y santa teresita.
Si se quiere priorizar o vender viviendas de alto valor, se debe priorizar la zona Sur y Oeste.