Resumen para dirección
Cobertura del análisis. Se revisaron 8322 inmuebles; tras el saneo de calidad quedaron 6694 (depuración del 19.6% por reglas simples y control de valores extremos).
Qué mueve el precio. El factor más importante está asociado a tamaño y calidad del inmueble (área, baños, habitaciones, parqueaderos) y explica 50.2% de las diferencias observadas entre propiedades (detalle técnico en el anexo).
Segmentación del mercado. Identificamos 3
grupos operativos con niveles de precio y
tamaño claramente diferenciados.
- Mediana de precio por grupo (Millones COP): 470 /
1000 / 250
- Mediana de área (m²): 180 / 360 / 85
Zonas de mayor valor. Top-3 por precio mediano: Zona Oeste, Zona Sur, Zona Norte.
Qué hacer ahora (resumen).
- Ajustar y revisar precios usando los segmentos
detectados.
- Enfocar compras y campañas en zonas y tipos
de vivienda que mejor funcionan.
- Mantener una oferta equilibrada entre segmentos para
no depender de uno solo.
- Mejorar la calidad del dato (área > 10 m², estrato
1–6 y el piso escrito de forma consistente).
Este informe analiza la base de datos
paqueteMODELOS::vivienda
para entender patrones del mercado
inmobiliario urbano: limpieza y trazabilidad de datos, EDA, ACP (PCA),
Clustering, Análisis de Correspondencias (CA/MCA) y visualizaciones para
comunicar hallazgos y generar estrategias para la dirección.
# Si faltan, instalar antes de knit (descomentando):
# install.packages(c("tidyverse","janitor","skimr","naniar","scales","GGally",
# "corrplot","factoextra","FactoMineR","NbClust","cluster",
# "leaflet","gridExtra","viridis","broom"))
library(tidyverse)
library(janitor)
library(skimr)
library(naniar)
library(scales)
library(GGally)
library(corrplot)
library(factoextra)
library(FactoMineR)
library(NbClust)
library(cluster)
library(leaflet)
library(gridExtra)
library(viridis)
library(broom)
library(paqueteMODELOS)
data("vivienda")
# Estandarizar nombres
vivienda <- vivienda |> janitor::clean_names()
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…
Variables:
Numéricas – preciom
, areaconst
,
estrato
, parqueaderos
, banios
,
habitaciones
.
Categóricas – zona
, tipo
, barrio
,
piso
(texto).
Geográficas – latitud
, longitud
.
naniar::miss_var_summary(vivienda) |> head(10)
tibble(variable = names(vivienda), clase = sapply(vivienda, class))
# Duplicados por id
vivienda |> count(id) |> filter(n > 1) |> nrow()
## [1] 1
winsor_iqr <- function(x, k = 3) {
q <- quantile(x, probs = c(0.25, 0.75), na.rm = TRUE)
iqr <- q[2] - q[1]; low <- q[1] - k * iqr; high <- q[2] + k * iqr
pmin(pmax(x, low), high)
}
vivienda_limp <- vivienda |>
mutate(
piso_num = suppressWarnings(readr::parse_number(piso)),
zona = as.factor(zona),
tipo = as.factor(tipo),
barrio = as.factor(barrio)
) |>
filter(
!is.na(preciom), preciom > 0,
!is.na(areaconst), areaconst > 10,
estrato %in% 1:6, banios >= 0,
habitaciones >= 1, parqueaderos >= 0
) |>
mutate(
preciom_w = winsor_iqr(preciom, 3),
areaconst_w = winsor_iqr(areaconst, 3),
banios_w = winsor_iqr(banios, 3),
habitaciones_w = winsor_iqr(habitaciones, 3),
parqueaderos_w = winsor_iqr(parqueaderos, 3)
)
n_raw <- nrow(vivienda); n_clean <- nrow(vivienda_limp)
tribble(~Etapa, ~Filas, "Original", n_raw, "Limpieza", n_clean)
skimr::skim(vivienda_limp |> select(preciom, areaconst, estrato, banios, habitaciones, parqueaderos, piso_num))
Name | select(…) |
Number of rows | 6694 |
Number of columns | 7 |
_______________________ | |
Column type frequency: | |
numeric | 7 |
________________________ | |
Group variables | None |
Variable type: numeric
skim_variable | n_missing | complete_rate | mean | sd | p0 | p25 | p50 | p75 | p100 | hist |
---|---|---|---|---|---|---|---|---|---|---|
preciom | 0 | 1.00 | 468.68 | 335.05 | 58 | 246.5 | 355 | 580 | 1999 | ▇▃▁▁▁ |
areaconst | 0 | 1.00 | 180.96 | 144.03 | 30 | 86.0 | 130 | 232 | 1745 | ▇▁▁▁▁ |
estrato | 0 | 1.00 | 4.83 | 0.95 | 3 | 4.0 | 5 | 6 | 6 | ▂▅▁▇▆ |
banios | 0 | 1.00 | 3.26 | 1.38 | 0 | 2.0 | 3 | 4 | 10 | ▆▇▃▁▁ |
habitaciones | 0 | 1.00 | 3.62 | 1.35 | 1 | 3.0 | 3 | 4 | 10 | ▁▇▂▁▁ |
parqueaderos | 0 | 1.00 | 1.83 | 1.12 | 1 | 1.0 | 2 | 2 | 10 | ▇▁▁▁▁ |
piso_num | 1903 | 0.72 | 3.88 | 2.67 | 1 | 2.0 | 3 | 5 | 12 | ▇▃▁▁▁ |
vivienda_limp |>
select(preciom_w, areaconst_w, estrato, banios_w, habitaciones_w, parqueaderos_w) |>
pivot_longer(everything(), names_to = "variable", values_to = "valor") |>
ggplot(aes(valor)) +
geom_histogram(bins = 30) +
facet_wrap(~ variable, scales = "free") +
labs(title = "Distribuciones (winsorizadas)", x = NULL, y = "Frecuencia")
Comentario (EDA – distribuciones).
La gráfica muestra que las variables se concentran en rangos medios y
aparecen algunos valores altos, propios del mercado (precios y áreas
grandes). Tras la limpieza no vemos valores imposibles (como áreas
negativas), lo que da confianza para usar estos datos en decisiones.
vivienda_limp |>
ggplot(aes(x = factor(estrato), y = preciom_w)) +
geom_boxplot() +
scale_y_continuous(labels = scales::label_number(scale_cut = scales::cut_short_scale())) +
labs(title = "Precio por estrato", x = "Estrato", y = "Precio (millones)")
Comentario (Precio por estrato).
La gráfica muestra que el precio sube a medida que
aumenta el estrato. En los estratos altos el precio es
mayor y suele ser más estable; en los bajos hay más variación y valores
menores.
vivienda_limp |>
ggplot(aes(areaconst_w, preciom_w, color = factor(estrato))) +
geom_point(alpha = 0.5) +
scale_y_continuous(labels = scales::label_number(scale_cut = scales::cut_short_scale())) +
labs(title = "Área vs precio", x = "Área (m²)", y = "Precio (millones)", color = "Estrato")
Comentario (Área vs precio).
La gráfica muestra que a mayor área, mayor precio.
Además, los puntos de estratos altos se ubican en la zona de precios
superiores, lo que refuerza que tamaño y
estrato empujan el valor.
num_mat <- vivienda_limp |>
select(preciom_w, areaconst_w, estrato, banios_w, habitaciones_w, parqueaderos_w, piso_num) |>
drop_na() |>
as.matrix()
cor_mat <- cor(num_mat)
corrplot::corrplot(cor_mat, method = "color", type = "upper", addCoef.col = "black", tl.cex = 0.8)
Comentario (Matriz de correlaciones).
La gráfica muestra que el precio está directamente
relacionado con área, baños,
habitaciones y parqueaderos; el
estrato también aporta en positivo. Son las palancas
principales del valor.
map_df <- vivienda_limp |> filter(!is.na(latitud), !is.na(longitud))
leaflet(map_df) |>
addTiles() |>
addCircleMarkers(~longitud, ~latitud, radius = 4, stroke = FALSE, fillOpacity = 0.6,
popup = ~paste0("<b>", tipo, "</b><br>Zona: ", zona,
"<br>Precio: ", scales::number(preciom_w, accuracy = 1), " M",
"<br>Área: ", areaconst_w, " m²"))
Comentario (Mapa).
El mapa permite ubicar la oferta en la ciudad y cruzar ubicación
con precio y área. Esto ayuda a priorizar zonas y entender
dónde se concentran las propiedades de mayor valor.
vars_num <- vivienda_limp |>
select(preciom_w, areaconst_w, estrato, banios_w, habitaciones_w, parqueaderos_w, piso_num) |>
drop_na()
vars_num_s <- scale(vars_num)
# PCA con precio
pca_con <- prcomp(vars_num_s, center = TRUE, scale. = TRUE)
# PCA sin precio
vars_sin_precio <- vars_num |> select(-preciom_w) |> scale()
pca_sin <- prcomp(vars_sin_precio, center = TRUE, scale. = TRUE)
fviz_eig(pca_con, addlabels = TRUE)
Comentario (Varianza por componente).
La gráfica resume qué tanto aporta cada factor del
análisis. Los primeros componentes concentran la mayor parte de la
información, por lo que con pocos factores ya
entendemos gran parte del comportamiento del mercado.
fviz_pca_biplot(pca_sin, repel = TRUE, col.var = "#2c7fb8", col.ind = "#7fcdbb",
title = "Biplot PCA (sin precio)")
Comentario (Biplot PCA).
La gráfica posiciona propiedades y variables en dos ejes de sentido
económico. El primer eje ordena principalmente por
tamaño/calidad (área y amenidades); el segundo
diferencia otros rasgos de oferta. Las flechas indican hacia dónde crece
cada característica.
cargas <- pca_con$rotation %>% # matriz de cargas
as.data.frame() %>%
tibble::rownames_to_column("variable") %>% # pasar nombres de fila a columna
tidyr::pivot_longer(
cols = -variable,
names_to = "component",
values_to = "value"
) %>%
dplyr::arrange(component, dplyr::desc(abs(value)))
head(cargas, 20)
# Varianza explicada
eig_con <- factoextra::get_eigenvalue(pca_con)
eig_sin <- factoextra::get_eigenvalue(pca_sin)
Resultados PCA (dinámico): - PC1 explica 50.2% (tamaño/calidad: área, baños, habitaciones, parqueaderos) y suele alinearse positivamente con el precio. - PC2 explica 21.2% (contraste de amenidades/verticalidad vs. estrato). - Incluyendo precio en la matriz de variables del PCA, PC1 explica 53.1%.
vivienda_ix <- vivienda_limp |> mutate(rowid = dplyr::row_number())
clust_df <- vivienda_ix |>
select(rowid, preciom_w, areaconst_w, estrato, banios_w, habitaciones_w, parqueaderos_w, piso_num) |>
drop_na()
clust_mat <- scale(clust_df |> select(-rowid))
fviz_nbclust(clust_mat, kmeans, method = "wss") + ggtitle("K óptimo — método del codo")
Comentario (Método del codo).
La gráfica sugiere un punto de equilibrio alrededor de
3 grupos: más segmentos ya no reducen sustancialmente el error y
complican la lectura para negocio.
fviz_nbclust(clust_mat, kmeans, method = "silhouette") + ggtitle("K óptimo — silhouette")
Comentario (Silhouette).
La gráfica confirma que el número de grupos elegido separa de
forma razonable los perfiles; hay distancia útil entre
segmentos.
K <- 3; set.seed(123)
km <- kmeans(clust_mat, centers = K, nstart = 25)
seg_df_full <- vivienda_ix |>
inner_join(tibble::tibble(rowid = clust_df$rowid, segmento = factor(km$cluster)), by = "rowid")
get_mode <- function(x) names(sort(table(x), decreasing = TRUE))[1]
perfil <- seg_df_full |>
group_by(segmento) |>
summarize(
n = n(),
precio_mediana = median(preciom_w, na.rm = TRUE),
area_mediana = median(areaconst_w, na.rm = TRUE),
estrato_mediana = median(estrato, na.rm = TRUE),
banios_mediana = median(banios_w, na.rm = TRUE),
habitaciones_mediana = median(habitaciones_w, na.rm = TRUE),
parqueaderos_mediana = median(parqueaderos_w, na.rm = TRUE),
zona_moda = get_mode(zona),
tipo_moda = get_mode(tipo),
.groups = 'drop'
) |>
arrange(segmento)
perfil
fviz_cluster(list(data = clust_mat, cluster = km$cluster)) +
ggtitle("Clusters sobre plano principal (k-means)") + theme_minimal()
Comentario (Clusters).
La gráfica muestra tres segmentos bien diferenciados
por tamaño y precio. Cada grupo
representa un tipo de oferta con necesidades distintas de precio,
abastecimiento y marketing.
tab_tz <- vivienda_limp |>
count(tipo, zona) |>
tidyr::pivot_wider(names_from = zona, values_from = n, values_fill = 0) |>
tibble::column_to_rownames("tipo")
top_barrios <- vivienda_limp |>
count(barrio, sort = TRUE) |>
slice_head(n = 30) |>
pull(barrio)
tab_tb <- vivienda_limp |>
filter(barrio %in% top_barrios) |>
count(tipo, barrio) |>
tidyr::pivot_wider(names_from = barrio, values_from = n, values_fill = 0) |>
tibble::column_to_rownames("tipo")
# --- CA: Tipo vs Zona (robusto 0D/1D/2D) ---
mat_tz <- vivienda_limp %>%
count(tipo, zona) %>%
tidyr::pivot_wider(names_from = zona, values_from = n, values_fill = 0) %>%
tibble::column_to_rownames("tipo") %>%
as.matrix()
# limpiar filas/columnas vacías
mat_tz <- mat_tz[rowSums(mat_tz) > 0, , drop = FALSE]
mat_tz <- mat_tz[, colSums(mat_tz) > 0, drop = FALSE]
ndim_tz <- min(nrow(mat_tz) - 1, ncol(mat_tz) - 1)
if (ndim_tz >= 1) {
ca_tz <- FactoMineR::CA(mat_tz, graph = FALSE)
if (ndim_tz >= 2) {
# Biplot normal con dos dimensiones
print(factoextra::fviz_ca_biplot(ca_tz, axes = c(1, 2), repel = TRUE,
title = "CA: Tipo vs Zona"))
} else {
# 1 dimensión: coordenas pueden venir como vector -> conviértelas
row_coords <- ca_tz$row$coord
col_coords <- ca_tz$col$coord
if (is.null(dim(row_coords))) {
if (is.null(names(row_coords))) names(row_coords) <- rownames(mat_tz)
rows <- tibble::tibble(tipo = names(row_coords),
Dim1 = as.numeric(row_coords))
} else {
tmp <- as.data.frame(row_coords)[, 1, drop = TRUE]
rows <- tibble::tibble(tipo = rownames(row_coords),
Dim1 = as.numeric(tmp))
}
if (is.null(dim(col_coords))) {
if (is.null(names(col_coords))) names(col_coords) <- colnames(mat_tz)
cols <- tibble::tibble(zona = names(col_coords),
Dim1 = as.numeric(col_coords))
} else {
tmp <- as.data.frame(col_coords)[, 1, drop = TRUE]
cols <- tibble::tibble(zona = rownames(col_coords),
Dim1 = as.numeric(tmp))
}
print(ggplot(rows, aes(x = reorder(tipo, Dim1), y = Dim1)) +
geom_col() + coord_flip() +
labs(title = "CA 1D: Tipos (Dim1)", x = NULL, y = "Coordenada Dim1") +
theme_minimal())
print(ggplot(cols, aes(x = reorder(zona, Dim1), y = Dim1)) +
geom_col() + coord_flip() +
labs(title = "CA 1D: Zonas (Dim1)", x = NULL, y = "Coordenada Dim1") +
theme_minimal())
}
} else {
cat("No hay suficiente variación para CA en Tipo vs Zona (1xN o Nx1).\n")
}
Comentario (CA – tipo vs zona).
La gráfica muestra afinidades entre el tipo de
vivienda y las zonas. Las categorías que
aparecen cercanas tienden a presentarse juntas en la oferta, lo que
orienta qué tipo encaja mejor en cada zona.
# --- CA: Tipo vs Barrios (Top 30) — robusto 0D/1D/2D ---
top_barrios <- vivienda_limp %>%
count(barrio, sort = TRUE) %>% slice_head(n = 30) %>% pull(barrio)
mat_tb <- vivienda_limp %>%
filter(barrio %in% top_barrios) %>%
count(tipo, barrio) %>%
tidyr::pivot_wider(names_from = barrio, values_from = n, values_fill = 0) %>%
tibble::column_to_rownames("tipo") %>%
as.matrix()
mat_tb <- mat_tb[rowSums(mat_tb) > 0, , drop = FALSE]
mat_tb <- mat_tb[, colSums(mat_tb) > 0, drop = FALSE]
ndim_tb <- min(nrow(mat_tb) - 1, ncol(mat_tb) - 1)
if (ndim_tb >= 1) {
ca_tb <- FactoMineR::CA(mat_tb, graph = FALSE)
if (ndim_tb >= 2) {
print(factoextra::fviz_ca_biplot(ca_tb, axes = c(1, 2), repel = TRUE,
title = "CA: Tipo vs Barrios (Top 30)"))
} else {
row_coords <- ca_tb$row$coord
col_coords <- ca_tb$col$coord
if (is.null(dim(row_coords))) {
if (is.null(names(row_coords))) names(row_coords) <- rownames(mat_tb)
rows <- tibble::tibble(tipo = names(row_coords),
Dim1 = as.numeric(row_coords))
} else {
tmp <- as.data.frame(row_coords)[, 1, drop = TRUE]
rows <- tibble::tibble(tipo = rownames(row_coords),
Dim1 = as.numeric(tmp))
}
if (is.null(dim(col_coords))) {
if (is.null(names(col_coords))) names(col_coords) <- colnames(mat_tb)
cols <- tibble::tibble(barrio = names(col_coords),
Dim1 = as.numeric(col_coords))
} else {
tmp <- as.data.frame(col_coords)[, 1, drop = TRUE]
cols <- tibble::tibble(barrio = rownames(col_coords),
Dim1 = as.numeric(tmp))
}
print(ggplot(rows, aes(x = reorder(tipo, Dim1), y = Dim1)) +
geom_col() + coord_flip() +
labs(title = "CA 1D: Tipos (Dim1)", x = NULL, y = "Coordenada Dim1") +
theme_minimal())
print(ggplot(cols, aes(x = reorder(barrio, Dim1), y = Dim1)) +
geom_col() + coord_flip() +
labs(title = "CA 1D: Barrios (Dim1)", x = NULL, y = "Coordenada Dim1") +
theme_minimal())
}
} else {
cat("No hay suficiente variación para CA en Tipo vs Barrios (1xN o Nx1).\n")
}
Comentario (CA – tipo vs barrios).
La gráfica muestra combinaciones frecuentes entre
tipos de vivienda y barrios. Donde la
barra (o el punto) es más alta, hay mayor presencia conjunta de ese tipo
en ese barrio.
cat_df <- vivienda_limp |>
transmute(
zona, tipo,
estrato_cat = factor(dplyr::case_when(
estrato <= 2 ~ "Bajo", estrato <= 4 ~ "Medio", TRUE ~ "Alto"),
levels = c("Bajo","Medio","Alto")),
categoria_area = factor(dplyr::case_when(
areaconst_w < quantile(areaconst_w, 0.33, na.rm = TRUE) ~ "Pequeña",
areaconst_w < quantile(areaconst_w, 0.66, na.rm = TRUE) ~ "Mediana",
TRUE ~ "Grande"), levels = c("Pequeña","Mediana","Grande")),
categoria_precio = factor(dplyr::case_when(
preciom_w < quantile(preciom_w, 0.33, na.rm = TRUE) ~ "Económica",
preciom_w < quantile(preciom_w, 0.66, na.rm = TRUE) ~ "Media",
TRUE ~ "Alta"), levels = c("Económica","Media","Alta")),
tiene_parqueadero = factor(ifelse(parqueaderos_w > 0, "Sí", "No"), levels = c("No","Sí"))
) |>
drop_na()
mca_res <- FactoMineR::MCA(cat_df, graph = FALSE)
factoextra::fviz_mca_biplot(mca_res, repel = TRUE, ggtheme = theme_minimal(),
title = "MCA — mapa conjunto de categorías")
Comentario (MCA).
La gráfica muestra qué categorías suelen aparecer
juntas (zona, tipo, tamaño, nivel de precio, parqueadero). Las
categorías cercanas comparten patrón de oferta.
factoextra::fviz_contrib(mca_res, choice = "var", axes = 1:2, top = 15) +
ggtitle("Contribución de variables al MCA") + theme_minimal()
Comentario (Contribución al MCA).
La gráfica muestra qué atributos pesan más para
diferenciar la oferta en el mapa de categorías. Es útil para priorizar
qué variables contar siempre en la captura del dato.
# 1) Precio promedio por zona y tipo
p1 <- seg_df_full |>
group_by(zona, tipo) |>
summarise(precio_promedio = mean(preciom_w, na.rm = TRUE), .groups = 'drop') |>
ggplot(aes(x = zona, y = precio_promedio, fill = tipo)) +
geom_col(position = "dodge") +
scale_fill_viridis_d() +
labs(title = "Precio promedio por zona y tipo", x = "Zona", y = "Precio (millones)", fill = "Tipo") +
theme_minimal() + theme(axis.text.x = element_text(angle = 45, hjust = 1))
# 2) Composición de estratos por zona
p2 <- seg_df_full |>
ggplot(aes(x = zona, fill = factor(estrato))) +
geom_bar(position = "fill") +
scale_fill_viridis_d() +
labs(title = "Composición de estratos por zona", x = "Zona", y = "Proporción", fill = "Estrato") +
theme_minimal() + theme(axis.text.x = element_text(angle = 45, hjust = 1))
# 3) Área vs Precio por segmento
p3 <- seg_df_full |>
ggplot(aes(x = areaconst_w, y = preciom_w, color = segmento)) +
geom_point(alpha = 0.6) +
labs(title = "Área vs Precio por segmento", x = "Área (m²)", y = "Precio (millones)", color = "Segmento") +
theme_minimal()
gridExtra::grid.arrange(p1, p2, p3, nrow = 3)
Comentario (Dashboard).
Arriba: precio promedio por zona y tipo; se observan
zonas y tipologías con valores superiores que conviene priorizar.
En medio: composición de estratos por zona; ayuda a
alinear oferta con el perfil socioeconómico local.
Abajo: área vs precio por segmento; confirma que los
segmentos se diferencian claramente por tamaño y
valor.
Nota: Los detalles técnicos (métodos y gráficos) están documentados en el informe y el anexo de trazabilidad; no se requiere formación estadística para implementar estas acciones.
# ANEXO: Trazabilidad de limpieza — tabla comparativa antes vs. después por regla
base_raw <- vivienda |>
mutate(piso_num = suppressWarnings(readr::parse_number(piso)),
zona = as.factor(zona), tipo = as.factor(tipo), barrio = as.factor(barrio))
n0 <- nrow(base_raw); curr <- base_raw
apply_step <- function(desc, f) {
n_before <- nrow(curr); curr <<- f(curr); n_after <- nrow(curr)
tibble::tibble(regla = desc, n_antes = n_before, n_despues = n_after,
removidos = n_before - n_after,
pct_sobre_inicial = round(100*(n_before - n_after)/n0, 2))
}
audit <- dplyr::bind_rows(
apply_step("1) Quitar NA en precio (preciom)", function(d) d |> dplyr::filter(!is.na(preciom))),
apply_step("2) Precio > 0", function(d) d |> dplyr::filter(preciom > 0)),
apply_step("3) Quitar NA en área (areaconst)", function(d) d |> dplyr::filter(!is.na(areaconst))),
apply_step("4) Área > 10 m²", function(d) d |> dplyr::filter(areaconst > 10)),
apply_step("5) Estrato en 1–6", function(d) d |> dplyr::filter(estrato %in% 1:6)),
apply_step("6) Baños >= 0", function(d) d |> dplyr::filter(banios >= 0)),
apply_step("7) Habitaciones >= 1", function(d) d |> dplyr::filter(habitaciones >= 1)),
apply_step("8) Parqueaderos >= 0", function(d) d |> dplyr::filter(parqueaderos >= 0))
) |> dplyr::mutate(n_acumulado = n_despues)
knitr::kable(audit, caption = "Trazabilidad de limpieza: efecto de cada regla sobre N")
regla | n_antes | n_despues | removidos | pct_sobre_inicial | n_acumulado |
---|---|---|---|---|---|
1) Quitar NA en precio (preciom) | 8322 | 8320 | 2 | 0.02 | 8320 |
2) Precio > 0 | 8320 | 8320 | 0 | 0.00 | 8320 |
3) Quitar NA en área (areaconst) | 8320 | 8319 | 1 | 0.01 | 8319 |
4) Área > 10 m² | 8319 | 8319 | 0 | 0.00 | 8319 |
5) Estrato en 1–6 | 8319 | 8319 | 0 | 0.00 | 8319 |
6) Baños >= 0 | 8319 | 8319 | 0 | 0.00 | 8319 |
7) Habitaciones >= 1 | 8319 | 8253 | 66 | 0.79 | 8253 |
8) Parqueaderos >= 0 | 8253 | 6694 | 1559 | 18.73 | 6694 |
resumen_n <- tibble::tibble(
etapa = c("Original", "Tras filtros"),
N = c(n0, nrow(curr)),
Removidos = c(NA, n0 - nrow(curr)),
`% de la base removida` = c(NA, round(100*(n0 - nrow(curr))/n0, 2))
)
knitr::kable(resumen_n, caption = "Resumen global de depuración")
etapa | N | Removidos | % de la base removida |
---|---|---|---|
Original | 8322 | NA | NA |
Tras filtros | 6694 | 1628 | 19.56 |
stats_before <- base_raw |> summarise(
precio_min = min(preciom, na.rm = TRUE),
precio_p50 = median(preciom, na.rm = TRUE),
precio_max = suppressWarnings(max(preciom, na.rm = TRUE)),
area_min = min(areaconst, na.rm = TRUE),
area_p50 = median(areaconst, na.rm = TRUE),
area_max = suppressWarnings(max(areaconst, na.rm = TRUE))
)
stats_after_filters <- curr |> summarise(
precio_min = min(preciom, na.rm = TRUE),
precio_p50 = median(preciom, na.rm = TRUE),
precio_max = suppressWarnings(max(preciom, na.rm = TRUE)),
area_min = min(areaconst, na.rm = TRUE),
area_p50 = median(areaconst, na.rm = TRUE),
area_max = suppressWarnings(max(areaconst, na.rm = TRUE))
)
stats_after_winsor <- vivienda_limp |> summarise(
precio_min = min(preciom_w, na.rm = TRUE),
precio_p50 = median(preciom_w, na.rm = TRUE),
precio_max = max(preciom_w, na.rm = TRUE),
area_min = min(areaconst_w, na.rm = TRUE),
area_p50 = median(areaconst_w, na.rm = TRUE),
area_max = max(areaconst_w, na.rm = TRUE)
)
comparativa <- dplyr::bind_rows(
stats_before |> mutate(etapa = "Antes (original)"),
stats_after_filters |> mutate(etapa = "Después (filtros)"),
stats_after_winsor |> mutate(etapa = "Después (winsorización)")
) |> dplyr::select(etapa, dplyr::everything())
knitr::kable(comparativa, caption = "Comparativa antes vs. después por etapa (precio y área)")
etapa | precio_min | precio_p50 | precio_max | area_min | area_p50 | area_max |
---|---|---|---|---|---|---|
Antes (original) | 58 | 330 | 1999.0 | 30 | 123 | 1745 |
Después (filtros) | 58 | 355 | 1999.0 | 30 | 130 | 1745 |
Después (winsorización) | 58 | 355 | 1580.5 | 30 | 130 | 670 |
sessionInfo()
## R version 4.4.3 (2025-02-28)
## Platform: aarch64-apple-darwin20
## Running under: macOS Sequoia 15.5
##
## Matrix products: default
## BLAS: /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRblas.0.dylib
## LAPACK: /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRlapack.dylib; LAPACK version 3.12.0
##
## locale:
## [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
##
## time zone: America/Bogota
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## other attached packages:
## [1] viridis_0.6.5 viridisLite_0.4.2 leaflet_2.2.2
## [4] cluster_2.1.8 NbClust_3.0.1 FactoMineR_2.12
## [7] factoextra_1.0.7 corrplot_0.95 scales_1.3.0
## [10] naniar_1.1.0 skimr_2.2.1 janitor_2.2.1
## [13] lubridate_1.9.4 forcats_1.0.0 stringr_1.5.1
## [16] dplyr_1.1.4 purrr_1.0.4 readr_2.1.5
## [19] tidyr_1.3.1 tibble_3.2.1 tidyverse_2.0.0
## [22] paqueteMODELOS_0.1.0 summarytools_1.1.4 knitr_1.50
## [25] gridExtra_2.3 GGally_2.3.0 ggplot2_3.5.2
## [28] broom_1.0.8 boot_1.3-31
##
## loaded via a namespace (and not attached):
## [1] tidyselect_1.2.1 farver_2.1.2 S7_0.2.0
## [4] fastmap_1.2.0 digest_0.6.37 timechange_0.3.0
## [7] estimability_1.5.1 lifecycle_1.0.4 multcompView_0.1-10
## [10] magrittr_2.0.3 compiler_4.4.3 rlang_1.1.5
## [13] sass_0.4.9 tools_4.4.3 yaml_2.3.10
## [16] ggsignif_0.6.4 labeling_0.4.3 htmlwidgets_1.6.4
## [19] scatterplot3d_0.3-44 plyr_1.8.9 repr_1.1.7
## [22] RColorBrewer_1.1-3 abind_1.4-8 withr_3.0.2
## [25] grid_4.4.3 ggpubr_0.6.1 xtable_1.8-4
## [28] colorspace_2.1-1 emmeans_1.11.2 MASS_7.3-64
## [31] flashClust_1.01-2 cli_3.6.4 mvtnorm_1.3-3
## [34] rmarkdown_2.29 generics_0.1.3 rstudioapi_0.17.1
## [37] reshape2_1.4.4 tzdb_0.5.0 cachem_1.1.0
## [40] pander_0.6.6 matrixStats_1.5.0 base64enc_0.1-3
## [43] vctrs_0.6.5 carData_3.0-5 jsonlite_1.9.1
## [46] car_3.1-3 hms_1.1.3 rapportools_1.2
## [49] rstatix_0.7.2 ggrepel_0.9.6 visdat_0.6.0
## [52] Formula_1.2-5 crosstalk_1.2.1 magick_2.8.7
## [55] jquerylib_0.1.4 glue_1.8.0 ggstats_0.10.0
## [58] codetools_0.2-20 DT_0.33 stringi_1.8.4
## [61] gtable_0.3.6 munsell_0.5.1 pillar_1.10.1
## [64] htmltools_0.5.8.1 R6_2.6.1 tcltk_4.4.3
## [67] evaluate_1.0.3 lattice_0.22-6 backports_1.5.0
## [70] leaps_3.2 snakecase_0.11.1 pryr_0.1.6
## [73] bslib_0.9.0 Rcpp_1.0.14 checkmate_2.3.2
## [76] xfun_0.51 pkgconfig_2.0.3