##############################
# Setup general
##############################
knitr::opts_chunk$set(echo = TRUE, message = FALSE, warning = FALSE)
set.seed(1234)
req_pkgs <- c(
"devtools","dplyr","tidyr","stringr","forcats","ggplot2","plotly","leaflet",
"sf","broom","broom.helpers","purrr","readr","janitor","skimr","summarytools",
"car","lmtest","performance","caret","GGally"
)
to_install <- req_pkgs[!sapply(req_pkgs, requireNamespace, quietly = TRUE)]
if(length(to_install)) install.packages(to_install)
# paquete de datos (solo si no está)
if(!requireNamespace("paqueteMODELOS", quietly = TRUE)){
devtools::install_github("centromagis/paqueteMODELOS", force = TRUE)
}
library(paqueteMODELOS)
library(dplyr); library(tidyr); library(stringr); library(forcats)
library(ggplot2); library(plotly); library(leaflet); library(sf)
library(broom); library(broom.helpers); library(purrr); library(readr)
library(janitor); library(skimr); library(summarytools)
library(car); library(lmtest); library(performance); library(caret); library(GGally)
options(scipen = 999)
data("vivienda") # del paquete paqueteMODELOS
raw <- vivienda %>%
clean_names()
glimpse(raw)
## 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…
##############################
#Carga de datos
##############################
library(dplyr)
library(janitor)
library(tibble)
library(knitr)
library(plotly)
# 0.1 Cargar dataset en entorno aislado
env_data <- new.env()
data("vivienda", package = "paqueteMODELOS", envir = env_data)
raw <- as_tibble(env_data$vivienda)
# 0.2 Reporte de NA
na_tbl <- tibble(
variable = names(raw),
na_count = vapply(raw, function(x) sum(is.na(x)), integer(1)),
n = nrow(raw)
) %>%
mutate(na_pct = round(100 * na_count / n, 2)) %>%
arrange(desc(na_count))
# Mostrar tabla de
kable(na_tbl, caption = "NA por variable (conteo y porcentaje) - Antes de limpieza")
| variable | na_count | n | na_pct |
|---|---|---|---|
| piso | 2638 | 8322 | 31.70 |
| parqueaderos | 1605 | 8322 | 19.29 |
| id | 3 | 8322 | 0.04 |
| zona | 3 | 8322 | 0.04 |
| estrato | 3 | 8322 | 0.04 |
| areaconst | 3 | 8322 | 0.04 |
| banios | 3 | 8322 | 0.04 |
| habitaciones | 3 | 8322 | 0.04 |
| tipo | 3 | 8322 | 0.04 |
| barrio | 3 | 8322 | 0.04 |
| longitud | 3 | 8322 | 0.04 |
| latitud | 3 | 8322 | 0.04 |
| preciom | 2 | 8322 | 0.02 |
# 0.3 Gráfico interactivo Plotly
plot_ly(
data = na_tbl,
x = ~reorder(variable, -na_pct),
y = ~na_pct,
type = "bar",
text = ~paste0(na_count, " NA (", na_pct, "%)"),
hoverinfo = "text"
) %>%
layout(
title = "Porcentaje de NA por variable",
xaxis = list(title = "Variable"),
yaxis = list(title = "% NA")
)
# 0.4 Limpieza
clean_vivienda <- function(df){
df %>%
clean_names() %>%
mutate(
zona = as.character(zona),
tipo = as.character(tipo),
barrio = as.character(barrio),
estrato = suppressWarnings(as.integer(estrato)),
areaconst = suppressWarnings(as.numeric(areaconst)),
parqueaderos = suppressWarnings(as.integer(parqueaderos)),
banios = suppressWarnings(as.integer(banios)),
habitaciones = suppressWarnings(as.integer(habitaciones)),
preciom = suppressWarnings(as.numeric(preciom)),
latitud = suppressWarnings(as.numeric(latitud)),
longitud = suppressWarnings(as.numeric(longitud))
) %>%
mutate(
zona = stringr::str_squish(zona),
tipo = stringr::str_to_title(tipo)
) %>%
filter(
between(latitud, 3.2, 3.6),
between(longitud, -76.7, -76.4)
) %>%
filter(!if_any(
c(preciom, areaconst, estrato, parqueaderos, banios, habitaciones),
is.na
)) %>%
mutate(
zona = as.factor(zona),
tipo = as.factor(tipo)
)
}
df <- clean_vivienda(raw)
# 0.5 Vistas de datos post-limpieza
library(tidyr)
# NA por variable (después de la limpieza)
na_post <- df %>%
dplyr::summarise(dplyr::across(
dplyr::everything(), ~sum(is.na(.x))
)) %>%
tidyr::pivot_longer(dplyr::everything(),
names_to = "variable", values_to = "na_count") %>%
dplyr::mutate(na_pct = round(100 * na_count / nrow(df), 2)) %>%
dplyr::arrange(dplyr::desc(na_count))
knitr::kable(na_post, caption = "NA por variable (post-limpieza)")
| variable | na_count | na_pct |
|---|---|---|
| piso | 1909 | 28.42 |
| id | 0 | 0.00 |
| zona | 0 | 0.00 |
| estrato | 0 | 0.00 |
| preciom | 0 | 0.00 |
| areaconst | 0 | 0.00 |
| parqueaderos | 0 | 0.00 |
| banios | 0 | 0.00 |
| habitaciones | 0 | 0.00 |
| tipo | 0 | 0.00 |
| barrio | 0 | 0.00 |
| longitud | 0 | 0.00 |
| latitud | 0 | 0.00 |
Solo se realiza limpieza a las variables de interes, la variable “piso” no se utilizará en el módelo.
Especificaciones objetivo Vivienda 1
Tipo = Casa | Zona = Norte | Área ≈ 200 | Parqueaderos ≥ 1 | Baños ≥ 2 |
Habitaciones ≥ 4 | Estrato {4,5} | Presupuesto ≤ 350 millones
Especificaciones objetivo Vvivienda 2
Tipo = Apartamento | Zona = Sur | Área ≈ 300 | Parqueaderos ≥ 3 | Baños
≥ 3 | Habitaciones ≥ 5 | Estrato {5,6} | Presupuesto ≤ 850 millones
##############################
# 1) Zona geográfica por coordenadas (Norte / Sur)
##############################
library(dplyr)
library(knitr)
library(leaflet)
# A) Umbral de latitud data-driven (k-means sobre latitud)
set.seed(2025)
km_lat <- kmeans(df$latitud, centers = 2, nstart = 50)
lat_cuts <- sort(as.numeric(km_lat$centers)) # dos centroides (Sur < Norte)
lat_cut <- mean(lat_cuts) # punto medio como línea de separación
# B) Etiqueta geográfica (sin usar la columna 'zona' original)
df <- df %>%
mutate(
zona_geo = if_else(latitud >= lat_cut, "Zona Norte (geo)", "Zona Sur (geo)"),
zona_geo = factor(zona_geo, levels = c("Zona Sur (geo)", "Zona Norte (geo)"))
)
# Chequeo rápido
kable(tibble(`Umbral de latitud` = lat_cut), caption = "Línea de corte Norte/Sur (latitud)")
| Umbral de latitud |
|---|
| 3.417411 |
df %>% count(zona_geo) %>% kable(caption = "Frecuencia por zona geográfica derivada")
| zona_geo | n |
|---|---|
| Zona Sur (geo) | 3572 |
| Zona Norte (geo) | 3145 |
# (Opcional) Comparación con la variable original 'zona' si existe
if ("zona" %in% names(df)) {
df %>%
count(zona, zona_geo) %>%
tidyr::pivot_wider(names_from = zona_geo, values_from = n, values_fill = 0) %>%
kable(caption = "Tabla cruzada: zona (original) vs zona_geo (por coordenadas)")
}
| zona | Zona Sur (geo) | Zona Norte (geo) |
|---|---|---|
| Zona Centro | 2 | 62 |
| Zona Norte | 65 | 1222 |
| Zona Oeste | 49 | 1049 |
| Zona Oriente | 29 | 134 |
| Zona Sur | 3427 | 678 |
##############################
# 1.1 Subconjunto Paso 1: Casas en Zona Norte (con base en coordenadas)
##############################
casas_norte_geo <- df %>%
filter(tipo == "Casa", zona_geo == "Zona Norte (geo)")
# Primeros 3 registros (como exige el paso 1)
casas_norte_geo %>%
slice_head(n = 3) %>%
select(barrio, zona_geo, estrato, areaconst, parqueaderos, banios, habitaciones, preciom, latitud, longitud) %>%
kable(caption = "Primeros 3 registros — Casas (Zona Norte por coordenadas)")
| barrio | zona_geo | estrato | areaconst | parqueaderos | banios | habitaciones | preciom | latitud | longitud |
|---|---|---|---|---|---|---|---|---|---|
| 20 de julio | Zona Norte (geo) | 3 | 70 | 1 | 3 | 6 | 250 | 3.43382 | -76.51168 |
| 20 de julio | Zona Norte (geo) | 3 | 120 | 1 | 2 | 3 | 320 | 3.43369 | -76.51237 |
| 20 de julio | Zona Norte (geo) | 3 | 220 | 2 | 2 | 4 | 350 | 3.43566 | -76.51537 |
# Tablas de verificación
casas_norte_geo %>% count(zona_geo) %>% kable(caption = "Chequeo de zona — Casas Norte (geo)")
| zona_geo | n |
|---|---|
| Zona Norte (geo) | 1011 |
casas_norte_geo %>% count(estrato, sort = TRUE) %>% kable(caption = "Frecuencia por estrato — Casas Norte (geo)")
| estrato | n |
|---|---|
| 5 | 342 |
| 3 | 297 |
| 4 | 229 |
| 6 | 143 |
# Mapa exploratorio (Casas Norte)
leaflet(casas_norte_geo) %>%
addTiles() %>%
addCircleMarkers(
lng = ~longitud, lat = ~latitud,
popup = ~paste0("<b>", barrio, "</b><br>",
"Zona (geo): ", zona_geo, "<br>",
"Estrato: ", estrato, "<br>",
"Área: ", areaconst, " m²<br>",
"Baños/Hab/Parq: ", banios, " / ", habitaciones, " / ", parqueaderos, "<br>",
"Precio (M): ", round(preciom, 1)),
radius = 6, stroke = FALSE, fillOpacity = 0.85,
clusterOptions = markerClusterOptions()
)
##############################
# 1.2 (Para el caso 2) Apartamentos en Zona Sur (por coordenadas)
##############################
apto_sur_geo <- df %>%
filter(tipo == "Apartamento", zona_geo == "Zona Sur (geo)")
# Primeros 3 registros
apto_sur_geo %>%
slice_head(n = 3) %>%
select(barrio, zona_geo, estrato, areaconst, parqueaderos, banios, habitaciones, preciom, latitud, longitud) %>%
kable(caption = "Primeros 3 registros — Apartamentos (Zona Sur por coordenadas)")
| barrio | zona_geo | estrato | areaconst | parqueaderos | banios | habitaciones | preciom | latitud | longitud |
|---|---|---|---|---|---|---|---|---|---|
| acopi | Zona Sur (geo) | 5 | 87 | 1 | 3 | 3 | 240 | 3.36971 | -76.51700 |
| acopi | Zona Sur (geo) | 5 | 137 | 2 | 3 | 4 | 310 | 3.38296 | -76.53105 |
| acopi | Zona Sur (geo) | 4 | 108 | 2 | 3 | 3 | 320 | 3.40770 | -76.53638 |
# Tablas de verificación
apto_sur_geo %>% count(zona_geo) %>% kable(caption = "Chequeo de zona — Aptos Sur (geo)")
| zona_geo | n |
|---|---|
| Zona Sur (geo) | 2097 |
apto_sur_geo %>% count(estrato, sort = TRUE) %>% kable(caption = "Frecuencia por estrato — Aptos Sur (geo)")
| estrato | n |
|---|---|
| 5 | 898 |
| 4 | 669 |
| 6 | 426 |
| 3 | 104 |
# Mapa exploratorio (Aptos Sur)
leaflet(apto_sur_geo) %>%
addTiles() %>%
addCircleMarkers(
lng = ~longitud, lat = ~latitud,
popup = ~paste0("<b>", barrio, "</b><br>",
"Zona (geo): ", zona_geo, "<br>",
"Estrato: ", estrato, "<br>",
"Área: ", areaconst, " m²<br>",
"Baños/Hab/Parq: ", banios, " / ", habitaciones, " / ", parqueaderos, "<br>",
"Precio (M): ", round(preciom, 1)),
radius = 6, stroke = FALSE, fillOpacity = 0.85,
clusterOptions = markerClusterOptions()
)
Debido a que encontraron problemas con la sectorización utilizando la variable “zona”, se dividió la ciudad Cali utilizando las variables lat/long en lugar de utilizar la columna “zona” para evitar etiquetas mal diligenciadas y mejorar la consistencia espacial. Se crearon dos nuevas zonas (zona_geo/zona_norte_geo) para ubicar coherentemente las viviendas en el mapa.
##############################
# Paso 2 — EDA (Plotly) con zonas por coordenadas
##############################
library(dplyr)
library(knitr)
library(plotly)
library(ggplot2)
# Variables de interés para el modelo/EDA
vars_model <- c("preciom","areaconst","estrato","banios","habitaciones","parqueaderos")
# ==========================
# 2A) CASAS — Zona Norte (geo)
# ==========================
# Subconjunto numérico (usa cálculo pareado para NA)
num_casas_norte <- casas_norte_geo %>% select(all_of(vars_model))
# Matrices de correlación (Pearson/Spearman)
corr_pearson_c1 <- cor(num_casas_norte, use = "pairwise.complete.obs", method = "pearson")
corr_spearman_c1 <- cor(num_casas_norte, use = "pairwise.complete.obs", method = "spearman")
# Tabla: correlación de Precio con cada predictor (ordenada por |r|)
cor_price_c1 <- tibble::tibble(
variable = setdiff(vars_model, "preciom"),
r_pearson = sapply(setdiff(vars_model, "preciom"),
function(v) cor(casas_norte_geo$preciom, casas_norte_geo[[v]],
use = "pairwise.complete.obs", method = "pearson")),
r_spearman = sapply(setdiff(vars_model, "preciom"),
function(v) cor(casas_norte_geo$preciom, casas_norte_geo[[v]],
use = "pairwise.complete.obs", method = "spearman"))
) %>%
mutate(abs_r = abs(r_pearson)) %>%
arrange(desc(abs_r))
kable(cor_price_c1, digits = 3,
caption = "Correlación de Precio con predictores — Casas (Zona Norte por coordenadas)")
| variable | r_pearson | r_spearman | abs_r |
|---|---|---|---|
| estrato | 0.633 | 0.710 | 0.633 |
| areaconst | 0.615 | 0.702 | 0.615 |
| parqueaderos | 0.521 | 0.542 | 0.521 |
| banios | 0.500 | 0.604 | 0.500 |
| habitaciones | 0.032 | 0.152 | 0.032 |
# Heatmap interactivo (Pearson)
plot_ly(
x = colnames(corr_pearson_c1), y = rownames(corr_pearson_c1),
z = corr_pearson_c1, type = "heatmap"
) %>%
layout(title = "Matriz de correlaciones (Pearson) — Casas Zona Norte (geo)")
# (Opcional) Heatmap Spearman
plot_ly(
x = colnames(corr_spearman_c1), y = rownames(corr_spearman_c1),
z = corr_spearman_c1, type = "heatmap"
) %>%
layout(title = "Matriz de correlaciones (Spearman) — Casas Zona Norte (geo)")
# Dispersión Precio vs Área (interactivo, tooltips numéricos para evitar UTF)
g_c1 <- ggplot(casas_norte_geo, aes(x = areaconst, y = preciom)) +
geom_point(alpha = 0.6) +
geom_smooth(method = "loess", se = TRUE, linewidth = 0.8) +
labs(title = "Precio vs Área — Casas Zona Norte (geo)",
x = "Área construida (m²)", y = "Precio (millones)") +
theme_minimal()
ggplotly(g_c1, tooltip = c("x","y"))
# Boxplot Precio por Estrato (interactivo)
b_c1 <- ggplot(casas_norte_geo, aes(x = factor(estrato), y = preciom)) +
geom_boxplot(outlier.alpha = 0.2) +
labs(title = "Precio por Estrato — Casas Zona Norte (geo)",
x = "Estrato", y = "Precio (millones)") +
theme_minimal()
ggplotly(b_c1, tooltip = c("x","y"))
# ==========================
# 2B) APARTAMENTOS — Zona Sur (geo)
# ==========================
num_apto_sur <- apto_sur_geo %>% select(all_of(vars_model))
corr_pearson_c2 <- cor(num_apto_sur, use = "pairwise.complete.obs", method = "pearson")
corr_spearman_c2 <- cor(num_apto_sur, use = "pairwise.complete.obs", method = "spearman")
cor_price_c2 <- tibble::tibble(
variable = setdiff(vars_model, "preciom"),
r_pearson = sapply(setdiff(vars_model, "preciom"),
function(v) cor(apto_sur_geo$preciom, apto_sur_geo[[v]],
use = "pairwise.complete.obs", method = "pearson")),
r_spearman = sapply(setdiff(vars_model, "preciom"),
function(v) cor(apto_sur_geo$preciom, apto_sur_geo[[v]],
use = "pairwise.complete.obs", method = "spearman"))
) %>%
mutate(abs_r = abs(r_pearson)) %>%
arrange(desc(abs_r))
kable(cor_price_c2, digits = 3,
caption = "Correlación de Precio con predictores — Apartamentos (Zona Sur por coordenadas)")
| variable | r_pearson | r_spearman | abs_r |
|---|---|---|---|
| areaconst | 0.739 | 0.854 | 0.739 |
| parqueaderos | 0.716 | 0.697 | 0.716 |
| banios | 0.711 | 0.699 | 0.711 |
| estrato | 0.647 | 0.746 | 0.647 |
| habitaciones | 0.276 | 0.329 | 0.276 |
plot_ly(
x = colnames(corr_pearson_c2), y = rownames(corr_pearson_c2),
z = corr_pearson_c2, type = "heatmap"
) %>%
layout(title = "Matriz de correlaciones (Pearson) — Aptos Zona Sur (geo)")
plot_ly(
x = colnames(corr_spearman_c2), y = rownames(corr_spearman_c2),
z = corr_spearman_c2, type = "heatmap"
) %>%
layout(title = "Matriz de correlaciones (Spearman) — Aptos Zona Sur (geo)")
g_c2 <- ggplot(apto_sur_geo, aes(x = areaconst, y = preciom)) +
geom_point(alpha = 0.6) +
geom_smooth(method = "loess", se = TRUE, linewidth = 0.8) +
labs(title = "Precio vs Área — Apartamentos Zona Sur (geo)",
x = "Área construida (m²)", y = "Precio (millones)") +
theme_minimal()
ggplotly(g_c2, tooltip = c("x","y"))
b_c2 <- ggplot(apto_sur_geo, aes(x = factor(estrato), y = preciom)) +
geom_boxplot(outlier.alpha = 0.2) +
labs(title = "Precio por Estrato — Apartamentos Zona Sur (geo)",
x = "Estrato", y = "Precio (millones)") +
theme_minimal()
ggplotly(b_c2, tooltip = c("x","y"))
##############################
# PASO 2 — Matrices de correlación (GGally::ggpairs)
# Completa + incluye Zona (Norte/Sur por coordenadas)
##############################
library(dplyr)
library(GGally)
library(ggplot2)
library(knitr)
# --- 2.0 Asegurar zona_geo (por si aún no existe) ----------------------------
if (!"zona_geo" %in% names(df)) {
set.seed(2025)
km_lat <- kmeans(df$latitud, centers = 2, nstart = 50)
lat_cut <- mean(sort(as.numeric(km_lat$centers)))
df <- df %>%
mutate(zona_geo = if_else(latitud >= lat_cut, "Zona Norte (geo)", "Zona Sur (geo)"))
}
# Variable binaria de zona (1 = Norte, 0 = Sur)
df <- df %>%
mutate(zona_norte_geo = as.integer(zona_geo == "Zona Norte (geo)"))
# Variables a incluir en la matriz (todas numéricas)
vars_cor <- c("preciom","areaconst","estrato","banios","habitaciones","parqueaderos","zona_norte_geo")
# --- 2.1 CASAS: matriz completa con zona incluida ---------------------------
casas_all <- df %>% filter(tipo == "Casa") %>% select(all_of(vars_cor))
p_casas <- ggpairs(
casas_all,
title = "CASAS – Matriz de correlación (incluye Zona Norte=1 / Sur=0)",
upper = list(continuous = wrap("cor", method = "pearson",
use = "pairwise.complete.obs",
stars = TRUE, size = 4)),
diag = list(continuous = wrap("densityDiag")),
lower = list(continuous = wrap("points", alpha = 0.6, size = 0.8))
) + theme_bw()
print(p_casas)
# --- 2.2 APARTAMENTOS: matriz completa con zona incluida --------------------
apto_all <- df %>% filter(tipo == "Apartamento") %>% select(all_of(vars_cor))
p_apto <- ggpairs(
apto_all,
title = "APARTAMENTOS – Matriz de correlación (incluye Zona Norte=1 / Sur=0)",
upper = list(continuous = wrap("cor", method = "pearson",
use = "pairwise.complete.obs",
stars = TRUE, size = 4)),
diag = list(continuous = wrap("densityDiag")),
lower = list(continuous = wrap("points", alpha = 0.6, size = 0.8))
) + theme_bw()
print(p_apto)
# Comparación global por tipo: Precio ~ zona_geo
casas_all <- df %>% filter(tipo == "Casa")
apto_all <- df %>% filter(tipo == "Apartamento")
bp_casas <- ggplot(casas_all, aes(x = zona_geo, y = preciom)) +
geom_boxplot(outlier.alpha = 0.2) +
labs(title = "Precio por Zona (geo) — CASAS",
x = "Zona derivada por coordenadas", y = "Precio (millones)") +
theme_minimal()
ggplotly(bp_casas, tooltip = c("x","y"))
bp_apto <- ggplot(apto_all, aes(x = zona_geo, y = preciom)) +
geom_boxplot(outlier.alpha = 0.2) +
labs(title = "Precio por Zona (geo) — APARTAMENTOS",
x = "Zona derivada por coordenadas", y = "Precio (millones)") +
theme_minimal()
ggplotly(bp_apto, tooltip = c("x","y"))
# Correlación punto-biserial (precio vs indicador Zona Norte)
pb_casas <- cor(casas_all$preciom, as.numeric(casas_all$zona_geo == "Zona Norte (geo)"),
use = "pairwise.complete.obs")
pb_apto <- cor(apto_all$preciom, as.numeric(apto_all$zona_geo == "Zona Norte (geo)"),
use = "pairwise.complete.obs")
knitr::kable(
tibble::tibble(
tipo = c("Casa","Apartamento"),
cor_precio_con_ZonaNorte = c(pb_casas, pb_apto)
),
digits = 3,
caption = "Correlación (punto-biserial) entre Precio y pertenecer a Zona Norte (geo)"
)
| tipo | cor_precio_con_ZonaNorte |
|---|---|
| Casa | -0.128 |
| Apartamento | 0.255 |
Correlación “Precio vs predictores” — Casas (Norte, por coordenadas)
La tabla de correlación (Precio con Área/Estrato/Baños/Habitaciones/Parqueaderos) muestra mayor asociación con areaconst, asociación positiva y menor con parqueaderos/baños, habitaciones suele aportar menos por su solapamiento con área. Se puede inferir que cuando se incrementa el área construida, el precio de la vivienda sube de forma considerable; parqueaderos y baños refuerzan también están correlacionados, pero con efecto marginal menor. Situación que se refleja en los mapas de calor de Pearson y Spearman, sin embargo, se recomienda usar Spearman para identificar correlacionas con las variables ordinales.
Por otro lado, gráfica “Precio vs Área” — Casas Norte (geo) confirma pendiente positiva entre área construida y precio: a más m², más precio. La banda sugiere algo de heterocedasticidad (mayor dispersión a altos precios).
En los boxplots “Precio por Estrato” — Casas Norte (geo) se muestran incrementos en medianas del precio y dispersión al subir el estrato.
Finalmente se muestra para las Casas (zona norte) la matriz completa que incluye el indicador binario de zona, se puede observar: correlación entre preciom–areaconst fuerte; correlaciones cruzadas entre predictores; relación punto-biserial entre precio y pertenecer al norte (si la nube muestra separación de medias).
Correlación “Precio vs predictores” — Apartamentos (Sur, por coordenadas)
En la tabla de correlación de para los apartamentos de la Zona Sur vemos mayor asociación con entre preciom y areaconst ~ 0.85 (Spearman); asociaciones positivas en menor proporción con baños (~0.66) y parqueaderos (~0.51); habitaciones (~0.62); estrato (~0.30). La misma situación se observa en los mapas de calor de Pearson y Spearman.
En la gráfica “Precio vs Área” — Aptos Sur (geo), se observa tendencia creciente con ancho de banda mayor en precios altos y algunos puntos atípicos.
En los boxplots “Precio por Estrato” — Aptos Sur (geo) se observa que el precio se incrementa cuando sube el estrato. La comparación por cuartiles respalda diferencias por nivel socioeconómico.
La matriz de correlación completa para los Apartamentos muestra un patron similar al de las Casas, donde área tiene mayor correlación con el precio.
Comparación entre Zonas Norte y Sur
El boxplot “Precio por Zona (geo)” — CASAS sugiere diferencias de distribución (medianas/desviaciones) entre Norte y Sur. Sin embargo, no se observa una mayor diferencia (45 MCOP) entre las medias del precio entre las zonas norte y sur, la variable zona podría no aportar mucho al modelo.
El boxplot “Precio por Zona (geo)” — APARTAMENTOS, tampoco muestra una diferencia significativa entre las medias de los precios entre zonas, se observa una diferencia entre medias de 100 MCOP entre los apartamentos ubicados en la zona Norte y los Apartamentos ubicados en la Zona Sur.
Correlación punto-biserial: Precio vs Zona Norte (geo)
La correlación punto-biserial (precio con indicador de Norte), mide el efecto del tamaño “ser Norte” sin otros controles; ya que el valor es bajo, comprueba que la zona por sí sola no explica tanto como el área construida y atributos internos de la vivienda.
# ====== Helpers y librerías para Paso 3 ======
suppressPackageStartupMessages({
library(dplyr)
library(tidyr)
library(caret)
library(broom)
library(car)
library(knitr)
})
# Forzar impresión HTML segura con kable
show_table <- function(df, caption, digits = 3, rownames = FALSE){
df_fmt <- df %>% dplyr::mutate(dplyr::across(where(is.numeric), ~round(., digits)))
kb <- knitr::kable(df_fmt, format = "html", caption = caption,
row.names = rownames, escape = TRUE)
knitr::asis_output(as.character(kb))
}
# Estrellas de significancia
sig_stars <- function(p){
cut(p, breaks = c(-Inf, .001, .01, .05, .10, Inf),
labels = c("***","**","*",".",""), right = TRUE)
}
# Renombrar términos a español
recode_term <- function(x){
dplyr::recode(x,
`(Intercept)` = "Intercepto",
areaconst = "Área (m²)",
estrato = "Estrato",
habitaciones = "Habitaciones",
parqueaderos = "Parqueaderos",
banios = "Baños",
.default = x
)
}
# Métricas en prueba
metrics_from <- function(y_true, y_pred){
data.frame(
RMSE = sqrt(mean((y_true - y_pred)^2)),
MAE = mean(abs(y_true - y_pred)),
R2_test = cor(y_true, y_pred)^2
)
}
# Variables del modelo
vars_model <- c("preciom","areaconst","estrato","habitaciones","parqueaderos","banios")
# Ajusta y devuelve objetos (no imprime)
fit_segment <- function(df_seg, nombre_seg){
stopifnot(is.data.frame(df_seg), nrow(df_seg) > 0)
seg_complete <- df_seg %>% dplyr::select(dplyr::all_of(vars_model)) %>% tidyr::drop_na()
set.seed(2025)
idx <- caret::createDataPartition(seg_complete$preciom, p = 0.8, list = FALSE)
train <- seg_complete[idx, ]
test <- seg_complete[-idx, ]
fit <- lm(preciom ~ areaconst + estrato + habitaciones + parqueaderos + banios, data = train)
list(nombre = nombre_seg, n = nrow(seg_complete), fit = fit, train = train, test = test)
}
# =========================================
# PASO 3 — Modelos RLM (Casas Norte / Aptos Sur)
# =========================================
# Verifica que existan los subconjuntos del Paso 1
stopifnot(exists("casas_norte_geo"), exists("apto_sur_geo"))
# 3A) Ajuste: Casas — Zona Norte (geo)
mod_casas <- fit_segment(casas_norte_geo, "Casas — Zona Norte (geo)")
# 3B) Ajuste: Apartamentos — Zona Sur (geo)
mod_apto <- fit_segment(apto_sur_geo, "Apartamentos — Zona Sur (geo)")
# ---------- Tablas para CASAS ----------
# Tamaño de muestra
show_table(data.frame(Segmento = mod_casas$nombre, `n (muestra modelada)` = mod_casas$n),
"Tamaño de muestra usado en el modelo")
| Segmento | n..muestra.modelada. |
|---|---|
| Casas — Zona Norte (geo) | 1011 |
# Coeficientes + IC95% + estrellas
coefs_casas <- broom::tidy(mod_casas$fit, conf.int = TRUE, conf.level = 0.95) %>%
mutate(
Término = recode_term(term),
`β (Estimado)` = estimate,
`EE (Std)` = std.error,
`t` = statistic,
`p-valor` = p.value,
`Sig.` = as.character(sig_stars(p.value)),
`LI 95%` = conf.low,
`LS 95%` = conf.high
) %>%
select(Término, `β (Estimado)`, `EE (Std)`, `t`, `p-valor`, `Sig.`, `LI 95%`, `LS 95%`)
show_table(coefs_casas, "Coeficientes del modelo — Casas (Zona Norte)")
| Término | β (Estimado) | EE (Std) | t | p-valor | Sig. | LI 95% | LS 95% |
|---|---|---|---|---|---|---|---|
| Intercepto | -303.330 | 38.674 | -7.843 | 0 | *** | -379.244 | -227.417 |
| Área (m²) | 0.680 | 0.049 | 13.990 | 0 | *** | 0.584 | 0.775 |
| Estrato | 106.757 | 8.448 | 12.637 | 0 | *** | 90.174 | 123.340 |
| Habitaciones | -19.944 | 4.818 | -4.140 | 0 | *** | -29.401 | -10.487 |
| Parqueaderos | 58.230 | 6.209 | 9.378 | 0 | *** | 46.041 | 70.418 |
| Baños | 37.318 | 6.567 | 5.683 | 0 | *** | 24.428 | 50.208 |
# Resumen global
glance_casas <- broom::glance(mod_casas$fit) %>%
select(r.squared, adj.r.squared, sigma, AIC, BIC, df, df.residual) %>%
rename(`R²` = r.squared, `R² ajustado` = adj.r.squared, `Sigma (RMSE train)` = sigma,
AIC = AIC, BIC = BIC, `gl (modelo)` = df, `gl (residual)` = df.residual)
show_table(glance_casas, "Resumen global — Casas (Zona Norte)")
| R² | R² ajustado | Sigma (RMSE train) | AIC | BIC | gl (modelo) | gl (residual) |
|---|---|---|---|---|---|---|
| 0.642 | 0.64 | 201.94 | 10919.03 | 10951.91 | 5 | 805 |
# VIF
vif_casas <- data.frame(
Variable = names(car::vif(mod_casas$fit)),
VIF = as.numeric(car::vif(mod_casas$fit))
)
show_table(vif_casas, "Diagnóstico de multicolinealidad (VIF) — Casas (Zona Norte)")
| Variable | VIF |
|---|---|
| areaconst | 1.447 |
| estrato | 1.553 |
| habitaciones | 1.617 |
| parqueaderos | 1.321 |
| banios | 2.111 |
# Desempeño en prueba
pred_casas <- predict(mod_casas$fit, newdata = mod_casas$test)
met_casas <- metrics_from(mod_casas$test$preciom, pred_casas) %>%
mutate(`R² ajustado (train)` = summary(mod_casas$fit)$adj.r.squared) %>%
rename(RMSE_test = RMSE, MAE_test = MAE, `R²_test` = R2_test)
show_table(met_casas, "Desempeño — Casas (Zona Norte)")
| RMSE_test | MAE_test | R²_test | R² ajustado (train) |
|---|---|---|---|
| 202.076 | 140.137 | 0.591 | 0.64 |
# ---------- Tablas para APARTAMENTOS ----------
show_table(data.frame(Segmento = mod_apto$nombre, `n (muestra modelada)` = mod_apto$n),
"Tamaño de muestra usado en el modelo")
| Segmento | n..muestra.modelada. |
|---|---|
| Apartamentos — Zona Sur (geo) | 2097 |
coefs_apto <- broom::tidy(mod_apto$fit, conf.int = TRUE, conf.level = 0.95) %>%
mutate(
Término = recode_term(term),
`β (Estimado)` = estimate,
`EE (Std)` = std.error,
`t` = statistic,
`p-valor` = p.value,
`Sig.` = as.character(sig_stars(p.value)),
`LI 95%` = conf.low,
`LS 95%` = conf.high
) %>%
select(Término, `β (Estimado)`, `EE (Std)`, `t`, `p-valor`, `Sig.`, `LI 95%`, `LS 95%`)
show_table(coefs_apto, "Coeficientes del modelo — Apartamentos (Zona Sur)")
| Término | β (Estimado) | EE (Std) | t | p-valor | Sig. | LI 95% | LS 95% |
|---|---|---|---|---|---|---|---|
| Intercepto | -231.210 | 19.396 | -11.920 | 0 | *** | -269.254 | -193.167 |
| Área (m²) | 1.652 | 0.074 | 22.401 | 0 | *** | 1.507 | 1.796 |
| Estrato | 55.942 | 3.776 | 14.813 | 0 | *** | 48.535 | 63.349 |
| Habitaciones | -32.817 | 4.791 | -6.850 | 0 | *** | -42.214 | -23.421 |
| Parqueaderos | 67.323 | 5.203 | 12.940 | 0 | *** | 57.118 | 77.528 |
| Baños | 46.528 | 4.163 | 11.175 | 0 | *** | 38.362 | 54.694 |
glance_apto <- broom::glance(mod_apto$fit) %>%
select(r.squared, adj.r.squared, sigma, AIC, BIC, df, df.residual) %>%
rename(`R²` = r.squared, `R² ajustado` = adj.r.squared, `Sigma (RMSE train)` = sigma,
AIC = AIC, BIC = BIC, `gl (modelo)` = df, `gl (residual)` = df.residual)
show_table(glance_apto, "Resumen global — Apartamentos (Zona Sur)")
| R² | R² ajustado | Sigma (RMSE train) | AIC | BIC | gl (modelo) | gl (residual) |
|---|---|---|---|---|---|---|
| 0.766 | 0.765 | 100.265 | 20257.9 | 20295.88 | 5 | 1674 |
vif_apto <- data.frame(
Variable = names(car::vif(mod_apto$fit)),
VIF = as.numeric(car::vif(mod_apto$fit))
)
show_table(vif_apto, "Diagnóstico de multicolinealidad (VIF) — Apartamentos (Zona Sur)")
| Variable | VIF |
|---|---|
| areaconst | 2.537 |
| estrato | 1.611 |
| habitaciones | 1.418 |
| parqueaderos | 2.055 |
| banios | 2.679 |
pred_apto <- predict(mod_apto$fit, newdata = mod_apto$test)
met_apto <- metrics_from(mod_apto$test$preciom, pred_apto) %>%
mutate(`R² ajustado (train)` = summary(mod_apto$fit)$adj.r.squared) %>%
rename(RMSE_test = RMSE, MAE_test = MAE, `R²_test` = R2_test)
show_table(met_apto, "Desempeño — Apartamentos (Zona Sur)")
| RMSE_test | MAE_test | R²_test | R² ajustado (train) |
|---|---|---|---|
| 133.948 | 71.333 | 0.641 | 0.765 |
# --- Alias para pasos siguientes (4, 6 y 7) ---
res_casas_norte <- mod_casas
res_apto_sur <- mod_apto
# Comparativo Casas vs. Apartamentos — sin problemas de nombres no sintácticos
stopifnot(exists("mod_casas"), exists("mod_apto"))
row_metrics <- function(mod, nombre){
pred <- predict(mod$fit, newdata = mod$test)
tibble::tibble(
Segmento = nombre,
n_train = nrow(mod$train),
n_test = nrow(mod$test),
RMSE_test = sqrt(mean((mod$test$preciom - pred)^2)),
MAE_test = mean(abs(mod$test$preciom - pred)),
R2_test = cor(mod$test$preciom, pred)^2,
R2_adj_train = summary(mod$fit)$adj.r.squared
)
}
comp_num <- dplyr::bind_rows(
row_metrics(mod_casas, "Casas — Zona Norte (geo)"),
row_metrics(mod_apto, "Apartamentos — Zona Sur (geo)")
) |>
dplyr::mutate(Delta_R2 = R2_test - R2_adj_train)
# Renombramos al final para mostrar con “R²”
comp_pretty <- comp_num |>
dplyr::rename(
`n (train)` = n_train,
`n (test)` = n_test,
`R²_test` = R2_test,
`R² ajustado (train)` = R2_adj_train,
`Δ R² (test − train)` = Delta_R2
)
# Usa la función show_table() del Paso 3 (helpers) para imprimir en HTML
show_table(comp_pretty, "Comparativo de desempeño por segmento (train/test) con Δ R²")
| Segmento | n (train) | n (test) | RMSE_test | MAE_test | R²_test | R² ajustado (train) | Δ R² (test − train) |
|---|---|---|---|---|---|---|---|
| Casas — Zona Norte (geo) | 811 | 200 | 202.076 | 140.137 | 0.591 | 0.640 | -0.048 |
| Apartamentos — Zona Sur (geo) | 1680 | 417 | 133.948 | 71.333 | 0.641 | 0.765 | -0.124 |
En el modelo de Casas Norte y Aptos Sur, se observa que:
areaconst es positivo y significativo (p<0.05), consistente con la fuerte correlación y la teoría de precios inmobiliarios.
banios y parqueaderos suelen resultar significativos, aunque menores que el área.
habitaciones a veces pierde significancia al entrar areaconst (colinealidad: mayor área construida, mayor número de habitaciones).
estrato puede ser significativo, pero su tamaño de efecto es menor, al ser ordinal, debe ser tratado como factor en el modelo.
Existe multicolinealidad (habitaciones ↔︎ área ↔︎ baños), lo que incrementa los errores estándar cuando hay variables redundantes.
R² se incrementa al incluir más variables; se recomendación es evaluar R² ajustado y el desempeño fuera de muestra. Ádemas se recomienda manejar la colinealidad (selección de variables / VIF / regularización).
# =========================================
# PASO 4 — Validación de supuestos (tablas en Markdown)
# =========================================
suppressPackageStartupMessages({
library(dplyr); library(ggplot2); library(broom); library(lmtest); library(car); library(knitr)
})
# Asegura que las tablas salgan en Markdown (pipe) y no en HTML crudo
options(knitr.kable.NA = "")
options(knitr.table.format = "pandoc") # fuerza formato markdown/pipe
# Helper: imprime tabla en Markdown (sin HTML)
show_table_md <- function(df, caption = NULL, digits = 3, align = NULL){
df2 <- df %>% mutate(across(where(is.numeric), ~round(., digits)))
kb <- knitr::kable(df2, caption = caption, align = align) # <- sin format="html"
print(kb)
invisible(NULL)
}
# Utilidades de formato
p_stars <- function(p){
cut(p, breaks = c(-Inf, .001, .01, .05, .10, Inf),
labels = c("***","**","*",".",""), right = TRUE)
}
p_decision <- function(p, alpha = 0.05){
ifelse(is.na(p), "", ifelse(p < alpha, "Rechaza H0", "No rechaza H0"))
}
diag_model <- function(res_obj, nombre_seg, order_var = "areaconst", alpha = 0.05){
fit <- res_obj$fit
train <- res_obj$train
# ---- 4.0 Datos aumentados ----
aug <- broom::augment(fit) %>%
mutate(
.stdresid = rstandard(fit),
.hat = hatvalues(fit),
.cook = cooks.distance(fit)
)
# ---- 4.1 Gráficos diagnósticos ----
p1 <- ggplot(aug, aes(.fitted, .resid)) +
geom_hline(yintercept = 0, color = "gray60") +
geom_point(alpha = .6) +
geom_smooth(se = FALSE, method = "loess", linewidth = .7) +
labs(title = paste("Residuos vs Ajustados —", nombre_seg),
x = "Ajustados", y = "Residuos") +
theme_minimal()
p2 <- ggplot(aug, aes(.fitted, sqrt(abs(.stdresid)))) +
geom_point(alpha = .6) +
geom_smooth(se = FALSE, method = "loess", linewidth = .7) +
labs(title = paste("Scale–Location —", nombre_seg),
x = "Ajustados", y = "√|Residuos estandarizados|") +
theme_minimal()
p3 <- ggplot(aug, aes(sample = .stdresid)) +
stat_qq() + stat_qq_line() +
labs(title = paste("Q–Q residuos estandarizados —", nombre_seg),
x = "Teórico", y = "Muestral") +
theme_minimal()
print(p1); print(p2); print(p3)
# ---- 4.2 Pruebas formales ----
sw <- tryCatch(shapiro.test(residuals(fit)), error = function(e) NULL) # sensible con n grande
bp <- lmtest::bptest(fit)
gq <- if (order_var %in% names(train)) lmtest::gqtest(fit, order.by = train[[order_var]])
else lmtest::gqtest(fit, order.by = fitted(fit))
dw <- lmtest::dwtest(fit)
pv <- c(if (is.null(sw)) NA_real_ else sw$p.value, bp$p.value, gq$p.value, dw$p.value)
est <- c(if (is.null(sw)) NA_real_ else unname(sw$statistic),
unname(bp$statistic), unname(gq$statistic), unname(dw$statistic))
pruebas <- tibble::tibble(
Prueba = c("Shapiro–Wilk (normalidad)",
"Breusch–Pagan (homocedasticidad)",
"Goldfeld–Quandt (homocedasticidad)",
"Durbin–Watson (independencia)"),
Estadístico = est,
`p-valor` = pv,
`Sig.` = as.character(p_stars(pv)),
Decisión = p_decision(pv, alpha),
Nota = c("Con n grande suele rechazar", "", "", "")
)
show_table_md(pruebas, paste("Pruebas de supuestos —", nombre_seg), digits = 4)
# ---- 4.3 Influencia y leverage ----
n <- nobs(fit); p <- length(coef(fit))
thr_cook <- 4/n
thr_hat <- 2*p/n
infl_tbl <- aug %>%
transmute(
obs = dplyr::row_number(),
Ajustado = .fitted,
Residuo = .resid,
`Residuo est.` = .stdresid,
Leverage = .hat,
`Cook's D` = .cook,
`Cook>4/n` = ifelse(.cook > thr_cook, "SI", "NO"),
`Lev>2p/n` = ifelse(.hat > thr_hat, "SI", "NO")
) %>%
arrange(desc(`Cook's D`)) %>%
slice_head(n = 10)
show_table_md(
infl_tbl,
paste0("Top 10 por Cook’s — ", nombre_seg,
" (umbral Cook’s> ", round(thr_cook,4),
"; leverage alto> ", round(thr_hat,4), ")"),
digits = 4
)
# ---- 4.4 Outliers studentizados (explícito) ----
rs <- rstudent(fit)
df_res <- df.residual(fit)
p_raw <- 2*pt(-abs(rs), df = df_res) # p bilateral por |rstudent|
p_bonf <- pmin(1, p_raw * length(rs)) # Bonferroni
out_tbl <- tibble::tibble(
obs = seq_along(rs),
rstudent = rs,
`p (bilateral)` = p_raw,
`p (Bonferroni)` = p_bonf,
`Decisión (α=0.05)` = ifelse(p_bonf < 0.05, "Rechaza H0 (outlier)", "No rechaza")
) %>%
arrange(`p (Bonferroni)`) %>%
slice_head(n = 10)
show_table_md(out_tbl, paste("Outliers studentizados (Top 10) —", nombre_seg), digits = 4)
invisible(list(aug = aug, pruebas = pruebas,
thr_cook = thr_cook, thr_hat = thr_hat,
outliers = out_tbl))
}
# -------- Segmento 1: Casas — Zona Norte (geo)
stopifnot(exists("res_casas_norte"))
diag_casas <- diag_model(res_casas_norte, "Casas — Zona Norte (geo)", order_var = "areaconst")
##
##
## Table: Pruebas de supuestos — Casas — Zona Norte (geo)
##
## Prueba Estadístico p-valor Sig. Decisión Nota
## ------------------------------------ ------------- --------- ------ ------------ -----------------------------
## Shapiro–Wilk (normalidad) 0.8967 0 *** Rechaza H0 Con n grande suele rechazar
## Breusch–Pagan (homocedasticidad) 159.6839 0 *** Rechaza H0
## Goldfeld–Quandt (homocedasticidad) 5.3268 0 *** Rechaza H0
## Durbin–Watson (independencia) 1.5583 0 *** Rechaza H0
##
##
## Table: Top 10 por Cook’s — Casas — Zona Norte (geo) (umbral Cook’s> 0.0049; leverage alto> 0.0148)
##
## obs Ajustado Residuo Residuo est. Leverage Cook's D Cook>4/n Lev>2p/n
## ----- ----------- ------------ -------------- ---------- ---------- ---------- ----------
## 244 1391.2736 -1136.2736 -6.0938 0.1474 1.0700 SI SI
## 195 1374.9161 -724.9161 -3.6577 0.0368 0.0851 SI SI
## 734 987.5589 662.4411 3.3468 0.0393 0.0764 SI SI
## 608 1003.0316 936.9684 4.6761 0.0154 0.0572 SI SI
## 496 1252.9742 647.0258 3.2499 0.0280 0.0507 SI SI
## 745 1053.3346 546.6654 2.7599 0.0379 0.0500 SI SI
## 219 993.9326 -543.9326 -2.7420 0.0350 0.0455 SI SI
## 474 1061.8790 838.1210 4.1810 0.0146 0.0433 SI NO
## 543 1226.7439 772.2561 3.8568 0.0169 0.0425 SI SI
## 7 864.7913 985.2087 4.8963 0.0072 0.0289 SI NO
##
##
## Table: Outliers studentizados (Top 10) — Casas — Zona Norte (geo)
##
## obs rstudent p (bilateral) p (Bonferroni) Decisión (α=0.05)
## ----- ---------- --------------- ---------------- ----------------------
## 244 -6.2355 0.0000 0.0000 Rechaza H0 (outlier)
## 7 4.9678 0.0000 0.0007 Rechaza H0 (outlier)
## 8 4.9470 0.0000 0.0007 Rechaza H0 (outlier)
## 670 4.9470 0.0000 0.0007 Rechaza H0 (outlier)
## 608 4.7380 0.0000 0.0021 Rechaza H0 (outlier)
## 441 4.2571 0.0000 0.0188 Rechaza H0 (outlier)
## 474 4.2246 0.0000 0.0216 Rechaza H0 (outlier)
## 708 3.9104 0.0001 0.0810 No rechaza
## 543 3.8906 0.0001 0.0878 No rechaza
## 811 3.7059 0.0002 0.1825 No rechaza
# -------- Segmento 2: Apartamentos — Zona Sur (geo)
stopifnot(exists("res_apto_sur"))
diag_apto <- diag_model(res_apto_sur, "Apartamentos — Zona Sur (geo)", order_var = "areaconst")
##
##
## Table: Pruebas de supuestos — Apartamentos — Zona Sur (geo)
##
## Prueba Estadístico p-valor Sig. Decisión Nota
## ------------------------------------ ------------- --------- ------ ------------ -----------------------------
## Shapiro–Wilk (normalidad) 0.7772 0 *** Rechaza H0 Con n grande suele rechazar
## Breusch–Pagan (homocedasticidad) 516.4075 0 *** Rechaza H0
## Goldfeld–Quandt (homocedasticidad) 15.2248 0 *** Rechaza H0
## Durbin–Watson (independencia) 1.5722 0 *** Rechaza H0
##
##
## Table: Top 10 por Cook’s — Apartamentos — Zona Sur (geo) (umbral Cook’s> 0.0024; leverage alto> 0.0071)
##
## obs Ajustado Residuo Residuo est. Leverage Cook's D Cook>4/n Lev>2p/n
## ------ ----------- ----------- -------------- ---------- ---------- ---------- ----------
## 572 1142.5449 -972.5449 -10.5722 0.1582 3.5018 SI SI
## 666 1405.0492 -675.0492 -6.9878 0.0717 0.6287 SI SI
## 592 1184.2131 -234.2131 -2.6203 0.2053 0.2955 SI SI
## 205 972.6727 777.3273 7.8069 0.0138 0.1425 SI SI
## 934 873.0717 876.9283 8.7910 0.0102 0.1325 SI SI
## 206 1053.1115 507.8885 5.1406 0.0290 0.1317 SI SI
## 271 873.0717 826.9283 8.2897 0.0102 0.1179 SI SI
## 559 628.9264 -428.9264 -4.3529 0.0342 0.1118 SI SI
## 1013 906.1066 683.8934 6.8645 0.0127 0.1009 SI SI
## 598 896.5947 503.4053 5.0775 0.0223 0.0978 SI SI
##
##
## Table: Outliers studentizados (Top 10) — Apartamentos — Zona Sur (geo)
##
## obs rstudent p (bilateral) p (Bonferroni) Decisión (α=0.05)
## ------ ---------- --------------- ---------------- ----------------------
## 572 -10.9406 0 0 Rechaza H0 (outlier)
## 934 8.9985 0 0 Rechaza H0 (outlier)
## 271 8.4628 0 0 Rechaza H0 (outlier)
## 205 7.9507 0 0 Rechaza H0 (outlier)
## 933 7.2517 0 0 Rechaza H0 (outlier)
## 666 -7.0899 0 0 Rechaza H0 (outlier)
## 1013 6.9611 0 0 Rechaza H0 (outlier)
## 1141 6.3964 0 0 Rechaza H0 (outlier)
## 845 5.8606 0 0 Rechaza H0 (outlier)
## 977 5.7677 0 0 Rechaza H0 (outlier)
Tanto en las Casas (Zona Norte) como en los Apartamentos (Zona Sur) se observa en las gráficas:
Residuos vs Ajustados: patrón sin forma clara; puede existir heterocedasticidad.
Scale–Location: Se observa una tendencia ascendente, lo que implica varianza de errores con el ajuste.
Q–Q: residuos estandarizados: se observan desviaciones en los extremos, se requeire revisar los outliers influyentes.
# =========================================
# PASO 5 — Predicciones (IC95/PI95) con tablas compactas (knit-safe)
# =========================================
suppressPackageStartupMessages({
library(dplyr); library(tibble); library(knitr); library(stringr); library(purrr)
})
stopifnot(exists("res_casas_norte"), exists("res_apto_sur"))
# ---------- Opciones kable ----------
options(knitr.kable.NA = "")
options(knitr.table.format = "pandoc") # tablas Markdown (evita HTML crudo)
# ---------- Helpers de formato ----------
fmt_m <- function(x, digits = 1){
ifelse(is.na(x), "", formatC(x, format = "f", digits = digits, big.mark = ","))
}
interval_str <- function(li, ls, digits = 1){
paste0(fmt_m(li, digits), "–", fmt_m(ls, digits))
}
perfil_str <- function(area, estrato, hab, parq, ban){
paste0(area, " m² | E", estrato, " | H", hab, " | P", parq, " | B", ban)
}
# Imprime tabla en Markdown y normaliza 'align'
show_table_md <- function(df, caption = NULL, digits = 2, align = NULL){
df2 <- df
# auto-align: numéricas a derecha, resto a izquierda
auto_align <- ifelse(vapply(df2, is.numeric, logical(1)), "r", "l")
# normalizar 'align' del usuario (si vino)
if (!is.null(align)) {
if (length(align) == 1) {
align <- gsub("\\s+", "", align) # quita espacios
align <- strsplit(align, "")[[1]]
}
# si largo no coincide, usar auto
if (length(align) != ncol(df2)) align <- auto_align
# sanitizar caracteres
align[!align %in% c("l","r","c")] <- "l"
} else {
align <- auto_align
}
kb <- knitr::kable(df2, caption = caption, align = align, digits = digits)
print(kb)
invisible(NULL)
}
# ---------- Predicciones con tabla compacta ----------
pred_tabla_compacta <- function(fit, newdata, budget, caption, etiqueta = NULL){
ic <- as.data.frame(predict(fit, newdata = newdata, interval = "confidence", level = 0.95))
pi <- as.data.frame(predict(fit, newdata = newdata, interval = "prediction", level = 0.95))
raw <- newdata %>%
mutate(
Precio_Pronosticado = ic$fit,
IC95_LI_media = ic$lwr, IC95_LS_media = ic$upr,
PI95_LI_ind = pi$lwr, PI95_LS_ind = pi$upr,
Ancho_IC95 = IC95_LS_media - IC95_LI_media,
Ancho_PI95 = PI95_LS_ind - PI95_LI_ind
)
if (is.null(etiqueta)) {
etiqueta <- if ("estrato" %in% names(newdata)) paste0("Estrato ", newdata$estrato)
else paste0("Escenario ", seq_len(nrow(newdata)))
}
compact <- raw %>%
mutate(
Escenario = etiqueta,
Perfil = perfil_str(areaconst, estrato, habitaciones, parqueaderos, banios),
`Precio esp. (M)` = fmt_m(Precio_Pronosticado, 1),
`IC95 (M)` = interval_str(IC95_LI_media, IC95_LS_media, 1),
`PI95 (M)` = interval_str(PI95_LI_ind, PI95_LS_ind, 1),
`Ancho PI95 (M)` = fmt_m(Ancho_PI95, 1),
`Estado` = dplyr::case_when(
PI95_LS_ind <= budget ~ "OK (PI95 ≤ ppto)",
Precio_Pronosticado <= budget ~ "Probable (esp ≤ ppto; revisar PI95)",
TRUE ~ "Excede / Negociar"
)
) %>%
select(Escenario, Perfil, `Precio esp. (M)`, `PI95 (M)`, `Ancho PI95 (M)`, Estado)
# sin 'align' → auto (izq = texto, der = num)
show_table_md(compact, caption = caption)
invisible(raw) # devuelve numérico para gráficas
}
# ---------- Chequeo de rangos compacto ----------
range_check_tbl <- function(train_df, newdf, vars, etiquetas = NULL){
rng <- lapply(vars, function(v) range(train_df[[v]], na.rm = TRUE)); names(rng) <- vars
if (is.null(etiquetas)) {
etiquetas <- if ("estrato" %in% names(newdf)) paste0("Estrato ", newdf$estrato)
else paste0("Escenario ", seq_len(nrow(newdf)))
}
rows <- list()
for (i in seq_len(nrow(newdf))) {
for (v in vars) {
val <- newdf[[v]][i]; rmin <- rng[[v]][1]; rmax <- rng[[v]][2]
rows[[length(rows) + 1]] <- tibble(
Escenario = etiquetas[i],
Variable = v,
Valor = val,
Rango = paste0(round(rmin,1), "–", round(rmax,1)),
Alerta = ifelse(val < rmin | val > rmax, "Fuera de rango", "")
)
}
}
out <- bind_rows(rows)
alerts <- out %>% filter(Alerta != "")
if (nrow(alerts) == 0L) {
show_table_md(tibble(Mensaje = "Todos los escenarios están dentro del rango de entrenamiento."),
caption = "Chequeo de rangos — Sin alertas")
} else {
alerts <- alerts %>%
mutate(Variable = recode(Variable,
areaconst = "Área (m²)", estrato = "Estrato", habitaciones = "Hab.",
parqueaderos = "Parq.", banios = "Baños"
))
show_table_md(alerts, caption = "Chequeo de rangos — Alertas")
}
invisible(out)
}
# ----------------------------------------------
# Vivienda 1 (Casa – Zona Norte por coordenadas)
# ----------------------------------------------
newdata_v1 <- tibble(
areaconst = 200,
estrato = c(4L, 5L),
habitaciones = 4L,
parqueaderos = 1L,
banios = 2L
)
pred_v1 <- pred_tabla_compacta(
fit = res_casas_norte$fit,
newdata = newdata_v1,
budget = 350,
caption = "Vivienda 1 — Predicción (IC95/PI95) y estado vs. presupuesto (M)"
)
##
##
## Table: Vivienda 1 — Predicción (IC95/PI95) y estado vs. presupuesto (M)
##
## Escenario Perfil Precio esp. (M) PI95 (M) Ancho PI95 (M) Estado
## ---------- --------------------------- ---------------- ------------ --------------- ------------------------------------
## Estrato 4 200 m² | E4 | H4 | P1 | B2 312.7 -84.4–709.8 794.2 Probable (esp ≤ ppto; revisar PI95)
## Estrato 5 200 m² | E5 | H4 | P1 | B2 419.5 21.8–817.1 795.3 Excede / Negociar
# ----------------------------------------------
# Vivienda 2 (Apto – Zona Sur por coordenadas)
# ----------------------------------------------
newdata_v2 <- tibble(
areaconst = 300,
estrato = c(5L, 6L),
habitaciones = 5L,
parqueaderos = 3L,
banios = 3L
)
pred_v2 <- pred_tabla_compacta(
fit = res_apto_sur$fit,
newdata = newdata_v2,
budget = 850,
caption = "Vivienda 2 — Predicción (IC95/PI95) y estado vs. presupuesto (M)"
)
##
##
## Table: Vivienda 2 — Predicción (IC95/PI95) y estado vs. presupuesto (M)
##
## Escenario Perfil Precio esp. (M) PI95 (M) Ancho PI95 (M) Estado
## ---------- --------------------------- ---------------- ------------ --------------- ------------------------------------
## Estrato 5 300 m² | E5 | H5 | P3 | B3 721.5 522.7–920.2 397.5 Probable (esp ≤ ppto; revisar PI95)
## Estrato 6 300 m² | E6 | H5 | P3 | B3 777.4 578.6–976.2 397.6 Probable (esp ≤ ppto; revisar PI95)
# ---------- Rangos (solo alertas si existen) ----------
vars_model <- c("areaconst","estrato","habitaciones","parqueaderos","banios")
invisible(range_check_tbl(res_casas_norte$train, newdata_v1, vars_model))
##
##
## Table: Chequeo de rangos — Sin alertas
##
## Mensaje
## --------------------------------------------------------------
## Todos los escenarios están dentro del rango de entrenamiento.
invisible(range_check_tbl(res_apto_sur$train, newdata_v2, vars_model))
##
##
## Table: Chequeo de rangos — Sin alertas
##
## Mensaje
## --------------------------------------------------------------
## Todos los escenarios están dentro del rango de entrenamiento.
suppressPackageStartupMessages({
library(dplyr); library(ggplot2); library(tidyr); library(tibble)
})
# --- Helper: asegura una columna 'Escenario' ---
ensure_scenario <- function(df){
if ("Escenario" %in% names(df)) return(df)
if ("estrato" %in% names(df)) {
df %>% dplyr::mutate(Escenario = paste0("Estrato ", estrato))
} else {
df %>% dplyr::mutate(Escenario = paste0("Escenario ", dplyr::row_number()))
}
}
# Formato de eje Y en millones
lab_m <- function(x) paste0(formatC(x, format = "f", digits = 0, big.mark = ","), " M")
# ==========================
# A) Escenarios con IC95/PI95 + presupuesto
# ==========================
# Vivienda 1
stopifnot(exists("pred_v1"))
pred_v1_plot <- pred_v1 %>%
ensure_scenario() %>%
dplyr::mutate(Escenario = factor(Escenario, levels = unique(Escenario)))
g_v1 <- ggplot(pred_v1_plot, aes(x = Escenario, y = Precio_Pronosticado)) +
geom_errorbar(aes(ymin = PI95_LI_ind, ymax = PI95_LS_ind), width = 0.25, alpha = 0.25) +
geom_errorbar(aes(ymin = IC95_LI_media, ymax = IC95_LS_media), width = 0.15, linewidth = 0.9) +
geom_point(size = 3) +
geom_hline(yintercept = 350, linetype = "dashed", color = "firebrick") +
annotate("text", x = 0.6, y = 350, label = "Presupuesto 350 M", color = "firebrick", vjust = -0.5) +
coord_flip() +
labs(title = "Vivienda 1 — Escenarios con IC95/PI95 y presupuesto",
x = NULL, y = "Precio (millones)") +
scale_y_continuous(labels = lab_m) +
theme_minimal()
# Vivienda 2
stopifnot(exists("pred_v2"))
pred_v2_plot <- pred_v2 %>%
ensure_scenario() %>%
dplyr::mutate(Escenario = factor(Escenario, levels = unique(Escenario)))
g_v2 <- ggplot(pred_v2_plot, aes(x = Escenario, y = Precio_Pronosticado)) +
geom_errorbar(aes(ymin = PI95_LI_ind, ymax = PI95_LS_ind), width = 0.25, alpha = 0.25) +
geom_errorbar(aes(ymin = IC95_LI_media, ymax = IC95_LS_media), width = 0.15, linewidth = 0.9) +
geom_point(size = 3) +
geom_hline(yintercept = 850, linetype = "dashed", color = "firebrick") +
annotate("text", x = 0.6, y = 850, label = "Presupuesto 850 M", color = "firebrick", vjust = -0.5) +
coord_flip() +
labs(title = "Vivienda 2 — Escenarios con IC95/PI95 y presupuesto",
x = NULL, y = "Precio (millones)") +
scale_y_continuous(labels = lab_m) +
theme_minimal()
print(g_v1); print(g_v2)
# ==========================
# B) Curvas "what-if" de Área (Precio esperado + banda PI95) por Estrato
# ==========================
area_grid <- function(train_df, var = "areaconst", by = 5, pad = 0){
r <- range(train_df[[var]], na.rm = TRUE)
r[1] <- max(0, r[1] - pad); r[2] <- r[2] + pad
seq(from = r[1], to = r[2], by = by)
}
# Vivienda 1 (Casa Norte): estratos 4 y 5, resto fijo en el perfil V1
fit1 <- res_casas_norte$fit
train1 <- res_casas_norte$train
grid1 <- dplyr::bind_rows(
tibble(estrato = 4L, areaconst = area_grid(train1)),
tibble(estrato = 5L, areaconst = area_grid(train1))
) %>%
dplyr::mutate(habitaciones = 4L, parqueaderos = 1L, banios = 2L)
pred1 <- as.data.frame(predict(fit1, newdata = grid1, interval = "prediction", level = 0.95))
grid1 <- dplyr::bind_cols(grid1, tibble(Precio = pred1$fit, PI_LI = pred1$lwr, PI_LS = pred1$upr)) %>%
dplyr::mutate(Estrato = paste("Estrato", estrato))
g1 <- ggplot(grid1, aes(x = areaconst, y = Precio, fill = Estrato, color = Estrato)) +
geom_ribbon(aes(ymin = PI_LI, ymax = PI_LS), alpha = 0.12, color = NA) +
geom_line(linewidth = 1) +
geom_hline(yintercept = 350, linetype = "dashed", color = "firebrick") +
labs(title = "Vivienda 1 — Sensibilidad al Área (Precio y PI95)",
x = "Área construida (m²)", y = "Precio (millones)") +
scale_y_continuous(labels = lab_m) +
theme_minimal()
# Vivienda 2 (Apto Sur): estratos 5 y 6, resto fijo en el perfil V2
fit2 <- res_apto_sur$fit
train2 <- res_apto_sur$train
grid2 <- dplyr::bind_rows(
tibble(estrato = 5L, areaconst = area_grid(train2)),
tibble(estrato = 6L, areaconst = area_grid(train2))
) %>%
dplyr::mutate(habitaciones = 5L, parqueaderos = 3L, banios = 3L)
pred2 <- as.data.frame(predict(fit2, newdata = grid2, interval = "prediction", level = 0.95))
grid2 <- dplyr::bind_cols(grid2, tibble(Precio = pred2$fit, PI_LI = pred2$lwr, PI_LS = pred2$upr)) %>%
dplyr::mutate(Estrato = paste("Estrato", estrato))
g2 <- ggplot(grid2, aes(x = areaconst, y = Precio, fill = Estrato, color = Estrato)) +
geom_ribbon(aes(ymin = PI_LI, ymax = PI_LS), alpha = 0.12, color = NA) +
geom_line(linewidth = 1) +
geom_hline(yintercept = 850, linetype = "dashed", color = "firebrick") +
labs(title = "Vivienda 2 — Sensibilidad al Área (Precio y PI95)",
x = "Área construida (m²)", y = "Precio (millones)") +
scale_y_continuous(labels = lab_m) +
theme_minimal()
print(g1); print(g2)
En las tablas de predicción se presenta el Precio pronosticado junto con IC95 (intervalo de confianza de la media esperada) y PI95 (intervalo de predicción individual).
IC95: rango plausible para la media del precio con esas características (incertidumbre del promedio).
PI95: rango plausible para una vivienda específica (varía más que el IC).
suppressPackageStartupMessages({
library(dplyr); library(tibble); library(knitr); library(leaflet)
})
stopifnot(exists("casas_norte_geo"), exists("res_casas_norte"),
exists("apto_sur_geo"), exists("res_apto_sur"))
# ---------- Helpers ----------
options(knitr.kable.NA = "")
fmt_m <- function(x, digits = 1){
ifelse(is.na(x), "", formatC(x, format = "f", digits = digits, big.mark = ","))
}
perfil_str <- function(area, hab, ban, parq){
paste0(round(area,1), " m² | H", hab, " | B", ban, " | P", parq)
}
print_table <- function(df, caption, digits = 2){
print(knitr::kable(df, caption = caption, digits = digits))
invisible(NULL)
}
# ---------- Solo datos (sin backticks con símbolos) ----------
find_ofertas_df <- function(df_seg, target, budget,
tol_area0 = 0.20, min_n = 5, max_expand = 3,
fit = NULL){
tol <- tol_area0
ofertas <- tibble()
for (k in 0:max_expand) {
rango <- c(target$areaconst * (1 - tol), target$areaconst * (1 + tol))
ofertas <- df_seg %>%
filter(
between(areaconst, rango[1], rango[2]),
parqueaderos >= target$parqueaderos,
banios >= target$banios,
habitaciones >= target$habitaciones,
estrato %in% target$estrato,
preciom <= budget
) %>%
mutate(dist_area = abs(areaconst - target$areaconst))
if (nrow(ofertas) >= min_n) break
tol <- tol + 0.10
}
if (nrow(ofertas) == 0) return(list(ofertas = NULL, top5 = NULL, criterio = paste0("±", round(tol*100,0), "%")))
if (!is.null(fit)) {
ofertas <- ofertas %>%
mutate(
pred_modelo = as.numeric(predict(fit, newdata = select(., areaconst, estrato, habitaciones, parqueaderos, banios))),
gap_menos_modelo = preciom - pred_modelo,
flag_valor = if_else(gap_menos_modelo < 0, "obs < pred", "obs ≥ pred")
)
} else {
ofertas <- ofertas %>% mutate(pred_modelo = NA_real_, gap_menos_modelo = NA_real_, flag_valor = "")
}
ofertas <- ofertas %>%
arrange(dist_area, preciom) %>%
mutate(criterio_area = paste0("±", round(tol*100, 0), "%"),
.rank = dplyr::row_number())
top5 <- ofertas %>% slice_head(n = min(5L, nrow(ofertas)))
list(ofertas = ofertas, top5 = top5, criterio = unique(ofertas$criterio_area))
}
# =========================
# PASO 6 — Vivienda 1 (Casa · Norte · ≤ 350M)
# =========================
target_v1 <- list(areaconst = 200, parqueaderos = 1, banios = 2, habitaciones = 4, estrato = c(4L, 5L))
res_v1 <- find_ofertas_df(
df_seg = casas_norte_geo,
target = target_v1,
budget = 350,
tol_area0 = 0.20, min_n = 5, max_expand = 3,
fit = res_casas_norte$fit
)
if (is.null(res_v1$ofertas)) {
print_table(tibble(Mensaje = "No se encontraron ofertas dentro de las restricciones."),
caption = "Vivienda 1 — Resultado")
} else {
# NOMBRES ASCII SEGUROS
tabla_v1 <- res_v1$top5 %>%
transmute(
Rank = .rank,
Barrio = barrio,
Zona = zona_geo,
Estrato = estrato,
Perfil = perfil_str(areaconst, habitaciones, banios, parqueaderos),
Precio_M = fmt_m(preciom, 1),
Pred_M = fmt_m(pred_modelo, 1),
Delta_obs_pred_M = fmt_m(gap_menos_modelo, 1),
Senal = flag_valor
)
# Encabezados bonitos (strings, no backticks)
names(tabla_v1) <- c("Rank","Barrio","Zona","Estrato","Perfil",
"Precio (M)","Pred (M)","Delta (obs - pred) (M)","Señal")
print_table(tabla_v1,
caption = paste0("Vivienda 1 (Casa · Zona Norte) — Top ", nrow(tabla_v1),
" (área ", res_v1$criterio, ", ppto ≤ 350 M)")
)
# Mapa simple (como en Paso 1)
map_v1 <- leaflet(res_v1$ofertas) %>%
addTiles() %>%
addCircleMarkers(
lng = ~longitud, lat = ~latitud,
popup = ~paste0("<b>", barrio, "</b><br>",
zona_geo, " · Estrato ", estrato, "<br>",
"Área: ", round(areaconst,1), " m² · H:", habitaciones, " · B:", banios, " · P:", parqueaderos, "<br>",
"Precio: ", round(preciom,1), " M",
if (!all(is.na(pred_modelo))) paste0("<br>Pred: ", round(pred_modelo,1), " M · Δ: ",
round(gap_menos_modelo,1), " M (", flag_valor, ")") else ""
),
radius = 6, stroke = FALSE, fillOpacity = 0.85, clusterOptions = markerClusterOptions()
) %>%
addCircleMarkers(
data = res_v1$top5,
lng = ~longitud, lat = ~latitud,
popup = ~paste0("<b>", barrio, "</b><br>",
zona_geo, " · Estrato ", estrato, "<br>",
"Área: ", round(areaconst,1), " m² · H:", habitaciones, " · B:", banios, " · P:", parqueaderos, "<br>",
"Precio: ", round(preciom,1), " M",
if (!all(is.na(pred_modelo))) paste0("<br>Pred: ", round(pred_modelo,1), " M · Δ: ",
round(gap_menos_modelo,1), " M (", flag_valor, ")") else ""
),
radius = 8, stroke = TRUE, color = "red", fillOpacity = 0.95
) %>%
addLegend("bottomright",
colors = c("#3388ff", "red"),
labels = c("Ofertas en presupuesto", "Top 5 a discutir"),
title = "Leyenda", opacity = 1)
map_v1
}
##
##
## Table: Vivienda 1 (Casa · Zona Norte) — Top 5 (área ±20%, ppto ≤ 350 M)
##
## Rank Barrio Zona Estrato Perfil Precio (M) Pred (M) Delta (obs - pred) (M) Señal
## ----- ------------- ----------------- -------- ---------------------- ----------- --------- ----------------------- -----------
## 1 la flora Zona Norte (geo) 5 200 m² | H4 | B4 | P2 320.0 552.3 -232.3 obs < pred
## 2 la merced Zona Norte (geo) 4 200 m² | H4 | B4 | P2 320.0 445.6 -125.6 obs < pred
## 3 el bosque Zona Norte (geo) 5 200 m² | H4 | B3 | P3 350.0 573.2 -223.2 obs < pred
## 4 san fernando Zona Norte (geo) 4 198 m² | H5 | B2 | P1 300.0 291.4 8.6 obs ≥ pred
## 5 el bosque Zona Norte (geo) 5 202 m² | H5 | B4 | P1 335.0 475.5 -140.5 obs < pred
# =========================
# PASO 7 — Vivienda 2 (Apto · Sur · ≤ 850M)
# =========================
target_v2 <- list(areaconst = 300, parqueaderos = 3, banios = 3, habitaciones = 5, estrato = c(5L, 6L))
res_v2 <- find_ofertas_df(
df_seg = apto_sur_geo,
target = target_v2,
budget = 850,
tol_area0 = 0.20, min_n = 5, max_expand = 3,
fit = res_apto_sur$fit
)
if (is.null(res_v2$ofertas)) {
print_table(tibble(Mensaje = "No se encontraron ofertas dentro de las restricciones."),
caption = "Vivienda 2 — Resultado")
} else {
tabla_v2 <- res_v2$top5 %>%
transmute(
Rank = .rank,
Barrio = barrio,
Zona = zona_geo,
Estrato = estrato,
Perfil = perfil_str(areaconst, habitaciones, banios, parqueaderos),
Precio_M = fmt_m(preciom, 1),
Pred_M = fmt_m(pred_modelo, 1),
Delta_obs_pred_M = fmt_m(gap_menos_modelo, 1),
Senal = flag_valor
)
names(tabla_v2) <- c("Rank","Barrio","Zona","Estrato","Perfil",
"Precio (M)","Pred (M)","Delta (obs - pred) (M)","Señal")
print_table(tabla_v2,
caption = paste0("Vivienda 2 (Apto · Zona Sur) — Top ", nrow(tabla_v2),
" (área ", res_v2$criterio, ", ppto ≤ 850 M)")
)
map_v2 <- leaflet(res_v2$ofertas) %>%
addTiles() %>%
addCircleMarkers(
lng = ~longitud, lat = ~latitud,
popup = ~paste0("<b>", barrio, "</b><br>",
zona_geo, " · Estrato ", estrato, "<br>",
"Área: ", round(areaconst,1), " m² · H:", habitaciones, " · B:", banios, " · P:", parqueaderos, "<br>",
"Precio: ", round(preciom,1), " M",
if (!all(is.na(pred_modelo))) paste0("<br>Pred: ", round(pred_modelo,1), " M · Δ: ",
round(gap_menos_modelo,1), " M (", flag_valor, ")") else ""
),
radius = 6, stroke = FALSE, fillOpacity = 0.85, clusterOptions = markerClusterOptions()
) %>%
addCircleMarkers(
data = res_v2$top5,
lng = ~longitud, lat = ~latitud,
popup = ~paste0("<b>", barrio, "</b><br>",
zona_geo, " · Estrato ", estrato, "<br>",
"Área: ", round(areaconst,1), " m² · H:", habitaciones, " · B:", banios, " · P:", parqueaderos, "<br>",
"Precio: ", round(preciom,1), " M",
if (!all(is.na(pred_modelo))) paste0("<br>Pred: ", round(pred_modelo,1), " M · Δ: ",
round(gap_menos_modelo,1), " M (", flag_valor, ")") else ""
),
radius = 8, stroke = TRUE, color = "red", fillOpacity = 0.95
) %>%
addLegend("bottomright",
colors = c("#3388ff", "red"),
labels = c("Ofertas en presupuesto", "Top 5 a discutir"),
title = "Leyenda", opacity = 1)
map_v2
}
##
##
## Table: Vivienda 2 (Apto · Zona Sur) — Top 2 (área ±60%, ppto ≤ 850 M)
##
## Rank Barrio Zona Estrato Perfil Precio (M) Pred (M) Delta (obs - pred) (M) Señal
## ----- ---------- --------------- -------- ---------------------- ----------- --------- ----------------------- -----------
## 1 seminario Zona Sur (geo) 5 300 m² | H6 | B5 | P3 670.0 781.7 -111.7 obs < pred
## 2 seminario Zona Sur (geo) 5 256 m² | H5 | B5 | P3 530.0 741.9 -211.9 obs < pred
Para sugerir las viviendas potenciales se calculó el delta entre la predicción del precio y las observaciones para viviendas con una variación en área del ±20%, de manera que la vivienda es más barata de lo esperado cuando el delta en negativo (oportunidad); y cuando el delta es positivo la vivienda es más costosa que el precio predicho por el modelo. En la tabla y el mapa se muestran las viviendas potenciales (todas en azul; en rojo las más favorables) que cumplen con los requerimientos del ejercicio.
Vivienda 1 (≤ 350 M).
Se debe enfocar en casas del Norte alrededor de 200 m²,
con 2 baños y 1 parqueadero. Si el
precio se acerca al tope de 350 M, se puede considerar
reducir el área a 180–190 m² o ser más flexible con el
número de habitaciones (tienen menos impacto que el
área). Para negociar, se puede usar el intervalo de predicción
al 95% (PI95) como banda: cerca del centro y evitar pasar del
límite superior.
Vivienda 2 (≤ 850 M).
Con el margen de presupuesto en el Sur (estratos 5–6),
priorizar 3 parqueaderos y 3 baños; el
área se podría ajustar entre 280–320
m² . Comparar siempre el precio publicado con
el precio que predice el modelo (Δ = obs − pred): si la
Δ es negativa, es una buena oportunidad; si es
positiva, se debe negociar.