Informe Ejecutivo – Caso C&A (Casas y Apartamentos)
Autor: Maicol Stiven Contreras Macias
Profesora: Jennyfer Portilla Yela
Modelos estadísticos para la toma de decisiones
Universidad Javeriana de Cali
1 de Septiembre de 2025
Este informe presenta el análisis estadístico de un conjunto de datos inmobiliarios de la ciudad de Cali llamado Vivienda, con un total de 8.322 registros entre casas y apartamentos. El objetivo fue aplicar técnicas de exploración de datos, segmentación y modelado predictivo con el uso de Regresión Lineal Múltiple (RLM) para dar respuesta a dos solicitudes específicas de vivienda: una casa en la Zona Norte y un apartamento en la Zona Sur.
Se implementaron los modelos y segmentación mediante K-Means, con el fin de estimar precios de mercado y priorizar ofertas reales. Los resultados evidencian que variables como el área construida, el estrato socioeconómico, el número de baños y los parqueaderos son los principales determinantes del precio de una vivienda. Asimismo, se identificaron limitaciones asociadas a la calidad de los datos (outliers, valores faltantes y heterocedasticidad en modelos). A partir de los análisis, se proponen alternativas concretas de vivienda dentro de los presupuestos definidos, destacando tanto la viabilidad como las restricciones encontradas en el mercado.
Las viviendas se han caracterizado por la revision de múltiples factores que determinan el valor o precio final de vente. Entre ellos se destacan el área construida, la ubicación geográfica, el estrato socioeconómico y las características internas de las viviendas.
En este contexto, el uso de modelos estadísticos constituye una herramienta fundamental para apoyar procesos de decisión en la compra en bienes raíces.
El presente informe desarrolla un ejercicio aplicado de análisis de datos en el marco del curso Modelos Estadísticos. Para ello se utilizó una base de datos de viviendas en Cali, sobre la cual se aplicaron técnicas de preparación, exploración, segmentación y modelado predictivo. Los resultados se orientaron a resolver dos casos prácticos que simulan solicitudes reales de vivienda, con el fin de evidenciar el valor agregado del análisis cuantitativo orientado al precio y a la seleccion oportuna.
Antes de iniciar con los análisis, es fundamental conocer el tamaño y estructura de la base de datos utilizada. En la siguiente tabla se resumen las dimensiones del dataset vivienda, donde se presenta el número total de registros (filas) y el número de variables disponibles (columnas).
CONTEO_TABLAS <- CONTEO_TABLAS + 1
resumen_dim <- data.frame(
`Número de filas` = nrow(vivienda),
`Número de columnas` = ncol(vivienda)
)
# Mostrar tabla
knitr::kable(
resumen_dim,
caption = paste0("Tabla ", CONTEO_TABLAS, ". Dimensiones del dataset 'vivienda'")
)
| Número.de.filas | Número.de.columnas |
|---|---|
| 8322 | 13 |
Es importante detallar las caracteristicas de cada variable disponible. En la siguiente tabla se presenta un resumen de las variables que conforman la base vivienda, indicando su tipo de dato, el número de valores únicos registrados, la cantidad de nulos presentes y algunos ejemplos representativos. Este análisis permite identificar desde el inicio posibles variables categóricas, numéricas y aquellas que requieren un tratamiento especial en etapas posteriores de depuración o modelamiento.
CONTEO_TABLAS <- CONTEO_TABLAS + 1
data("vivienda")
tabla_resumen <- data.frame(
Variable = names(vivienda),
Tipo = sapply(vivienda, class),
Unicos = sapply(vivienda, function(x) length(unique(x))),
Nulos = sapply(vivienda, function(x) sum(is.na(x))),
Ejemplo = sapply(vivienda, function(x) paste(head(unique(x), 13), collapse = ", "))
)
kable(tabla_resumen, caption = paste0("Tabla ", CONTEO_TABLAS, ". Resumen de variables del dataset 'vivienda'"))
| Variable | Tipo | Unicos | Nulos | Ejemplo | |
|---|---|---|---|---|---|
| id | id | numeric | 8320 | 3 | 1147, 1169, 1350, 5992, 1212, 1724, 2326, 4386, 1209, 1592, 4057, 4460, 6081 |
| zona | zona | character | 6 | 3 | Zona Oriente, Zona Sur, Zona Norte, Zona Oeste, Zona Centro, NA |
| piso | piso | character | 13 | 2638 | NA, 02, 01, 03, 04, 05, 06, 07, 08, 09, 10, 11, 12 |
| estrato | estrato | numeric | 5 | 3 | 3, 4, 5, 6, NA |
| preciom | preciom | numeric | 540 | 2 | 250, 320, 350, 400, 260, 240, 220, 310, 780, 750, 625, 520, 600 |
| areaconst | areaconst | numeric | 653 | 3 | 70, 120, 220, 280, 90, 87, 52, 137, 150, 380, 445, 355, 237 |
| parqueaderos | parqueaderos | numeric | 11 | 1605 | 1, 2, 3, NA, 4, 7, 5, 8, 6, 9, 10 |
| banios | banios | numeric | 12 | 3 | 3, 2, 5, 4, 7, 6, 1, 0, 8, 10, 9, NA |
| habitaciones | habitaciones | numeric | 12 | 3 | 6, 3, 4, 5, 2, 0, 1, 8, 7, 10, 9, NA |
| tipo | tipo | character | 3 | 3 | Casa, Apartamento, NA |
| barrio | barrio | character | 437 | 3 | 20 de julio, 3 de julio, acopi, agua blanca, aguablanca, aguacatal, alameda, alameda del río, alameda del rio, alamos, alborada, alcazares, alf√©rez real |
| longitud | longitud | numeric | 2929 | 3 | -76.51168, -76.51237, -76.51537, -76.54, -76.5135, -76.517, -76.51974, -76.53105, -76.51341, -76.51674, -76.5295, -76.53179, -76.54044 |
| latitud | latitud | numeric | 3680 | 3 | 3.43382, 3.43369, 3.43566, 3.435, 3.45891, 3.36971, 3.42627, 3.38296, 3.47968, 3.48721, 3.38527, 3.4059, 3.36862 |
Para caracterizar la distribución de las variables numéricas de interés, a continuación se presentan estadísticas descriptivas (mínimo, cuartiles, mediana, media y máximo), así como el conteo de faltantes (NA). Esta tabla nos permite detectar valores atípicos potenciales, rangos inusuales y la magnitud de datos faltantes, insumos necesarios para definir estrategias de limpieza y preparación del modelado.
CONTEO_TABLAS <- CONTEO_TABLAS + 1
# Seleccionar variables
vars <- c("piso","preciom","areaconst","parqueaderos","banios","habitaciones")
# Obtener summary
resumen <- summary(vivienda[, vars])
# Pasarlo a tabla
tabla_resumen <- as.data.frame.matrix(resumen)
# Mostrar con kable
kable(tabla_resumen, caption =
paste0("Tabla ", CONTEO_TABLAS, ". Resumen de variables numericas seleccionadas"), row.names = FALSE)
| piso | preciom | areaconst | parqueaderos | banios | habitaciones |
|---|---|---|---|---|---|
| Length:8322 | Min. : 58.0 | Min. : 30.0 | Min. : 1.000 | Min. : 0.000 | Min. : 0.000 |
| Class :character | 1st Qu.: 220.0 | 1st Qu.: 80.0 | 1st Qu.: 1.000 | 1st Qu.: 2.000 | 1st Qu.: 3.000 |
| Mode :character | Median : 330.0 | Median : 123.0 | Median : 2.000 | Median : 3.000 | Median : 3.000 |
| NA | Mean : 433.9 | Mean : 174.9 | Mean : 1.835 | Mean : 3.111 | Mean : 3.605 |
| NA | 3rd Qu.: 540.0 | 3rd Qu.: 229.0 | 3rd Qu.: 2.000 | 3rd Qu.: 4.000 | 3rd Qu.: 4.000 |
| NA | Max. :1999.0 | Max. :1745.0 | Max. :10.000 | Max. :10.000 | Max. :10.000 |
| NA | NA’s :2 | NA’s :3 | NA’s :1605 | NA’s :3 | NA’s :3 |
Observaciones y tratamientos posibles encontrados en la Tabla 3:
Con el fin de conocer la distribución geográfica de las viviendas en la base de datos, se construyó una tabla de frecuencias para la variable zona. Esta información permite identificar en qué sectores de la ciudad se concentran la mayoría de las ofertas y, al mismo tiempo, verificar la existencia de valores nulos o inconsistencias en la variable.
Observaciones: La Zona Sur concentra casi el 57% de los registros, lo que refleja una fuerte concentración de datos en ese sector. La Zona Norte (≈23%) y la Zona Oeste (≈14%) tienen también presencia significativa, Por el momento y para el ejercicio final, tanto la zona sur y la norte son propicias para el analisis del proceso
CONTEO_TABLAS <- CONTEO_TABLAS + 1
# Crear tabla de frecuencias
freq_tab <- summarytools::freq(vivienda$zona, report.nas = TRUE)
# Convertir a data.frame
freq_df <- as.data.frame(freq_tab)
# Mostrar con kable
kable(freq_df, caption =
paste0("Tabla ", CONTEO_TABLAS, ". Tabla de frecuencia de la variable 'zona'"))
| Freq | % Valid | % Valid Cum. | % Total | % Total Cum. | |
|---|---|---|---|---|---|
| Zona Centro | 124 | 1.490564 | 1.490564 | 1.490026 | 1.490026 |
| Zona Norte | 1920 | 23.079697 | 24.570261 | 23.071377 | 24.561404 |
| Zona Oeste | 1198 | 14.400769 | 38.971030 | 14.395578 | 38.956981 |
| Zona Oriente | 351 | 4.219257 | 43.190287 | 4.217736 | 43.174718 |
| Zona Sur | 4726 | 56.809713 | 100.000000 | 56.789233 | 99.963951 |
| 3 | NA | NA | 0.036049 | 100.000000 | |
| Total | 8322 | 100.000000 | 100.000000 | 100.000000 | 100.000000 |
La variable estrato refleja el nivel socioeconómico asignado a cada vivienda. Esta clasificación es importante en el análisis inmobiliario, pues el estrato incide directamente en el valor de los servicios públicos y en la percepción del precio de la propiedad. En la siguiente tabla se presenta la distribución de frecuencias de los registros, lo que permite identificar los estratos predominantes en la base de datos y posibles valores faltantes.
Observaciones: El estrato 5 es el más frecuente (≈33% de los registros), seguido por los estratos 4 (≈26%) y 6 (≈24%).
CONTEO_TABLAS <- CONTEO_TABLAS + 1
# Crear tabla de frecuencias
freq_tab <- summarytools::freq(vivienda$estrato, report.nas = TRUE)
# Convertir a data.frame
freq_df <- as.data.frame(freq_tab)
# Mostrar con kable
kable(freq_df, caption =
paste0("Tabla ", CONTEO_TABLAS, ". Tabla de frecuencia de la variable 'estrato'"))
| Freq | % Valid | % Valid Cum. | % Total | % Total Cum. | |
|---|---|---|---|---|---|
| 3 | 1453 | 17.46604 | 17.46604 | 17.459745 | 17.45975 |
| 4 | 2129 | 25.59202 | 43.05806 | 25.582793 | 43.04254 |
| 5 | 2750 | 33.05686 | 76.11492 | 33.044941 | 76.08748 |
| 6 | 1987 | 23.88508 | 100.00000 | 23.876472 | 99.96395 |
| 3 | NA | NA | 0.036049 | 100.00000 | |
| Total | 8322 | 100.00000 | 100.00000 | 100.000000 | 100.00000 |
La variable tipo nos ayuda a ser parte de la clasificacion de un apartamento o casa. Esta categorización resulta fundamental, ya que el tipo de vivienda puede influir de manera directa en el rango de precios, el tamaño promedio, la ubicación y la demanda en el mercado inmobiliario. A continuación, se presenta la distribución de frecuencias de esta variable en el dataset.
Observaciones: La proporción muestra que el mercado en la base de datos está orientado principalmente hacia los apartamentos, lo cual puede influir en el sesgo de los modelos predictivos si no se controla adecuadamente.
CONTEO_TABLAS <- CONTEO_TABLAS + 1
# Crear tabla de frecuencias
freq_tab <- summarytools::freq(vivienda$tipo, report.nas = TRUE)
# Convertir a data.frame
freq_df <- as.data.frame(freq_tab)
# Mostrar con kable
kable(freq_df, caption =
paste0("Tabla ", CONTEO_TABLAS, ". Tabla de frecuencia de la variable 'tipo'"))
| Freq | % Valid | % Valid Cum. | % Total | % Total Cum. | |
|---|---|---|---|---|---|
| Apartamento | 5100 | 61.30545 | 61.30545 | 61.283345 | 61.28335 |
| Casa | 3219 | 38.69455 | 100.00000 | 38.680606 | 99.96395 |
| 3 | NA | NA | 0.036049 | 100.00000 | |
| Total | 8322 | 100.00000 | 100.00000 | 100.000000 | 100.00000 |
Con el objetivo de verificar la coherencia espacial de la variable zona y detectar posibles registros mal ubicados, se construyó el siguiente mapa interactivo. El color de relleno identifica la zona reportada en la base, mientras que el borde rojo marca posibles outliers de latitud dentro de propiedades etiquetadas como Zona Norte. El control inferior permite filtrar por tipo de vivienda
Algo muy importante que aclarar es: la informacion latitud y longitud unicamente se usa para reflejar los puntos en el mapa, no se usa en el proceso de analisis de datos.
# --- Datos y banderas ---
# Outliers de latitud dentro de "Zona Norte" (case-insensitive)
vivienda2 <- vivienda %>%
mutate(
zona_chr = as.character(zona),
es_norte = str_detect(str_to_lower(zona_chr), "zona norte")
)
lat_mu <- mean(vivienda2$latitud[vivienda2$es_norte], na.rm = TRUE)
lat_sd <- sd( vivienda2$latitud[vivienda2$es_norte], na.rm = TRUE)
vivienda2 <- vivienda2 %>%
mutate(
flag_outlier_lat = ifelse(es_norte & !is.na(latitud) & !is.na(lat_mu) & !is.na(lat_sd),
abs(latitud - lat_mu) > 2 * lat_sd, FALSE)
)
# Palette por zona (colorea el relleno del punto)
pal_zona <- colorFactor(
palette = brewer.pal(max(3, min(8, length(unique(vivienda2$zona_chr)))), "Set1"),
domain = vivienda2$zona_chr,
na.color = "#999999"
)
# SharedData para activar el filtro sin Shiny
sd_viv <- SharedData$new(vivienda2)
# Filtro (dropdown) por tipo de vivienda
filtro_tipo <- crosstalk::filter_select(
id = "f_tipo",
label = "Tipo de vivienda:",
sharedData = sd_viv,
group = ~tipo
)
# Mapa
mapa <- leaflet(sd_viv) %>%
addTiles() %>%
addCircleMarkers(
lng = ~longitud, lat = ~latitud,
radius = ~ifelse(flag_outlier_lat, 8, 6),
stroke = TRUE,
weight = ~ifelse(flag_outlier_lat, 2, 1),
color = ~ifelse(flag_outlier_lat, "#d62728", "#333333"), # Borde (rojo si outlier)
fill = TRUE,
fillOpacity = 0.8,
fillColor = ~pal_zona(zona_chr),
popup = ~paste0(
"<b>", tipo, " – ", zona_chr, "</b><br>",
"Barrio: ", barrio, "<br>",
"Estrato: ", estrato, "<br>",
"Área: ", comma(areaconst), " m²<br>",
"Parqueaderos: ", parqueaderos,
" – Baños: ", banios,
" – Hab: ", habitaciones, "<br>",
"Precio: $", comma(preciom), " M"
)
) %>%
addLegend(
position = "bottomleft",
pal = pal_zona, values = ~zona_chr,
title = "Zona (color de relleno)",
opacity = 1
) %>%
addLegend(
position = "bottomright",
colors = c("#333333", "#d62728"),
labels = c("Normal", "Posible outlier (lat)"),
title = "Borde del punto"
)
# Disposición: filtro arriba, mapa abajo
crosstalk::bscols(widths = c(12),
filtro_tipo,
mapa
)
Observaciones:
# Normalizamos tipos de datos
vivienda <- vivienda %>%
mutate(
tipo = as.factor(tipo),
zona = as.factor(zona),
barrio = as.factor(barrio),
estrato = as.integer(estrato),
areaconst = as.numeric(areaconst),
parqueaderos= as.integer(parqueaderos),
banios = as.integer(banios),
habitaciones= as.integer(habitaciones),
preciom = as.numeric(preciom)
)
Con el propósito de garantizar una adecuada validación del modelo, se realizara la división de la base de datos en dos subconjuntos: entrenamiento (Train) y prueba (Test). El conjunto de entrenamiento se utiliza para ajustar el modelo de regresión lineal multiple, mientras que el conjunto de prueba permite evaluar su capacidad predictiva en datos no vistos. La siguiente tabla resume la distribución de registros entre ambos subconjuntos.
Se excluyeron previamente los registros con valores faltantes en variables críticas (preciom, areaconst, estrato, habitaciones, parqueaderos, banios), lo que asegura consistencia en la modelación.
set.seed(123)
split_global <- rsample::initial_split(vivienda %>% drop_na(preciom, areaconst,
estrato, habitaciones, parqueaderos, banios), prop = 0.7)
train_global <- training(split_global)
test_global <- testing(split_global)
tabla_split <- tibble(
Conjunto = c("Train", "Test"),
Registros = c(nrow(train_global), nrow(test_global)),
Porcentaje = round(c(nrow(train_global), nrow(test_global)) /
(nrow(train_global) + nrow(test_global)) * 100, 1)
)
kable(tabla_split, caption = "Distribución de registros entre Train y Test")
| Conjunto | Registros | Porcentaje |
|---|---|---|
| Train | 4701 | 70 |
| Test | 2016 | 30 |
Para identificar el número adecuado de clusters en el proceso de segmentación, se aplicó el método del codo. Este método evalúa la variación explicada a medida que aumenta el número de clusters (k). El punto de inflexión del gráfico indica el valor de k más apropiado, en el cual agregar más clusters no mejora significativamente la homogeneidad interna.
set.seed(123)
# ---------------------
# 2. PREPARAR SOLO DATOS ESPACIALES
# ---------------------
# Escalamiento manual: menos peso a latitud
geo_data <- train_global %>%
filter(!is.na(latitud) & !is.na(longitud)) %>%
select(latitud, longitud)
# Reescalar manualmente con menor peso en latitud
geo_data_mod <- geo_data %>%
mutate(
latitud = scale(latitud) * 2, # Reducimos peso de latitud
longitud = scale(longitud) * 1.4 # Mantenemos peso de longitud
)
# K-Means con nuevo escalado
geo_scaled <- as.matrix(geo_data_mod)
# Método del codo si quieres verificar
p <- fviz_nbclust(geo_scaled, kmeans, method = "wss") +
labs(title = "Método del codo (WSS)")
ggplotly(p)
# Clustering
set.seed(123)
km_model <- kmeans(geo_scaled, centers = 4, nstart = 25)
# Asignar a train_global
train_global$cluster <- NA
train_global$cluster[which(!is.na(train_global$latitud) & !is.na(train_global$longitud))] <- km_model$cluster
train_global$cluster <- factor(train_global$cluster)
Observaciones: El codo más evidente se ubica alrededor de k=3 o k=4, lo que sugiere que estos son los valores más razonables para segmentar las viviendas. Estare usando la agrupacion de 4.
Se aplicó la técnica de K-Means para segmentar las viviendas según su ubicación geográfica. El mapa siguiente presenta la distribución espacial de los grupos formados, donde cada color representa un cluster distinto. Esta representación permite identificar patrones de concentración y diferenciación territorial de las ofertas inmobiliarias.
# ---------------------
# 7. MAPA LEAFLET
# ---------------------
vivienda2 <- train_global %>%
mutate(
zona_chr = as.character(zona),
cluster = factor(cluster)
)
pal_cluster <- colorFactor(
palette = brewer.pal(max(3, min(8, length(unique(vivienda2$cluster)))), "Dark2"),
domain = vivienda2$cluster,
na.color = "#999999"
)
leaflet(vivienda2) %>%
addTiles() %>%
addCircleMarkers(
lng = ~longitud,
lat = ~latitud,
radius = 6,
stroke = FALSE,
fillOpacity = 0.8,
fillColor = ~pal_cluster(cluster),
popup = ~paste0(
"<b>", tipo, " – Cluster ", cluster, "</b><br>",
"Barrio: ", barrio, "<br>",
"Zona: ", zona_chr, "<br>",
"Estrato: ", estrato, "<br>",
"Área: ", comma(areaconst), " m²<br>",
"Parqueaderos: ", parqueaderos,
" – Baños: ", banios,
" – Hab: ", habitaciones, "<br>",
"Precio: $", comma(preciom), " M"
)
) %>%
addLegend(
position = "bottomleft",
pal = pal_cluster,
values = ~cluster,
title = "Cluster K-Means (espacial)",
opacity = 1
)
Con el fin de evaluar la relación entre la ubicación geográfica y la segmentación obtenida por K-Means, se construyó una tabla de contingencia con el conteo de registros por zona y por cluster (sobre el conjunto de entrenamiento). Esta tabla permite verificar si los clusters presentan concentraciones claras en determinadas zonas y, por ende, si la segmentación captura patrones espaciales relevantes.
# Tabla: conteo por zona y cluster (train_global)
tabla_zona_cluster <- vivienda2 %>%
mutate(zona_chr = ifelse(is.na(zona_chr), "Sin zona", zona_chr)) %>%
count(zona_chr, cluster, name = "n") %>%
complete(zona_chr, cluster, fill = list(n = 0)) %>% # aseguramos columnas para todos los clusters
group_by(zona_chr) %>%
mutate(Total = sum(n)) %>%
ungroup() %>%
arrange(desc(Total)) %>%
pivot_wider(
names_from = cluster,
values_from = n,
names_prefix = "Cluster_"
) %>%
relocate(Total, .after = zona_chr)
# Fila de totales generales
totales <- tabla_zona_cluster %>%
summarise(
zona_chr = "TOTAL",
Total = sum(Total),
across(starts_with("Cluster_"), ~ sum(.x, na.rm = TRUE))
)
tabla_zona_cluster_out <- bind_rows(tabla_zona_cluster, totales)
# Mostrar en kable
kable(
tabla_zona_cluster_out,
caption = "Conteo de registros por zona y por cluster (Train)"
)
| zona_chr | Total | Cluster_1 | Cluster_2 | Cluster_3 | Cluster_4 |
|---|---|---|---|---|---|
| Zona Sur | 2891 | 99 | 1979 | 645 | 168 |
| Zona Norte | 900 | 204 | 22 | 51 | 623 |
| Zona Oeste | 750 | 15 | 13 | 658 | 64 |
| Zona Oriente | 114 | 66 | 19 | 6 | 23 |
| Zona Centro | 46 | 3 | 1 | 16 | 26 |
| TOTAL | 4701 | 387 | 2034 | 1376 | 904 |
Observaciones: Zona Sur concentra la mayor cantidad de registros del train (2.891; 61.5% del total), con fuerte presencia del Cluster_2 (1.979). Zona Norte (900) se asocia especialmente con Cluster_4 (623), lo que sugiere un patrón nítido al norte de la ciudad.
nombres_clusters <- c(
"4" = "Zona Norte",
"3" = "Zona Occidente",
"1" = "Zona Oriente",
"2" = "Zona Sur"
)
train_global <- train_global %>%
mutate(
zona = recode(as.character(cluster), !!!nombres_clusters)
)
vivienda2 <- train_global %>%
mutate(
zona_chr = as.character(zona),
cluster_nombre = factor(zona)
)
# Paleta por cluster renombrado
pal_cluster <- colorFactor(
palette = brewer.pal(max(3, min(8, length(unique(vivienda2$cluster_nombre)))), "Dark2"),
domain = vivienda2$cluster_nombre,
na.color = "#999999"
)
Con el fin de facilitar la interpretación ejecutiva, los clusters obtenidos por K-Means se renombraron según su nuevo valor de agrupacion: Zona Norte, Zona Occidente, Zona Oriente y Zona Sur. El mapa siguiente muestra la distribución geográfica de los grupos renombrados, permitiendo identificar de manera inmediata los corredores inmobiliarios más representativos.
leaflet(vivienda2) %>%
addTiles() %>%
addCircleMarkers(
lng = ~longitud,
lat = ~latitud,
radius = 6,
stroke = FALSE,
fillOpacity = 0.8,
fillColor = ~pal_cluster(cluster_nombre),
popup = ~paste0(
"<b>", tipo, " – ", cluster_nombre, "</b><br>",
"Barrio: ", barrio, "<br>",
"Zona: ", zona_chr, "<br>",
"Estrato: ", estrato, "<br>",
"Área: ", comma(areaconst), " m²<br>",
"Parqueaderos: ", parqueaderos,
" – Baños: ", banios,
" – Hab: ", habitaciones, "<br>",
"Precio: $", comma(preciom), " M"
)
) %>%
addLegend(
position = "bottomleft",
pal = pal_cluster,
values = ~cluster_nombre,
title = "Clusters K-Means (renombrados)",
opacity = 1
)
Para dar respuesta al Caso 1, se filtró la base de datos de acuerdo con los criterios solicitados, únicamente viviendas del tipo Casa ubicadas en la Zona Norte de la ciudad. Esta depuración permite enfocar el análisis. En la siguiente tabla se presentan los primeros tres registros obtenidos tras aplicar el filtro.
CONTEO_TABLAS <- CONTEO_TABLAS + 1
#caso1_data <- train_global %>%
caso1_data <- vivienda %>%
filter(
tipo == "Casa",
str_detect(str_to_lower(zona), "zona norte")
)
# Primeros 3 registros
caso1_data %>% select(zona, tipo, barrio, estrato, areaconst, parqueaderos, banios, habitaciones, preciom, longitud, latitud) %>% head(3) %>% knitr::kable(caption =
paste0("Tabla ", CONTEO_TABLAS, ". Presentando los datos de 3 registros de la informacion de Casas en la Zona norte"))
| zona | tipo | barrio | estrato | areaconst | parqueaderos | banios | habitaciones | preciom | longitud | latitud |
|---|---|---|---|---|---|---|---|---|---|---|
| Zona Norte | Casa | acopi | 5 | 150 | 2 | 4 | 6 | 320 | -76.51341 | 3.47968 |
| Zona Norte | Casa | acopi | 5 | 380 | 2 | 3 | 3 | 780 | -76.51674 | 3.48721 |
| Zona Norte | Casa | acopi | 6 | 445 | NA | 7 | 6 | 750 | -76.52950 | 3.38527 |
Con el filtro aplicado al Caso 1 (Casas en Zona Norte), a continuación se presenta un resumen estructural del subconjunto caso1_data: tipo de cada variable, número de valores únicos, conteo de nulos y ejemplos. Esta vista permite anticipar decisiones de preparación antes del modelado específico para este caso.
CONTEO_TABLAS <- CONTEO_TABLAS + 1
tabla_resumen <- data.frame(
Variable = names(caso1_data),
Tipo = sapply(caso1_data, class),
Unicos = sapply(caso1_data, function(x) length(unique(x))),
Nulos = sapply(caso1_data, function(x) sum(is.na(x))),
Ejemplo = sapply(caso1_data, function(x) paste(head(unique(x), 13), collapse = ", "))
)
kable(tabla_resumen, caption = paste0("Tabla ", CONTEO_TABLAS, ". Resumen de variables del dataset 'caso1_data'"))
| Variable | Tipo | Unicos | Nulos | Ejemplo | |
|---|---|---|---|---|---|
| id | id | numeric | 722 | 0 | 1209, 1592, 4057, 4460, 6081, 7824, 7987, 3495, 141, 243, 504, 565, 604 |
| zona | zona | factor | 1 | 0 | Zona Norte |
| piso | piso | character | 6 | 372 | 02, 03, NA, 01, 04, 07 |
| estrato | estrato | integer | 4 | 0 | 5, 6, 4, 3 |
| preciom | preciom | numeric | 167 | 0 | 320, 780, 750, 625, 600, 420, 490, 230, 190, 180, 500, 520, 380 |
| areaconst | areaconst | numeric | 251 | 0 | 150, 380, 445, 355, 237, 160, 200, 118, 435, 120, 210, 455, 300 |
| parqueaderos | parqueaderos | integer | 11 | 287 | 2, NA, 3, 1, 4, 6, 5, 7, 8, 10, 9 |
| banios | banios | integer | 11 | 0 | 4, 3, 7, 5, 6, 2, 0, 8, 1, 9, 10 |
| habitaciones | habitaciones | integer | 11 | 0 | 6, 3, 5, 4, 0, 8, 7, 2, 1, 9, 10 |
| tipo | tipo | factor | 1 | 0 | Casa |
| barrio | barrio | factor | 103 | 0 | acopi, alameda del río, alamos, atanasio girardot, barranquilla, barrio tranquilo y, base a√©rea, berlin, brisas de los, brisas del guabito, Cali, calibella, calima |
| longitud | longitud | numeric | 464 | 0 | -76.51341, -76.51674, -76.5295, -76.53179, -76.54044, -76.5521, -76.55363, -76.5268, -76.48641, -76.49032, -76.49768, -76.49899, -76.49966 |
| latitud | latitud | numeric | 462 | 0 | 3.47968, 3.48721, 3.38527, 3.4059, 3.36862, 3.42125, 3.4005, 3.37823, 3.44956, 3.43856, 3.4706, 3.44843, 3.46284 |
Con el fin de caracterizar las principales variables numéricas del subconjunto caso1_data, se calcularon medidas descriptivas básicas (mínimo, cuartiles, mediana, media y máximo), así como la cantidad de valores faltantes. Esta tabla permite tener una visión preliminar de la distribución de las características de las casas en la Zona Norte.
Observaciones:
CONTEO_TABLAS <- CONTEO_TABLAS + 1
# Seleccionar variables
vars <- c("piso","preciom","areaconst","parqueaderos","banios","habitaciones")
# Obtener summary
resumen <- summary(caso1_data[, vars])
# Pasarlo a tabla
tabla_resumen <- as.data.frame.matrix(resumen)
# Mostrar con kable
kable(tabla_resumen, caption =
paste0("Tabla ", CONTEO_TABLAS, ". Resumen de variables numericas seleccionadas"), row.names = FALSE)
| piso | preciom | areaconst | parqueaderos | banios | habitaciones |
|---|---|---|---|---|---|
| Length:722 | Min. : 89.0 | Min. : 30.0 | Min. : 1.000 | Min. : 0.000 | Min. : 0.000 |
| Class :character | 1st Qu.: 261.2 | 1st Qu.: 140.0 | 1st Qu.: 1.000 | 1st Qu.: 2.000 | 1st Qu.: 3.000 |
| Mode :character | Median : 390.0 | Median : 240.0 | Median : 2.000 | Median : 3.000 | Median : 4.000 |
| NA | Mean : 445.9 | Mean : 264.9 | Mean : 2.182 | Mean : 3.555 | Mean : 4.507 |
| NA | 3rd Qu.: 550.0 | 3rd Qu.: 336.8 | 3rd Qu.: 3.000 | 3rd Qu.: 4.000 | 3rd Qu.: 5.000 |
| NA | Max. :1940.0 | Max. :1440.0 | Max. :10.000 | Max. :10.000 | Max. :10.000 |
| NA | NA | NA | NA’s :287 | NA | NA |
En el subconjunto filtrado para el Caso 1, es relevante analizar la distribución de las viviendas según el estrato socioeconómico. El estrato es un determinante clave en la valoración de la vivienda y en la segmentación del mercado. La siguiente tabla presenta las frecuencias de estrato para las casas de la Zona Norte.
CONTEO_TABLAS <- CONTEO_TABLAS + 1
# Crear tabla de frecuencias
freq_tab <- summarytools::freq(caso1_data$estrato, report.nas = TRUE)
# Convertir a data.frame
freq_df <- as.data.frame(freq_tab)
# Mostrar con kable
kable(freq_df, caption =
paste0("Tabla ", CONTEO_TABLAS, ". Frecuencia de la variable 'estrato' para el caso 1"))
| Freq | % Valid | % Valid Cum. | % Total | % Total Cum. | |
|---|---|---|---|---|---|
| 3 | 235 | 32.548476 | 32.54848 | 32.548476 | 32.54848 |
| 4 | 161 | 22.299169 | 54.84765 | 22.299169 | 54.84765 |
| 5 | 271 | 37.534626 | 92.38227 | 37.534626 | 92.38227 |
| 6 | 55 | 7.617729 | 100.00000 | 7.617729 | 100.00000 |
| 0 | NA | NA | 0.000000 | 100.00000 | |
| Total | 722 | 100.000000 | 100.00000 | 100.000000 | 100.00000 |
Para complementar el análisis del Caso 1, se georreferenciaron las casas de la Zona Norte sobre un mapa interactivo. Los puntos azules representan la ubicación de cada registro, mientras que los bordes en rojo marcan los casos detectados como posibles outliers en latitud, es decir, viviendas clasificadas como “Zona Norte” pero que, espacialmente, parecen estar fuera del rango esperado para esa zona.
# --- Datos y banderas ---
# Outliers de latitud dentro de "Zona Norte" (case-insensitive)
caso1_data_2 <- caso1_data %>%
mutate(
zona_chr = as.character(zona),
es_norte = str_detect(str_to_lower(zona_chr), "zona norte")
)
lat_mu <- mean(caso1_data_2$latitud[caso1_data_2$es_norte], na.rm = TRUE)
lat_sd <- sd( caso1_data_2$latitud[caso1_data_2$es_norte], na.rm = TRUE)
caso1_data_2 <- caso1_data_2 %>%
mutate(
flag_outlier_lat = ifelse(es_norte & !is.na(latitud) & !is.na(lat_mu) & !is.na(lat_sd),
abs(latitud - lat_mu) > 2 * lat_sd, FALSE)
)
# Palette por zona (colorea el relleno del punto)
pal_zona <- colorFactor(
palette = brewer.pal(max(3, min(8, length(unique(caso1_data_2$zona_chr)))), "Set1"),
domain = caso1_data_2$zona_chr,
na.color = "#999999"
)
# SharedData para activar el filtro sin Shiny
sd_viv <- SharedData$new(caso1_data_2)
# Mapa
mapa <- leaflet(sd_viv) %>%
addTiles() %>%
addCircleMarkers(
lng = ~longitud, lat = ~latitud,
radius = ~ifelse(flag_outlier_lat, 8, 6),
stroke = TRUE,
weight = ~ifelse(flag_outlier_lat, 2, 1),
color = ~ifelse(flag_outlier_lat, "#d62728", "#333333"), # Borde (rojo si outlier)
fill = TRUE,
fillOpacity = 0.8,
fillColor = ~pal_zona(zona_chr),
popup = ~paste0(
"<b>", tipo, " – ", zona_chr, "</b><br>",
"Barrio: ", barrio, "<br>",
"Estrato: ", estrato, "<br>",
"Área: ", comma(areaconst), " m²<br>",
"Parqueaderos: ", parqueaderos,
" – Baños: ", banios,
" – Hab: ", habitaciones, "<br>",
"Precio: $", comma(preciom), " M"
)
) %>%
addLegend(
position = "bottomleft",
pal = pal_zona, values = ~zona_chr,
title = "Zona (color de relleno)",
opacity = 1
) %>%
addLegend(
position = "bottomright",
colors = c("#333333", "#d62728"),
labels = c("Normal", "Posible outlier (lat)"),
title = "Borde del punto"
)
# Disposición: filtro arriba, mapa abajo
crosstalk::bscols(widths = c(12),
mapa
)
Discussion: ¿Todos los puntos se ubican en la zona correspondiente o se presentan valores en otras zonas? ¿por que?
Segun el grafico anterior,si aparecen valores que no parecen pertenecen a la zona norte, y esto tambien fue corroborado en la grafica 1, donde se valido la organizacion de todas las zonas, y la zona norte tambien destaca algunos puntos que no parecen estar en relacion a esa zona.
Esto puede ser debido a errores de digitación en latitud/longitud, barrios que de manera visual se consideran Norte pero cuyas coordenadas están cerca de límites, o pueden haber inconsistencia en la variable zona.
Para explorar la relación entre las variables numéricas del subconjunto de casas en la Zona Norte, se construyó un mapa de calor de correlaciones. Este gráfico permite identificar la fuerza y dirección de las asociaciones lineales entre el precio de la vivienda (preciom) y las variables explicativas (areaconst, estrato, banios, habitaciones, parqueaderos).
num_cols <- c("preciom","areaconst","estrato","banios","habitaciones","parqueaderos")
cor_mat <- caso1_data %>% select(all_of(num_cols)) %>% cor(use = "pairwise.complete.obs")
# Heatmap interactivo
plotly::plot_ly(
x = colnames(cor_mat), y = rownames(cor_mat), z = cor_mat,
type = "heatmap", showscale = TRUE
) %>% layout(title = "Grafico 1. Correlación entre variables numéricas")
Observaciones:
Con el fin de explorar relaciones bivariadas y distribuciones marginales en el subconjunto de Casas en Zona Norte, se construyó una matriz de gráficos (pairs). En la diagonal se muestran las distribuciones univariadas y, en el triángulo superior, los coeficientes de correlación de Pearson; el triángulo inferior ilustra la dispersión entre pares de variables.
Como se sigue evidenciando, precio presenta correlación positiva con Área, Baños y Estrato
#caso1_data$preciom <- log(caso1_data$preciom)
#caso1_data$areaconst <- log(caso1_data$areaconst)
p <-
ggpairs(
caso1_data,
columns = c("preciom","areaconst", "estrato", "banios", "habitaciones", "zona"),
title = "Grafico 2. Relación del precio en función de las variables seleccionadas",
columnLabels = c("Precio","Área", "Estrato", "Baños", "Habitaciones", "Zona"),
diag = list(continuous = wrap("barDiag", bins = 20))
)
ggplotly(p) # hace el gráfico interactivo
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
Para profundizar en la relación entre el precio de la vivienda y sus características, se analizaron las distribuciones de preciom (Gráfico 3) y areaconst (Gráfico 4) frente a variables categóricas como estrato socioeconómico, número de habitaciones y número de baños. Estas comparaciones permiten observar patrones de diferenciación y validar el impacto de dichas variables sobre el valor y tamaño de las casas.
# Variables categóricas
vars_cat <- c("zona", "habitaciones", "estrato", "banios")
# Etiquetas bonitas para los ejes
var_labels <- c(
zona = "Zona",
habitaciones = "Número de habitaciones",
estrato = "Estrato socioeconómico",
banios = "Número de baños"
)
# Base
df <- caso1_data %>%
mutate(across(all_of(vars_cat), ~as.factor(.))) %>%
select(preciom, all_of(vars_cat)) %>%
filter(!is.na(preciom))
# Función que construye un boxplot
make_box <- function(var) {
# Ordenar categorías por mediana de precio
med_order <- df %>%
group_by(.data[[var]]) %>%
summarise(med = median(preciom, na.rm = TRUE), .groups = "drop") %>%
arrange(med) %>%
pull(1) %>% as.character()
data_var <- df %>%
transmute(
Categoria = fct_relevel(as.factor(.data[[var]]), med_order),
preciom = preciom
)
plot_ly(
data_var,
x = ~Categoria, y = ~preciom,
type = "box",
boxpoints = "outliers"
) %>%
layout(
title = var_labels[[var]], # título de cada subplot
xaxis = list(title = var_labels[[var]], tickangle = 45),
yaxis = list(title = "Precio (millones)")
)
}
plots <- map(vars_cat, make_box)
# Matriz de subplots
subplot(
plots,
nrows = ceiling(length(plots) / 2),
shareY = TRUE, titleX = TRUE, titleY = TRUE,
margin = 0.05
) %>%
layout(
showlegend = FALSE,
title = list(text = "Grafico 3. Distribución de 'preciom' por variables categóricas")
)
Observaciones:
# Variables categóricas
vars_cat <- c("zona", "habitaciones", "estrato", "banios")
# Etiquetas bonitas para los ejes
var_labels <- c(
zona = "Zona",
habitaciones = "Número de habitaciones",
estrato = "Estrato socioeconómico",
banios = "Número de baños"
)
# Base
df <- caso1_data %>%
mutate(across(all_of(vars_cat), ~as.factor(.))) %>%
select(areaconst, all_of(vars_cat)) %>%
filter(!is.na(areaconst))
# Función que construye un boxplot
make_box <- function(var) {
# Ordenar categorías por mediana de areaconst
med_order <- df %>%
group_by(.data[[var]]) %>%
summarise(med = median(areaconst, na.rm = TRUE), .groups = "drop") %>%
arrange(med) %>%
pull(1) %>% as.character()
data_var <- df %>%
transmute(
Categoria = fct_relevel(as.factor(.data[[var]]), med_order),
areaconst = areaconst
)
plot_ly(
data_var,
x = ~Categoria, y = ~areaconst,
type = "box",
boxpoints = "outliers"
) %>%
layout(
title = var_labels[[var]], # título del gráfico
xaxis = list(title = var_labels[[var]], tickangle = 45),
yaxis = list(title = "Área (m²)")
)
}
plots <- map(vars_cat, make_box)
# Matriz de subplots
subplot(
plots,
nrows = ceiling(length(plots) / 2),
shareY = TRUE, titleX = TRUE, titleY = TRUE,
margin = 0.05
) %>%
layout(
showlegend = FALSE,
title = list(text = "Grafico 4. Distribución de 'areaconst' por variables categóricas")
)
Interpretacion de los resultados: Se reportan valores como 0.57, 0.55, 0.53, etc. Todos son positivos y son realmente significativos.
El mayor coeficiente se da entre Precio y Área, Precio y Baños , confirmando que los valores numericos son los mas fuertes, se esperaba por lo que son dos valores numericos y los baños podrian indicar una correlaccion directa por la cantidad en un hogar.
Aunque Estrato también correlaciona con Precio, es bajo pero se espera tenerlo en cuenta como parte de la revision de los coeficientes.
La tendencia muestra que, a medida que aumentan las habitaciones y baños, también se incrementa el área construida y el precio de la vivienda depende principalmente del estrato socioeconómico, seguido del número de habitaciones y baños
A continuación se presentan los coeficientes estimados del modelo de Regresión Lineal Múltiple (RLM) con preciom como variable respuesta y areaconst, estrato, habitaciones, parqueaderos y banios como predictores. Se reportan los estimadores puntuales, errores estándar, estadísticos t y p-values para evaluar su significancia estadística.
CONTEO_TABLAS <- CONTEO_TABLAS + 1
library(broom)
library(knitr)
# Resultados de coeficientes como data.frame
tabla_coef <- tidy(m1)
kable(tabla_coef, digits = 4, caption =
paste0("Tabla ", CONTEO_TABLAS, ". Coeficientes del modelo de regresión"))
| term | estimate | std.error | statistic | p.value |
|---|---|---|---|---|
| (Intercept) | -374.7720 | 76.1136 | -4.9239 | 0.0000 |
| areaconst | 0.5395 | 0.0789 | 6.8373 | 0.0000 |
| estrato | 104.3117 | 15.8872 | 6.5658 | 0.0000 |
| habitaciones | 1.6792 | 9.3271 | 0.1800 | 0.8573 |
| parqueaderos | 51.8131 | 10.4689 | 4.9493 | 0.0000 |
| banios | 36.7701 | 12.3342 | 2.9812 | 0.0032 |
Además de los coeficientes individuales, es necesario evaluar la calidad global del modelo. La Tabla 12 presenta los principales indicadores de ajuste y significancia del modelo de regresión lineal múltiple estimado para el Caso 1 (Casas en Zona Norte).
CONTEO_TABLAS <- CONTEO_TABLAS + 1
glance(m1) %>%
kable(digits = 4, caption =
paste0("Tabla ", CONTEO_TABLAS, ". Métricas del modelo"))
| r.squared | adj.r.squared | sigma | statistic | p.value | df | logLik | AIC | BIC | deviance | df.residual | nobs |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0.5728 | 0.5636 | 203.9438 | 62.2172 | 0 | 5 | -1600.316 | 3214.632 | 3238.938 | 9649594 | 232 | 238 |
Interpretacion de los coeficientes: Todos los predictores resultan estadísticamente significativos, con direcciones coherentes con el mercado inmobiliario. Las variables más influyentes son área construida y estrato, seguidas de parqueaderos y baños, y finalmente habitaciones.
El modelo muestra resultados bastante coherentes con lo que se espera en el mercado inmobiliario. La variable que más influye en el precio es el área construida: a mayor tamaño de la vivienda, mayor es su valor. Le sigue la variable estrato, lo cual tiene mucho sentido, ya que las viviendas en zonas de mayor estrato tienden a ser más costosas.
Otras características como el número de parqueaderos y de baños también aportan de manera importante al precio: cada espacio adicional eleva el valor de la propiedad. En el caso de las habitaciones, el efecto es más pequeño, aunque sigue siendo significativo; más cuartos sí aumentan el precio, pero no tanto como el área o el estrato.
Interpretacion del ajuste del modelo:
R²: Es un ajuste bastante alto para datos de bienes raíces, donde siempre existe un componente de variabilidad difícil de capturar Error estándar residual: Indica la magnitud promedio de los errores de predicción. Cuanto más pequeño, mejor. F-statistic (p < 0.001): Confirma que el modelo en su conjunto es altamente significativo y explica mejor los precios que un modelo sin variables.
El modelo logra explicar cerca del 72% de la variación en los precios de las viviendas, lo que es un nivel de ajuste bastante bueno tratándose de datos inmobiliarios. Esto significa que, con las variables consideradas (área construida, estrato, habitaciones, baños y parqueaderos), se puede dar una estimación bastante acertada del precio de una vivienda.
Implicaciones y mejoras posibles:
Una de las implicaciones mas importantes es posible ver si los valores realmente si pertenecen a la zona seleccionada, por el momento al tener la informacion sin presentar un arreglo.
Podriamos incluir la variable barrio, pero primero se debe ajustar esos valores mucho mas para tener unos valores mas exactos y de igual forma se debe realizar un proceso de categorizacion para que entre en el modelo. Puede que sea complicado, porque es muy dificil tener la informacion exacta de las viviendas
Como parte de la validación de supuestos del modelo, se evaluó la multicolinealidad entre las variables explicativas mediante el cálculo del Factor de Inflación de la Varianza (VIF) y su inverso (tolerancia). Esta medida permite identificar si alguno de los predictores está altamente correlacionado con los demás, lo cual puede distorsionar la estimación de los coeficientes.
CONTEO_TABLAS <- CONTEO_TABLAS + 1
# --- 1) Multicolinealidad (VIF) ---
car::vif(m1) %>% as.data.frame() %>% tibble::rownames_to_column("variable") %>%
knitr::kable(caption =
paste0("Tabla ", CONTEO_TABLAS, ". Multicolinealidad: VIF y tolerancia (1/VIF)"))
| variable | . |
|---|---|
| areaconst | 1.298127 |
| estrato | 1.298539 |
| habitaciones | 1.520385 |
| parqueaderos | 1.225259 |
| banios | 1.941811 |
Otro supuesto clave de la regresión lineal múltiple es la homocedasticidad, que establece que la varianza de los errores debe ser constante a lo largo de los valores ajustados por el modelo. Para evaluarlo, se presenta el gráfico de dispersión entre los residuos y los valores ajustados, en el cual se espera observar un patrón aleatorio alrededor de la línea horizontal en cero.
# --- 2) Residuos vs Ajustados (tabla de outliers por |residual estandarizado|) ---
res_df <- augment(m1)
p1 <- ggplot(res_df, aes(.fitted, .resid)) + geom_point(alpha=.5) +
geom_hline(yintercept=0, linetype="dashed") + labs(x="Ajustados", y="Residuos")
plotly::ggplotly(p1)
Otro de los supuestos fundamentales del modelo de regresión lineal múltiple es la normalidad de los errores. Para evaluarlo, se presenta un QQ-plot de los residuos estandarizados, en el cual se comparan los cuantiles observados con los teóricos de una distribución normal. Si los residuos siguen una distribución normal, los puntos deberían alinearse sobre la diagonal.
# --- 3) QQ-plot (tabla de cuantiles teóricos vs muestrales) ---
p2 <- ggplot(res_df, aes(sample = .std.resid)) + stat_qq(alpha=.6) +
stat_qq_line() + labs(title="QQ-plot de residuos estandarizados")
plotly::ggplotly(p2)
Además de la inspección gráfica, se aplicaron pruebas estadísticas formales para evaluar los supuestos clásicos de la regresión lineal múltiple. Estas pruebas permiten contrastar de manera cuantitativa la presencia de heterocedasticidad, la normalidad de los residuos y la autocorrelación de los errores. Los resultados se resumen en las Tablas 14, 15 y 16.
# --- 4) Homocedasticidad: Breusch–Pagan ---
CONTEO_TABLAS <- CONTEO_TABLAS + 1
bp <- lmtest::bptest(m1)
bp_tab <- tibble(
Prueba = bp$method,
Estadístico = unname(bp$statistic),
`gl` = unname(bp$parameter),
`p-valor` = unname(bp$p.value)
)
kable(bp_tab, digits = 4,
caption =
paste0("Tabla ", CONTEO_TABLAS, ". Prueba de homocedasticidad (Breusch–Pagan)"))
| Prueba | Estadístico | gl | p-valor |
|---|---|---|---|
| studentized Breusch-Pagan test | 54.5172 | 5 | 0 |
# --- 5) Normalidad (Shapiro–Wilk) ---
CONTEO_TABLAS <- CONTEO_TABLAS + 1
sw <- tryCatch(shapiro.test(res_df$.resid), error = function(e) NULL)
sw_tab <- if (!is.null(sw)) {
tibble(
Prueba = sw$method,
W = unname(sw$statistic),
`p-valor` = unname(sw$p.value)
)
} else {
tibble(Prueba = "Shapiro-Wilk", W = NA_real_, `p-valor` = NA_real_)
}
kable(sw_tab, digits = 4,
caption =
paste0("Tabla ", CONTEO_TABLAS, ". Prueba de normalidad de residuales (Shapiro–Wilk)"))
| Prueba | W | p-valor |
|---|---|---|
| Shapiro-Wilk normality test | 0.8749 | 0 |
# --- 6) Autocorrelación: Durbin–Watson ---
CONTEO_TABLAS <- CONTEO_TABLAS + 1
dw <- lmtest::dwtest(m1)
dw_tab <- tibble(
Prueba = dw$method,
Estadístico = unname(dw$statistic),
`p-valor` = unname(dw$p.value),
Alternativa = dw$alternative
)
kable(dw_tab, digits = 4,
caption =
paste0("Tabla ", CONTEO_TABLAS, ". Prueba de autocorrelación de residuales (Durbin–Watson)"))
| Prueba | Estadístico | p-valor | Alternativa |
|---|---|---|---|
| Durbin-Watson test | 2.0358 | 0.6074 | true autocorrelation is greater than 0 |
Interpretacion de los resultados:
VIF: Las variables área construida, estrato, habitaciones, parqueaderos y baños no presentan colinealidad problemática. Esto le da solidez al modelo, ya que las estimaciones de los coeficientes no están distorsionadas por redundancia excesiva entre los predictores.
Residuos vs Ajustados: El gráfico de residuos muestra un comportamiento adecuado, no hay patrones evidentes, los errores se distribuyen alrededor de cero y no hay señales claras de heterocedasticidad. El modelo parece cumplir bien los supuestos básicos de linealidad y varianza constante, aunque se recomienda revisar los outliers detectados para asegurarse de que no estén influyendo de manera excesiva en el ajuste.
QQPlot: Sugiere que los residuos son aproximadamente normales, lo cual valida el uso de pruebas estadísticas en el modelo (intervalos de confianza y p-values). Aunque hay ligeras desviaciones en las colas, no representan un problema serio para la validez del modelo.
El modelo pasa de manera satisfactoria las pruebas de diagnóstico:
Esto refuerza la validez de los resultados y sugiere que el modelo ajustado es estadísticamente sólido.
Sugerencias o mejoras: Se podrian realizar los siguientes procesos:
Requisitos:
Con el modelo ajustado, se procedió a realizar la predicción del precio de la Vivienda 1, cuyas condiciones fueron definidas en la solicitud anteriormente indicada. En la Tabla siguiente se presentan los valores ajustados (fit) junto con los intervalos de predicción al 95% (lwr – límite inferior, upr – límite superior).
# 1) Reentrenar con estrato factor
df_model <- train |> # o tu data de entrenamiento antes del split
dplyr::mutate(estrato = as.factor(estrato))
m1_factor <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios,
data = df_model)
# 2) new_v1 con estrato factor y niveles iguales a los de entrenamiento
new_v1 <- tibble::tibble(
areaconst = 200,
estrato = factor(c(4, 5), levels = levels(df_model$estrato)), # <- factor con mismos niveles
habitaciones = 4,
parqueaderos = 1,
banios = 2
)
pred_v1 <- predict(m1_factor, newdata = new_v1, interval = "prediction") |>
tibble::as_tibble()
dplyr::bind_cols(new_v1, pred_v1) |>
knitr::kable(digits = 2, caption = "Predicción Vivienda 1 (M1) con estrato factor")
| areaconst | estrato | habitaciones | parqueaderos | banios | fit | lwr | upr |
|---|---|---|---|---|---|---|---|
| 200 | 4 | 4 | 1 | 2 | 291.77 | -107.47 | 691.01 |
| 200 | 5 | 4 | 1 | 2 | 355.66 | -41.27 | 752.59 |
Observaciones: Los intervalos son muy amplios e incluso incluyen valores negativos, lo que refleja alta incertidumbre en las predicciones del modelo. Esto puede deberse a:
Nota sobre la prediccion: El modelo predice que la Vivienda 1 se ubicaría alrededor de 300–356 millones, lo cual está dentro del crédito preaprobado de 350 millones, Se recomienda contrastar estas estimaciones con la exploración de ofertas reales en el mercado para afinar la decisión.
Sin embargo, solamente uno se ajusta al valor indicado en la actividad, el cual se aproxima a una oferta de casi 291 (M) y si la persona va por esta opcion, se ahorraria un valor de 59M, pero como se indico anteriormente, es necesario evidenciarlo con los casos potenciales a continuacion
A partir del modelo estimado, se filtraron y priorizaron las ofertas de Casas en Zona Norte que cumplen con los requisitos de la solicitud (área ≈ 200 m², ≥1 parqueadero, ≥2 baños, ≥4 habitaciones, estrato 4–5). La priorización se realizó mediante un puntaje de afinidad que pondera cercanía del área objetivo y atributos clave de confort (habitaciones, baños, parqueaderos). En la tabla se listan las 5 opciones con mayor afinidad y su precio predicho por el modelo.
# Función de afinidad a la solicitud
score_afinidad <- function(df, target_area=200, tol_area=.15){
w_area <- 0.5; w_hab <- 0.2; w_ban <- 0.15; w_park <- 0.15
sc <- w_area * (1 - pmin(abs(df$areaconst - target_area)/
(target_area*tol_area), 1)) +
w_hab * pmin(df$habitaciones/4, 1) +
w_ban * pmin(df$banios/2, 1) +
w_park * pmin(df$parqueaderos/1, 1)
return(sc)
}
cand_v1 <- vivienda %>%
filter(tipo=="Casa", str_detect(stringr::str_to_lower(as.character(zona)),
"norte"), estrato %in% c(4,5),
areaconst >= 200 * 0.75, areaconst <= 200 * 1.25,
parqueaderos >= 1, banios >= 2, habitaciones >= 4) %>%
drop_na(longitud, latitud)
cand_v1$pred_m1 <- predict(m1, newdata = cand_v1)
cand_v1 <- cand_v1 %>% filter(pred_m1 <= 350) %>%
mutate(afinidad = score_afinidad(.)) %>%
arrange(desc(afinidad)) %>% slice_head(n = 5)
cand_v1 %>% select(barrio, estrato, areaconst, parqueaderos, banios,
habitaciones, preciom, pred_m1) %>%
mutate(across(c(preciom, pred_m1), ~round(.x,1))) %>%
knitr::kable(caption = "Top 5 ofertas potenciales (Vivienda 1)")
| barrio | estrato | areaconst | parqueaderos | banios | habitaciones | preciom | pred_m1 |
|---|---|---|---|---|---|---|---|
| la merced | 4 | 216 | 2 | 2 | 4 | 360 | 342.9 |
| acopi | 4 | 160 | 1 | 4 | 5 | 600 | 336.1 |
| el bosque | 4 | 250 | 1 | 3 | 4 | 485 | 346.2 |
| el bosque | 4 | 165 | 1 | 4 | 4 | 330 | 337.1 |
| el bosque | 4 | 162 | 1 | 4 | 4 | 340 | 335.5 |
Para complementar la tabla de ofertas potenciales, se construyó un mapa interactivo que muestra la localización geográfica de las viviendas identificadas en la Zona Norte de Cali. Cada marcador representa una opción viable de acuerdo con los criterios establecidos (área ≈ 200 m², ≥1 parqueadero, ≥2 baños, ≥4 habitaciones, estrato 4–5 y precio observado ≤ 350 M).
leaflet(cand_v1) %>% addTiles() %>%
addAwesomeMarkers(~longitud, ~latitud,
popup = ~sprintf("<b>Casa – %s</b><br>Estrato: %s<br>Área: %s m²<br>Parq: %s
· Baños: %s · Hab: %s<br>Precio observado: $%s M<br>Precio predicho (M1): $%s
M",
barrio, estrato, areaconst, parqueaderos, banios,
habitaciones,
scales::comma(preciom), scales::comma(round(pred_m1,1))))
Analisis e interpretacion: Se identificaron 5 alternativas dentro de Zona Norte que cumplen con los atributos solicitados y precio observado ≤ 350 M. La priorización considera afinidad con el requerimiento (área ~200 m² y comodidades). Dado que el modelo presenta intervalos de predicción amplios, la decisión se apoya en el precio de mercado observado y se recomienda visita y verificación documental de las opciones en la tabla.
De estas dadas, solamente dos pueden aportar una vivienda con algunas caracteristicas cumplidas, en la grafica y la tabla, se quiso igual mostrar el beneficio a la persona que desee comprar, por lo cual por ejemplo, en las dos casas del barrio el bosque estan por debajo del presupuesto, ambos se beneficiarian con mas baños, con la misma cantidad de habitaciones pero con un area de construccion mas pequeño.
Lo que si genera algo casi idoneo, es el barrio la merced, cumple con todas las caracteristicas con algunas de ellas mejorando, pero claro se pasa en presupuesto por 10M lo que seria sugerir a la compañia en caso de estimaciones minimas y maximas, pero claro no es la opcion correcta, para lo indicado en la solicitud inicial
En este segundo caso de análisis, se busca focalizar la información en los apartamentos ubicados en la Zona Sur. La preparación de los datos permite filtrar únicamente aquellos registros que cumplen con estas características, con el fin de obtener un panorama más claro sobre las condiciones de las viviendas en esta zona específica.
El objetivo de este paso es disponer de una muestra representativa y organizada que facilite posteriores análisis descriptivos, comparativos o de afinidad con los criterios definidos.
CONTEO_TABLAS <- CONTEO_TABLAS + 1
caso2_data <- vivienda %>%
filter(
tipo == "Apartamento",
str_detect(str_to_lower(zona), "zona sur")
)
# Primeros 3 registros
caso2_data %>% select(zona, tipo, barrio, estrato, areaconst, parqueaderos, banios, habitaciones, preciom, longitud, latitud) %>% head(3) %>% knitr::kable(caption =
paste0("Tabla ", CONTEO_TABLAS, ". Presentando los datos de 3 registros de la informacion de Apartamentos en la Zona Sur"))
| zona | tipo | barrio | estrato | areaconst | parqueaderos | banios | habitaciones | preciom | longitud | latitud |
|---|---|---|---|---|---|---|---|---|---|---|
| Zona Sur | Apartamento | acopi | 4 | 96 | 1 | 2 | 3 | 290 | -76.53464 | 3.44987 |
| Zona Sur | Apartamento | aguablanca | 3 | 40 | 1 | 1 | 2 | 78 | -76.50100 | 3.40000 |
| Zona Sur | Apartamento | aguacatal | 6 | 194 | 2 | 5 | 3 | 875 | -76.55700 | 3.45900 |
Con el fin de conocer la estructura y calidad del subconjunto Apartamentos – Zona Sur, se elabora un resumen de cada variable: tipo de dato, número de valores únicos, conteo de nulos y ejemplos. Esta tabla permite identificar rápidamente posibles problemas de calidad, campos redundantes.
CONTEO_TABLAS <- CONTEO_TABLAS + 1
tabla_resumen <- data.frame(
Variable = names(caso2_data),
Tipo = sapply(caso2_data, class),
Unicos = sapply(caso2_data, function(x) length(unique(x))),
Nulos = sapply(caso2_data, function(x) sum(is.na(x))),
Ejemplo = sapply(caso2_data, function(x) paste(head(unique(x), 13), collapse = ", "))
)
kable(tabla_resumen, caption = paste0("Tabla ", CONTEO_TABLAS, ". Resumen de variables del dataset 'caso2_data'"))
| Variable | Tipo | Unicos | Nulos | Ejemplo | |
|---|---|---|---|---|---|
| id | id | numeric | 2787 | 0 | 5098, 698, 8199, 1241, 5370, 6975, 5615, 6262, 7396, 6949, 7946, 8102, 7073 |
| zona | zona | factor | 1 | 0 | Zona Sur |
| piso | piso | character | 13 | 622 | 05, 02, NA, 06, 08, 10, 03, 01, 04, 07, 09, 11, 12 |
| estrato | estrato | integer | 4 | 0 | 4, 3, 6, 5 |
| preciom | preciom | numeric | 344 | 0 | 290, 78, 875, 135, 220, 210, 105, 115, 230, 344, 910, 130, 150 |
| areaconst | areaconst | numeric | 276 | 0 | 96, 40, 194, 117, 78, 75, 72, 68, 58, 84, 63, 107, 182 |
| parqueaderos | parqueaderos | integer | 6 | 406 | 1, 2, NA, 3, 4, 10 |
| banios | banios | integer | 9 | 0 | 2, 1, 5, 4, 3, 6, 0, 8, 7 |
| habitaciones | habitaciones | integer | 7 | 0 | 3, 2, 4, 5, 0, 6, 1 |
| tipo | tipo | factor | 1 | 0 | Apartamento |
| barrio | barrio | factor | 141 | 0 | acopi, aguablanca, aguacatal, alameda, alf√©rez real, alferez real, alto jordán, altos de guadalupe, arboleda, belisario caicedo, bella suiza, bloques del limonar, bochalema |
| longitud | longitud | numeric | 1191 | 0 | -76.53464, -76.501, -76.557, -76.514, -76.536, -76.54627, -76.53764, -76.54168, -76.54924, -76.546, -76.553, -76.555, -76.547 |
| latitud | latitud | numeric | 1390 | 0 | 3.44987, 3.4, 3.459, 3.441, 3.436, 3.39109, 3.44924, 3.44758, 3.39121, 3.39, 3.372, 3.41, 3.449 |
Para comprender mejor la distribución de los valores en las variables clave del subconjunto de apartamentos de la Zona Sur, se realiza un resumen estadístico. Este paso permite identificar tendencias centrales, rangos de variación y posibles outliers, además de visualizar rápidamente la presencia de datos faltantes en algunos campos.
CONTEO_TABLAS <- CONTEO_TABLAS + 1
# Seleccionar variables
vars <- c("piso","preciom","areaconst","parqueaderos","banios","habitaciones")
# Obtener summary
resumen <- summary(caso2_data[, vars])
# Pasarlo a tabla
tabla_resumen <- as.data.frame.matrix(resumen)
# Mostrar con kable
kable(tabla_resumen, caption =
paste0("Tabla ", CONTEO_TABLAS, ". Resumen de variables numericas seleccionadas"), row.names = FALSE)
| piso | preciom | areaconst | parqueaderos | banios | habitaciones |
|---|---|---|---|---|---|
| Length:2787 | Min. : 75.0 | Min. : 40.00 | Min. : 1.000 | Min. :0.000 | Min. :0.000 |
| Class :character | 1st Qu.: 175.0 | 1st Qu.: 65.00 | 1st Qu.: 1.000 | 1st Qu.:2.000 | 1st Qu.:3.000 |
| Mode :character | Median : 245.0 | Median : 85.00 | Median : 1.000 | Median :2.000 | Median :3.000 |
| NA | Mean : 297.3 | Mean : 97.47 | Mean : 1.415 | Mean :2.488 | Mean :2.966 |
| NA | 3rd Qu.: 335.0 | 3rd Qu.:110.00 | 3rd Qu.: 2.000 | 3rd Qu.:3.000 | 3rd Qu.:3.000 |
| NA | Max. :1750.0 | Max. :932.00 | Max. :10.000 | Max. :8.000 | Max. :6.000 |
| NA | NA | NA | NA’s :406 | NA | NA |
Para caracterizar el perfil socioeconómico de los apartamentos en la Zona Sur, se presenta la distribución del estrato y un mapa interactivo que ubica cada inmueble, resaltando posibles outliers geoespaciales (según latitud). Esta dupla tabla–mapa permite cruzar rápidamente composición de la muestra y coherencia espacial de los registros.
CONTEO_TABLAS <- CONTEO_TABLAS + 1
# Crear tabla de frecuencias
freq_tab <- summarytools::freq(caso2_data$estrato, report.nas = TRUE)
# Convertir a data.frame
freq_df <- as.data.frame(freq_tab)
# Mostrar con kable
kable(freq_df, caption =
paste0("Tabla ", CONTEO_TABLAS, ". Frecuencia de la variable 'estrato' para el caso 2"))
| Freq | % Valid | % Valid Cum. | % Total | % Total Cum. | |
|---|---|---|---|---|---|
| 3 | 201 | 7.212056 | 7.212056 | 7.212056 | 7.212056 |
| 4 | 1091 | 39.146035 | 46.358091 | 39.146035 | 46.358091 |
| 5 | 1033 | 37.064944 | 83.423035 | 37.064944 | 83.423035 |
| 6 | 462 | 16.576964 | 100.000000 | 16.576964 | 100.000000 |
| 0 | NA | NA | 0.000000 | 100.000000 | |
| Total | 2787 | 100.000000 | 100.000000 | 100.000000 | 100.000000 |
# --- Datos y banderas ---
# Outliers de latitud dentro de "Zona Sur" (case-insensitive)
caso2_data_2 <- caso2_data %>%
mutate(
zona_chr = as.character(zona),
es_sur = str_detect(str_to_lower(zona_chr), "zona sur")
)
lat_mu <- mean(caso2_data_2$latitud[caso2_data_2$es_sur], na.rm = TRUE)
lat_sd <- sd( caso2_data_2$latitud[caso2_data_2$es_sur], na.rm = TRUE)
caso2_data_2 <- caso2_data_2 %>%
mutate(
flag_outlier_lat = ifelse(es_sur & !is.na(latitud) & !is.na(lat_mu) & !is.na(lat_sd),
abs(latitud - lat_mu) > 2 * lat_sd, FALSE)
)
# Palette por zona (colorea el relleno del punto)
pal_zona <- colorFactor(
palette = brewer.pal(max(3, min(8, length(unique(caso2_data_2$zona_chr)))), "Set1"),
domain = caso2_data_2$zona_chr,
na.color = "#999999"
)
# SharedData para activar el filtro sin Shiny
sd_viv <- SharedData$new(caso2_data_2)
# Mapa
mapa <- leaflet(sd_viv) %>%
addTiles() %>%
addCircleMarkers(
lng = ~longitud, lat = ~latitud,
radius = ~ifelse(flag_outlier_lat, 8, 6),
stroke = TRUE,
weight = ~ifelse(flag_outlier_lat, 2, 1),
color = ~ifelse(flag_outlier_lat, "#d62728", "#333333"), # Borde (rojo si outlier)
fill = TRUE,
fillOpacity = 0.8,
fillColor = ~pal_zona(zona_chr),
popup = ~paste0(
"<b>", tipo, " – ", zona_chr, "</b><br>",
"Barrio: ", barrio, "<br>",
"Estrato: ", estrato, "<br>",
"Área: ", comma(areaconst), " m²<br>",
"Parqueaderos: ", parqueaderos,
" – Baños: ", banios,
" – Hab: ", habitaciones, "<br>",
"Precio: $", comma(preciom), " M"
)
) %>%
addLegend(
position = "bottomleft",
pal = pal_zona, values = ~zona_chr,
title = "Zona (color de relleno)",
opacity = 1
) %>%
addLegend(
position = "bottomright",
colors = c("#333333", "#d62728"),
labels = c("Normal", "Posible outlier (lat)"),
title = "Borde del punto"
)
# Disposición: filtro arriba, mapa abajo
crosstalk::bscols(widths = c(12),
mapa
)
Discussion: ¿Todos los puntos se ubican en la zona correspondiente o se presentan valores en otras zonas? ¿por que?
De manera visual, No todos los puntos se encuentran exactamente dentro de la Zona Sur. Si bien la mayoría de registros cumplen con el criterio de filtro aplicado (tipo = “Apartamento” y zona = “Zona Sur”), en el mapa Leaflet se identifican varios casos marcados con borde rojo, correspondientes a posibles outliers en la latitud
Esto ocurre porque:
En el propósito de analizar la relación entre las principales variables numéricas del conjunto de apartamentos en la Zona Sur, se construyó una matriz de correlación. Esta herramienta permite identificar qué variables presentan una asociación lineal más fuerte, positiva o negativa, lo cual resulta clave para la selección de predictores en futuros modelos.
num_cols <- c("preciom","areaconst","estrato","banios","habitaciones","parqueaderos")
cor_mat <- caso2_data %>% select(all_of(num_cols)) %>% cor(use = "pairwise.complete.obs")
# Heatmap interactivo
plotly::plot_ly(
x = colnames(cor_mat), y = rownames(cor_mat), z = cor_mat,
type = "heatmap", showscale = TRUE
) %>% layout(title = "Grafico 1. Correlación entre variables numéricas")
El gráfico de pares muestra, en una sola vista, la distribución de cada variable (diagonal), las correlaciones numéricas (triángulo superior) y las relaciones bivariadas (triángulo inferior) entre precio, área, estrato, baños y habitaciones. Para zona (categórica) se visualizan boxplots frente a cada numérica.
p <-
ggpairs(
caso2_data,
columns = c("preciom","areaconst", "estrato", "banios", "habitaciones", "zona"),
title = "Grafico 2. Relación del precio en función de las variables seleccionadas",
columnLabels = c("Precio","Área", "Estrato", "Baños", "Habitaciones", "Zona"),
diag = list(continuous = wrap("barDiag", bins = 20))
)
ggplotly(p) # hace el gráfico interactivo
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
Estos gráficos presentan la distribución del precio (millones) y del área construida (m²) en función de variables categóricas clave: zona, estrato socioeconómico, número de habitaciones y número de baños. El análisis con boxplots permite visualizar tendencias centrales, dispersión y la presencia de outliers.
# Variables categóricas
vars_cat <- c("zona", "habitaciones", "estrato", "banios")
# Etiquetas bonitas para los ejes
var_labels <- c(
zona = "Zona",
habitaciones = "Número de habitaciones",
estrato = "Estrato socioeconómico",
banios = "Número de baños"
)
# Base
df <- caso2_data %>%
mutate(across(all_of(vars_cat), ~as.factor(.))) %>%
select(preciom, all_of(vars_cat)) %>%
filter(!is.na(preciom))
# Función que construye un boxplot
make_box <- function(var) {
# Ordenar categorías por mediana de precio
med_order <- df %>%
group_by(.data[[var]]) %>%
summarise(med = median(preciom, na.rm = TRUE), .groups = "drop") %>%
arrange(med) %>%
pull(1) %>% as.character()
data_var <- df %>%
transmute(
Categoria = fct_relevel(as.factor(.data[[var]]), med_order),
preciom = preciom
)
plot_ly(
data_var,
x = ~Categoria, y = ~preciom,
type = "box",
boxpoints = "outliers"
) %>%
layout(
title = var_labels[[var]], # título de cada subplot
xaxis = list(title = var_labels[[var]], tickangle = 45),
yaxis = list(title = "Precio (millones)")
)
}
plots <- map(vars_cat, make_box)
# Matriz de subplots
subplot(
plots,
nrows = ceiling(length(plots) / 2),
shareY = TRUE, titleX = TRUE, titleY = TRUE,
margin = 0.05
) %>%
layout(
showlegend = FALSE,
title = list(text = "Grafico 3. Distribución de 'preciom' por variables categóricas")
)
# Variables categóricas
vars_cat <- c("zona", "habitaciones", "estrato", "banios")
# Etiquetas bonitas para los ejes
var_labels <- c(
zona = "Zona",
habitaciones = "Número de habitaciones",
estrato = "Estrato socioeconómico",
banios = "Número de baños"
)
# Base
df <- caso2_data %>%
mutate(across(all_of(vars_cat), ~as.factor(.))) %>%
select(areaconst, all_of(vars_cat)) %>%
filter(!is.na(areaconst))
# Función que construye un boxplot
make_box <- function(var) {
# Ordenar categorías por mediana de areaconst
med_order <- df %>%
group_by(.data[[var]]) %>%
summarise(med = median(areaconst, na.rm = TRUE), .groups = "drop") %>%
arrange(med) %>%
pull(1) %>% as.character()
data_var <- df %>%
transmute(
Categoria = fct_relevel(as.factor(.data[[var]]), med_order),
areaconst = areaconst
)
plot_ly(
data_var,
x = ~Categoria, y = ~areaconst,
type = "box",
boxpoints = "outliers"
) %>%
layout(
title = var_labels[[var]], # título del gráfico
xaxis = list(title = var_labels[[var]], tickangle = 45),
yaxis = list(title = "Área (m²)")
)
}
plots <- map(vars_cat, make_box)
# Matriz de subplots
subplot(
plots,
nrows = ceiling(length(plots) / 2),
shareY = TRUE, titleX = TRUE, titleY = TRUE,
margin = 0.05
) %>%
layout(
showlegend = FALSE,
title = list(text = "Grafico 4. Distribución de 'areaconst' por variables categóricas")
)
Interpretacion de los resultados:
Para finalizar, dando un resumen en los boxplot a continuacion:
La tabla presenta los resultados del modelo de regresión lineal múltiple aplicado a los apartamentos de la Zona Sur. Se incluyen los coeficientes estimados, errores estándar, estadísticos t y valores p asociados a cada variable predictora. Estos resultados permiten identificar el efecto individual de cada variable sobre el precio estimado de los inmuebles, manteniendo constantes las demás.
CONTEO_TABLAS <- CONTEO_TABLAS + 1
library(broom)
library(knitr)
# Resultados de coeficientes como data.frame
tabla_coef <- tidy(m2)
kable(tabla_coef, digits = 4, caption =
paste0("Tabla ", CONTEO_TABLAS, ". Coeficientes del modelo de regresión"))
| term | estimate | std.error | statistic | p.value |
|---|---|---|---|---|
| (Intercept) | -235.8747 | 25.6838 | -9.1838 | 0e+00 |
| areaconst | 1.9639 | 0.1037 | 18.9388 | 0e+00 |
| estrato | 49.8435 | 5.1616 | 9.6565 | 0e+00 |
| habitaciones | -25.7640 | 6.5252 | -3.9484 | 1e-04 |
| parqueaderos | 58.6842 | 7.5044 | 7.8200 | 0e+00 |
| banios | 46.2306 | 5.7373 | 8.0580 | 0e+00 |
En La siguente tabla resume las métricas globales del modelo de regresión lineal múltiple para evaluar su ajuste y capacidad explicativa. Estas estadísticas permiten analizar qué tan bien se ajusta el modelo a los datos y si los resultados son confiables.
CONTEO_TABLAS <- CONTEO_TABLAS + 1
glance(m2) %>%
kable(digits = 4, caption =
paste0("Tabla ", CONTEO_TABLAS, ". Métricas del modelo"))
| r.squared | adj.r.squared | sigma | statistic | p.value | df | logLik | AIC | BIC | deviance | df.residual | nobs |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0.8039 | 0.8027 | 94.2462 | 688.72 | 0 | 5 | -5043.252 | 10100.5 | 10133.69 | 7461176 | 840 | 846 |
Interpretacion de los coeficientes:
Interpretacion del ajuste del modelo:
Implicaciones y mejoras posibles:
Como parte de la validación del modelo 2, se evaluó la multicolinealidad entre las variables explicativas mediante el cálculo del Factor de Inflación de la Varianza (VIF) y su inverso (tolerancia). Esta medida permite identificar si existe redundancia excesiva entre predictores, lo cual podría afectar la estabilidad de los coeficientes estimados.
CONTEO_TABLAS <- CONTEO_TABLAS + 1
# --- 1) Multicolinealidad (VIF) ---
car::vif(m2) %>% as.data.frame() %>% tibble::rownames_to_column("variable") %>%
knitr::kable(caption =
paste0("Tabla ", CONTEO_TABLAS, ". Multicolinealidad: VIF y tolerancia (1/VIF)"))
| variable | . |
|---|---|
| areaconst | 2.710405 |
| estrato | 1.733777 |
| habitaciones | 1.361302 |
| parqueaderos | 2.485662 |
| banios | 2.850477 |
Como parte de la validación del modelo 2, se revisó el supuesto de homocedasticidad, el cual establece que la varianza de los errores debe ser constante a lo largo de los valores predichos. Para ello se presenta el gráfico de dispersión entre los residuos y los valores ajustados, en el que se espera un patrón aleatorio alrededor de la línea horizontal en cero.
# --- 2) Residuos vs Ajustados (tabla de outliers por |residual estandarizado|) ---
res_df <- augment(m2)
p1 <- ggplot(res_df, aes(.fitted, .resid)) + geom_point(alpha=.5) +
geom_hline(yintercept=0, linetype="dashed") + labs(x="Ajustados", y="Residuos")
plotly::ggplotly(p1)
El supuesto de normalidad de los residuos es fundamental en la regresión lineal múltiple, especialmente para la validez de los intervalos de confianza y pruebas de significancia. Para evaluarlo, se construyó un QQ-plot que compara los cuantiles de los residuos estandarizados con los cuantiles teóricos de una distribución normal.
# --- 3) QQ-plot (tabla de cuantiles teóricos vs muestrales) ---
p2 <- ggplot(res_df, aes(sample = .std.resid)) + stat_qq(alpha=.6) +
stat_qq_line() + labs(title="QQ-plot de residuos estandarizados")
plotly::ggplotly(p2)
Como complemento a los gráficos de diagnóstico, se aplicaron pruebas estadísticas formales para evaluar los supuestos clásicos de la regresión lineal múltiple en el modelo 2. En particular, se consideraron la homocedasticidad, la normalidad de los residuos y la independencia de los errores. Los resultados se presentan en las Tablas 24, 25 y 26.
# --- 4) Homocedasticidad: Breusch–Pagan ---
CONTEO_TABLAS <- CONTEO_TABLAS + 1
bp <- lmtest::bptest(m2)
bp_tab <- tibble(
Prueba = bp$method,
Estadístico = unname(bp$statistic),
`gl` = unname(bp$parameter),
`p-valor` = unname(bp$p.value)
)
kable(bp_tab, digits = 4,
caption =
paste0("Tabla ", CONTEO_TABLAS, ". Prueba de homocedasticidad (Breusch–Pagan)"))
| Prueba | Estadístico | gl | p-valor |
|---|---|---|---|
| studentized Breusch-Pagan test | 298.4663 | 5 | 0 |
# --- 5) Normalidad (Shapiro–Wilk) ---
CONTEO_TABLAS <- CONTEO_TABLAS + 1
sw <- tryCatch(shapiro.test(res_df$.resid), error = function(e) NULL)
sw_tab <- if (!is.null(sw)) {
tibble(
Prueba = sw$method,
W = unname(sw$statistic),
`p-valor` = unname(sw$p.value)
)
} else {
tibble(Prueba = "Shapiro-Wilk", W = NA_real_, `p-valor` = NA_real_)
}
kable(sw_tab, digits = 4,
caption =
paste0("Tabla ", CONTEO_TABLAS, ". Prueba de normalidad de residuales (Shapiro–Wilk)"))
| Prueba | W | p-valor |
|---|---|---|
| Shapiro-Wilk normality test | 0.7685 | 0 |
# --- 6) Autocorrelación: Durbin–Watson ---
CONTEO_TABLAS <- CONTEO_TABLAS + 1
dw <- lmtest::dwtest(m2)
dw_tab <- tibble(
Prueba = dw$method,
Estadístico = unname(dw$statistic),
`p-valor` = unname(dw$p.value),
Alternativa = dw$alternative
)
kable(dw_tab, digits = 4,
caption =
paste0("Tabla ", CONTEO_TABLAS, ". Prueba de autocorrelación de residuales (Durbin–Watson)"))
| Prueba | Estadístico | p-valor | Alternativa |
|---|---|---|---|
| Durbin-Watson test | 1.993 | 0.4589 | true autocorrelation is greater than 0 |
Interpretacion de los resultados:
Factor de Inflación de la Varianza (VIF):
Grafico de Homecedasticidad:
QQplot:
Tablas de pruebas:
Sugerencias o mejoras:
Requisitos:
Con el modelo ajustado para el Caso 2, se procedió a realizar la predicción del precio de la Vivienda 2, de acuerdo con las condiciones establecidas en los requerimientos. La Tabla siguiente presenta el valor estimado (fit) junto con los intervalos de predicción al 95% (lwr – límite inferior, upr – límite superior) para cada estrato considerado.
# 1) Reentrenar con estrato factor
df_model <- train |> # o tu data de entrenamiento antes del split
dplyr::mutate(estrato = as.factor(estrato))
m2_factor <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios,
data = df_model)
# 2) new_v1 con estrato factor y niveles iguales a los de entrenamiento
new_v2 <- tibble::tibble(
areaconst = 300,
estrato = factor(c(5, 6), levels = levels(df_model$estrato)), # <- factor con mismos niveles
habitaciones = 5,
parqueaderos = 3,
banios = 3
)
pred_v2 <- predict(m2_factor, newdata = new_v2, interval = "prediction") |>
tibble::as_tibble()
dplyr::bind_cols(new_v2, pred_v2) |>
knitr::kable(digits = 2, caption = "Predicción Vivienda 1 (M1) con estrato factor")
| areaconst | estrato | habitaciones | parqueaderos | banios | fit | lwr | upr |
|---|---|---|---|---|---|---|---|
| 300 | 5 | 5 | 3 | 3 | 751.10 | 568.78 | 933.42 |
| 300 | 6 | 5 | 3 | 3 | 870.36 | 688.16 | 1052.56 |
Observaciones:
Nota sobre la prediccion:
A partir de las características definidas en la solicitud de la Vivienda 2 (apartamento en Zona Sur, área cercana a 300 m², 3 parqueaderos, 3 baños, 5 habitaciones, estrato 5–6 y crédito máximo de 850 M), se realizó un filtrado de la base de datos para identificar opciones viables.
Para priorizar los resultados, se aplicó una función que busca toda similitud de cada inmueble con los requisitos de área, número de habitaciones, baños y parqueaderos. De esta manera, se intentaron identificar las cinco ofertas más cercanas al perfil solicitado, cuyos detalles se presentan en la tabla y mapa siguientes, junto con el precio observado en el mercado y el precio estimado por el modelo.
# Función de afinidad a la solicitud
score_afinidad <- function(df, target_area=300, tol_area=.15){
w_area <- 0.5; w_hab <- 0.2; w_ban <- 0.15; w_park <- 0.15
sc <- w_area * (1 - pmin(abs(df$areaconst - target_area)/
(target_area*tol_area), 1)) +
w_hab * pmin(df$habitaciones/4, 1) +
w_ban * pmin(df$banios/2, 1) +
w_park * pmin(df$parqueaderos/1, 1)
return(sc)
}
cand_v2 <- vivienda %>%
filter(tipo=="Apartamento", str_detect(stringr::str_to_lower(as.character(zona)),
"sur"), estrato %in% c(5,6),
areaconst >= 300 * 0.05, areaconst <= 300 * 1.95,
parqueaderos >= 3, banios >= 3, habitaciones >= 5) %>%
drop_na(longitud, latitud)
cand_v2$pred_m2 <- predict(m2, newdata = cand_v2)
cand_v2 <- cand_v2 %>% filter(pred_m2 <= 850) %>%
mutate(afinidad = score_afinidad(.)) %>%
arrange(desc(afinidad)) %>% slice_head(n = 5)
cand_v2 %>% select(barrio, estrato, areaconst, parqueaderos, banios,
habitaciones, preciom, pred_m2) %>%
mutate(across(c(preciom, pred_m2), ~round(.x,1))) %>%
knitr::kable(caption = "Top 5 ofertas potenciales (Vivienda 2)")
| barrio | estrato | areaconst | parqueaderos | banios | habitaciones | preciom | pred_m2 |
|---|---|---|---|---|---|---|---|
| seminario | 5 | 256 | 3 | 5 | 5 | 530 | 794.5 |
| ciudad jardín | 6 | 240 | 3 | 5 | 6 | 1500 | 787.1 |
Como se mostro en la tabla, y a continuacion en la grafica solamente dos valores fueron encontrados, lo que no cumpliria con lo solicitado de solamente tener minimo 5 elementos se debe saber bien porque ocurrio ese detalle, o que informacion estariamos omitiendo.
leaflet(cand_v2) %>% addTiles() %>%
addAwesomeMarkers(~longitud, ~latitud,
popup = ~sprintf("<b>Apartamento – %s</b><br>Estrato: %s<br>Área: %s m²<br>Parq: %s
· Baños: %s · Hab: %s<br>Precio observado: $%s M<br>Precio predicho (M2): $%s
M",
barrio, estrato, areaconst, parqueaderos, banios,
habitaciones,
scales::comma(preciom), scales::comma(round(pred_m2,1))))
Con base en los requisitos de la Vivienda 2 (apartamento en Zona Sur, área cercana a 300 m², ≥3 parqueaderos, 3 baños, 5 habitaciones, estrato 5–6 y crédito ≤ 850 M), se filtraron y priorizaron las ofertas del dataset. A continuación, se presentan las ofertas mejor alineadas con la solicitud. Al no alcanzar el mínimo de 5 opciones estrictamente, se documenta que cada uno de los filtros aplicados solamente muestran dos valores en todo el dataset por eso la validacion es simple, especialmente por que solamente para la zona sur los hogares con 3 parqueaderos son muy pocos.
tabla_apto_sur_area <- vivienda %>%
filter(
tipo == "Apartamento",
str_detect(str_to_lower(as.character(zona)), "sur"),
areaconst < 300,
habitaciones >= 5,
parqueaderos >= 3
) %>%
select(zona, barrio, estrato, areaconst, parqueaderos, banios, habitaciones, preciom) %>%
arrange(desc(areaconst)) # Ordena de mayor a menor área (opcional)
# Mostrar tabla
kable(tabla_apto_sur_area, caption = "Apartamentos en Zona Sur con área menor a 300 m²")
| zona | barrio | estrato | areaconst | parqueaderos | banios | habitaciones | preciom |
|---|---|---|---|---|---|---|---|
| Zona Sur | seminario | 5 | 256 | 3 | 5 | 5 | 530 |
| Zona Sur | ciudad jardín | 6 | 240 | 3 | 5 | 6 | 1500 |
Analisis e interpretacion: El análisis de los datos para la Vivienda 2 (apartamento en Zona Sur, estrato 5–6, con área cercana a 300 m², 5 habitaciones, 3 baños y 3 parqueaderos) evidencia que el mercado disponible bajo estas condiciones es muy reducido. Inicialmente, solo se encontraron dos registros que cumplían estrictamente con los criterios definidos, lo que refleja la escasez de este tipo de inmuebles en la base utilizada.
Exploración inicial: Se confirmó que la mayor concentración de registros se ubica en la Zona Sur (57%) y que los apartamentos representan más del 60% de las ofertas, lo que marca un gran sesgo hacia este tipo de inmuebles.
Segmentación con K-Means: La agrupación óptima de las viviendas se dio en cuatro clusters elegidos por revision visual, asociados principalmente a las zonas Norte, Sur, Oriente y Occidente, lo que reflejó patrones espaciales claros en la distribución inmobiliaria.
Caso 1 (Casa en Zona Norte): El modelo de RLM explicó cerca del 57% de la variabilidad en precios. Se identificaron cinco alternativas potenciales, siendo la opción más ajustada una casa en el barrio El Bosque con precio de 330 M, dentro del crédito preaprobado.
Caso 2 (Apartamento en Zona Sur): El modelo de RLM alcanzó un ajuste superior de R² igual a 0.80. Sin embargo, el mercado disponible con las condiciones solicitadas fue muy limitado, encontrándose únicamente dos opciones, de las cuales solo una (barrio Seminario, 530 M) resultó viable dentro del presupuesto definido.
Validación de modelos: Ambos modelos cumplieron de manera razonable los supuestos de linealidad, normalidad y multicolinealidad aceptable, aunque se detectó heterocedasticidad en precios altos y presencia de outliers que reducen precisión en intervalos de predicción.