El sector inmobiliario en Cali ha experimentado recientemente una disminución en sus ventas, a pesar de contar con un entorno financiero favorable por la oferta de créditos para vivienda y construcción. En este contexto, C&A (Casas y Apartamentos), dirigida por María, recibió la solicitud de una compañía internacional interesada en adquirir dos propiedades para ubicar a sus empleados en la ciudad.
El presente informe tiene como objetivo analizar los requerimientos planteados por la empresa solicitante y, mediante el uso de técnicas de modelación y análisis comparativo, identificar las opciones más adecuadas para la adquisición de las dos viviendas solicitadas. El documento presenta un análisis ejecutivo con recomendaciones, acompañado de anexos que incluyen las estimaciones y validaciones de los modelos aplicados.
Para dar respuesta a la solicitud planteada, se realizara el filtro de la base de datos en dos basado en las condiciones enviadas por el cliente y el análisis se desarrollará en cuatro etapas principales,:
Análisis exploratorio de datos (EDA): se revisarán las características del mercado inmobiliario en Cali con el fin de identificar los factores más relevantes que influyen en el precio de las viviendas, tales como área construida, número de habitaciones, baños, parqueaderos, estrato y ubicación. Igualmente se analizaran las relaciones entre variales y se tomaran las decisiones correpondientes para poder continuar con la estimacon de los modelos.
Estimación de modelos de regresión lineal múltiple: Se ajustarán modelos estadísticos (ajustando o eliminando outliers) que permitirán explicar la relación entre las características de las viviendas y su precio, con el propósito de predecir valores de referencia para los inmuebles solicitados.
Validación de supuestos del modelo: se verificarán los supuestos de la regresión lineal (normalidad, homocedasticidad, ausencia de multicolinealidad y linealidad) para garantizar la confiabilidad de los resultados.
Recomendaciones: con base en el análisis y las estimaciones obtenidas, se propondrán rangos de precios y ofertas adecuadas para las dos viviendas solicitadas por la compañía internacional.
A continuación, se presenta el análisis de la relación entre el precio de las viviendas y variables como el área construida, el estrato socioeconómico, el número de baños, el número de habitaciones y los parqueaderos, considerando cada tipo de vivienda según lo solicitado por el cliente
casas_norte <- viviendas_geo %>%
filter(tipo == "Casa" & zona_cluster_nombre == "Zona Norte")
leaflet(casas_norte) %>%
addTiles() %>% # mapa base
addMarkers(
lng = ~longitud,
lat = ~latitud,
popup = ~paste("Precio:", preciom, "millones")
)
#columnas no requeridas
vivienda_resumen_norte <- casas_norte %>%
select(-any_of(c(
"id","zona", "zona.x", "zona.y", "longitud", "latitud", "tipo",
"x_m", "y_m", "zona_cluster", "zona_cluster_nombre",
"zona_cluster_nombre.x", "zona_cluster_nombre.y",
"zona_cluster_nombre.x.x", "zona_cluster_nombre.y.y"
)))
dfSummary(vivienda_resumen_norte) %>%
print(
method = "render",
headings = FALSE,
style = "grid",
graph.col = FALSE,
graph.magnif = 0
)
No | Variable | Stats / Values | Freqs (% of Valid) | Valid | Missing | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | estrato [character] |
|
|
525 (100.0%) | 0 (0.0%) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
2 | preciom [numeric] |
|
138 distinct values | 525 (100.0%) | 0 (0.0%) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
3 | areaconst [numeric] |
|
204 distinct values | 525 (100.0%) | 0 (0.0%) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
4 | parqueaderos [numeric] |
|
|
525 (100.0%) | 0 (0.0%) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
5 | banios [numeric] |
|
|
525 (100.0%) | 0 (0.0%) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
6 | habitaciones [numeric] |
|
|
525 (100.0%) | 0 (0.0%) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
7 | barrio [character] |
|
|
525 (100.0%) | 0 (0.0%) |
Generated by summarytools 1.1.4 (R version 4.4.2)
2025-08-30
# Descriptivos numéricos
descr_resumen <- descr(vivienda_resumen_norte, headings = FALSE)
pander(descr_resumen)
areaconst | banios | habitaciones | parqueaderos | preciom | |
---|---|---|---|---|---|
Mean | 244 | 3.421 | 4.497 | 2.097 | 402.3 |
Std.Dev | 165.5 | 1.388 | 1.706 | 1.167 | 244 |
Min | 30 | 0 | 0 | 1 | 100 |
Q1 | 120 | 2 | 3 | 1 | 230 |
Median | 219 | 3 | 4 | 2 | 350 |
Q3 | 318 | 4 | 5 | 2 | 500 |
Max | 1500 | 8 | 10 | 10 | 1650 |
MAD | 146.8 | 1.483 | 1.483 | 0 | 194.2 |
IQR | 198 | 2 | 2 | 1 | 270 |
CV | 0.6784 | 0.4058 | 0.3794 | 0.5566 | 0.6063 |
Skewness | 2.637 | 0.5541 | 1.051 | 2.642 | 2.049 |
SE.Skewness | 0.1066 | 0.1066 | 0.1066 | 0.1066 | 0.1066 |
Kurtosis | 13.55 | 0.3092 | 1.413 | 10.16 | 6.621 |
N.Valid | 525 | 525 | 525 | 525 | 525 |
N | 525 | 525 | 525 | 525 | 525 |
Pct.Valid | 100 | 100 | 100 | 100 | 100 |
# Crear los gráficos individualmente
p1 <- ggplot(casas_norte, aes(y = preciom)) +
geom_boxplot() +
theme_minimal() +
labs(x = "", y = "Precio millones")
p2 <- ggplot(casas_norte, aes(y = areaconst)) +
geom_boxplot() +
theme_minimal() +
labs(x = "", y = "Área construida")
p3 <- ggplot(casas_norte, aes(y = banios)) +
geom_boxplot() +
theme_minimal() +
labs(x = "", y = "N° de baños")
p4 <- ggplot(casas_norte, aes(y = habitaciones)) +
geom_boxplot() +
theme_minimal() +
labs(x = "", y = "N° de habitaciones")
# Combinar los gráficos en un mismo panel
grid.arrange(p1, p2,p3, p4, ncol = 2)
Al realizar el resumen general de las variables, se observa lo siguiente:
Estrato: Predominan los estratos 5 (37,5%) y 3 (37%), mientras que los estratos 4 y 6 representan el 25,5% restante.
Precio: El precio promedio es de 402 millones, con alta dispersión. La mediana es de 350 millones, lo que indica una asimetría positiva hacia valores más altos. El rango intercuartílico (IQR) es de 270 millones, lo que confirma una variabilidad significativa.
Área construida: El promedio es de 244 m², con mediana de 219 m², presentando valores cercanos a la media. El rango va de 30 a 1 500 m², lo que sugiere la posible presencia de valores atípicos. El IQR es de 198 m², indicando buena dispersión en el rango central.
Parqueaderos: La mayoría de las viviendas cuentan con 2 parqueaderos, aunque se registran datos atípicos de hasta 10 parqueaderos.
Baños: En promedio, los inmuebles tienen 3 baños, con algunos valores atípicos de hasta 8 baños.
Habitaciones: El promedio es de 4 habitaciones, también con algunos valores atípicos.
Barrios: Se registran un total de 94 barrios, y el 50% de los datos se concentran en 10 barrios principales, como La Flora, Villa del Prado, El Bosque y Vipasa
ggplot(casas_norte,aes(x=areaconst,y=preciom))+
geom_point(color = "blue")+
labs(x="Área construida",y="Precio millones")+
theme_bw()
La mayoría de los datos se concentra en viviendas con áreas entre 0 y 500 m² y precios entre 100 y 1 000 millones, lo que indica que la mayor parte de las viviendas presentan tamaños y precios moderados. Se observan algunos puntos que se encuentran muy por encima de la nube principal (precios cercanos a 2 000 millones o áreas muy grandes), los cuales podrían considerarse valores atípicos; estos serán revisados en el siguiente apartado.
Se evidencia una relación positiva entre el área construida y el precio, es decir, el precio tiende a aumentar a medida que aumenta el área. Sin embargo, se realizará la prueba de correlación de Pearson para confirmar esta relación lineal.
cor.test(casas_norte$areaconst,casas_norte$preciom)
##
## Pearson's product-moment correlation
##
## data: casas_norte$areaconst and casas_norte$preciom
## t = 28.774, df = 720, p-value < 2.2e-16
## alternative hypothesis: true correlation is not equal to 0
## 95 percent confidence interval:
## 0.6954970 0.7635667
## sample estimates:
## cor
## 0.731348
Dado que el valor p = 2.2 × 10⁻¹⁶ es menor al nivel de significancia, se rechaza la hipótesis nula (H₀), lo que indica que existe una relación lineal significativa entre las variables. Además, la correlación de Pearson entre el precio y el área construida es de 0.73, lo que refleja una relación positiva moderada-alta.
# Selecciona solo columnas numéricas excluyendo las que no quieres
cols_excluir <- c(1,2,3,9,10,11,12,13,14,15) # excluir
vivienda_num_norte <- casas_norte[, sapply(casas_norte, is.numeric)]
vivienda_num_norte <- vivienda_num_norte[, !(colnames(vivienda_num_norte) %in% colnames(casas_norte)[cols_excluir])]
# Genera la correlación
chart.Correlation(vivienda_num_norte, histogram = TRUE, method = "pearson")
Al analizar la correlación entre las variables, se observa que el precio presenta una alta relación con el área construida (0,74), es decir, a mayor área, mayor precio. Además, muestra una relación moderada con el número de baños y habitaciones, mientras que la correlación con los parqueaderos es baja. Por su parte, el área construida se relaciona de manera moderada con baños y habitaciones, pero mantiene una correlación baja con los parqueaderos (0,22). El número de baños muestra una correlación moderada con las habitaciones, mientras que los parqueaderos presentan las correlaciones más bajas con las demás variables, lo que indica que su cantidad no varía significativamente según el tamaño o precio de la vivienda.
aptos_sur <- viviendas_geo %>%
filter(tipo == "Apartamento" & zona_cluster_nombre == "Zona Sur")
leaflet(aptos_sur) %>%
addTiles() %>% # mapa base
addMarkers(
lng = ~longitud,
lat = ~latitud,
popup = ~paste("Precio:", preciom, "millones")
)
#columnas no requeridas
vivienda_resumen_sur <- aptos_sur %>%
select(-any_of(c(
"id","zona", "zona.x", "zona.y", "longitud", "latitud", "tipo",
"x_m", "y_m", "zona_cluster", "zona_cluster_nombre",
"zona_cluster_nombre.x", "zona_cluster_nombre.y",
"zona_cluster_nombre.x.x", "zona_cluster_nombre.y.y"
)))
# Crear cuadro resumen
dfSummary(vivienda_resumen_sur) %>%
print(
method = "render", # para RMarkdown / HTML
headings = FALSE, # sin títulos
style = "grid", # estilo tipo tabla
graph.col = FALSE, # desactiva barras gráficas por columna
graph.magnif = 0 # desactiva gráficos de magnitud
)
No | Variable | Stats / Values | Freqs (% of Valid) | Valid | Missing | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | estrato [character] |
|
|
1632 (100.0%) | 0 (0.0%) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
2 | preciom [numeric] |
|
281 distinct values | 1632 (100.0%) | 0 (0.0%) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
3 | areaconst [numeric] |
|
217 distinct values | 1632 (100.0%) | 0 (0.0%) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
4 | parqueaderos [numeric] |
|
|
1632 (100.0%) | 0 (0.0%) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
5 | banios [numeric] |
|
|
1632 (100.0%) | 0 (0.0%) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
6 | habitaciones [numeric] |
|
|
1632 (100.0%) | 0 (0.0%) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
7 | barrio [character] |
|
|
1632 (100.0%) | 0 (0.0%) |
Generated by summarytools 1.1.4 (R version 4.4.2)
2025-08-30
#descriptivos numericos
descr_resumen <- descr(vivienda_resumen_sur, headings = FALSE)
pander(descr_resumen)
areaconst | banios | habitaciones | parqueaderos | preciom | |
---|---|---|---|---|---|
Mean | 99.77 | 2.588 | 2.909 | 1.4 | 329.4 |
Std.Dev | 56.89 | 0.9671 | 0.6108 | 0.6465 | 222.2 |
Min | 40 | 0 | 0 | 1 | 78 |
Q1 | 65 | 2 | 3 | 1 | 185 |
Median | 85 | 2 | 3 | 1 | 260 |
Q3 | 115 | 3 | 3 | 2 | 375 |
Max | 932 | 7 | 6 | 4 | 1750 |
MAD | 34.1 | 0 | 0 | 0 | 133.4 |
IQR | 50 | 1 | 0 | 1 | 190 |
CV | 0.5702 | 0.3736 | 0.21 | 0.4618 | 0.6747 |
Skewness | 4.308 | 1.187 | -0.1753 | 1.742 | 2.369 |
SE.Skewness | 0.06058 | 0.06058 | 0.06058 | 0.06058 | 0.06058 |
Kurtosis | 38.02 | 1.14 | 1.93 | 3.19 | 7.917 |
N.Valid | 1632 | 1632 | 1632 | 1632 | 1632 |
N | 1632 | 1632 | 1632 | 1632 | 1632 |
Pct.Valid | 100 | 100 | 100 | 100 | 100 |
# Crear los gráficos individualmente
p1 <- ggplot(aptos_sur, aes(y = preciom)) +
geom_boxplot() +
theme_minimal() +
labs(x = "", y = "Precio millones")
p2 <- ggplot(aptos_sur, aes(y = areaconst)) +
geom_boxplot() +
theme_minimal() +
labs(x = "", y = "Área construida")
p3 <- ggplot(aptos_sur, aes(y = banios)) +
geom_boxplot() +
theme_minimal() +
labs(x = "", y = "N° de baños")
p4 <- ggplot(aptos_sur, aes(y = habitaciones)) +
geom_boxplot() +
theme_minimal() +
labs(x = "", y = "N° de habitaciones")
# Combinar los gráficos en un mismo panel
grid.arrange(p1, p2,p3, p4, ncol = 2)
Al realizar el resumen general de las variables de los apartamentos, se observa lo siguiente:
Estrato: Predominan los estratos 4 (37,1%) y 5 (33,9%), mientras que los estratos 3 y 6 representan el 29% restante.
Precio: El precio promedio es de 329 millones, con alta dispersión. La mediana es de 260 millones, lo que indica una asimetría positiva hacia valores más altos. El rango intercuartílico (IQR) es de 190 millones, mostrando variabilidad significativa en los precios.
Área construida: El promedio es de 99 m², con mediana de 85 m², con valores cercanos a la media; sin embargo, se registran algunos apartamentos con hasta 932 m², generando una distribución sesgada hacia la derecha. Estos datos podrían considerarse valores atípicos, los cuales serán tratados en el siguiente apartado.
Parqueaderos: La mayoría de los apartamentos cuentan con un parqueadero, aunque se presentan casos atípicos de hasta 4 parqueaderos.
Baños: En promedio, los apartamentos tienen 2 baños, con algunos valores atípicos de hasta 7 baños.
Habitaciones: El promedio es de 3 habitaciones, también con algunos valores atípicos.
Barrios: Se registran un total de 46 barrios, concentrándose la mayoría de los apartamentos en 3 barrios principales: Valle del Lili, Ciudad Jardín y Pance
ggplot(aptos_sur,aes(x=areaconst,y=preciom))+
geom_point(color = "blue")+
labs(x="Área construida",y="Precio millones")+
theme_bw()
La mayoría de los datos se concentra en viviendas con áreas entre 0 y 250m² , lo que indica que la mayor parte de las viviendas presentan tamaños y precios moderados. Se observan algunos puntos que se encuentran alejados de la nube principal (precios cercanos a 2000 millones o áreas muy grandes a precios muy bajos), los cuales podrían considerarse valores atípicos; estos serán revisados en el siguiente apartado.
Se evidencia una relación positiva entre el área construida y el precio, es decir, el precio tiende a aumentar a medida que aumenta el área. Sin embargo, se realizará la prueba de correlación de Pearson para confirmar esta relación lineal.
cor.test(aptos_sur$areaconst,aptos_sur$preciom)
##
## Pearson's product-moment correlation
##
## data: aptos_sur$areaconst and aptos_sur$preciom
## t = 54.292, df = 1630, p-value < 2.2e-16
## alternative hypothesis: true correlation is not equal to 0
## 95 percent confidence interval:
## 0.7844697 0.8190780
## sample estimates:
## cor
## 0.8024476
Dado que el valor p = 2.2 × 10⁻¹⁶ es significativamente menor que el nivel de significancia, se rechaza la hipótesis nula (H₀), lo que confirma la existencia de una relación lineal significativa entre las variables. Además, el coeficiente de correlación de Pearson entre el precio y el área construida es de 0.802, lo que indica una fuerte relación positiva entre ambas variables. Esto sugiere que a medida que el área construida aumenta, también lo hace el precio de las propiedades en el conjunto de datos analizado.
# Selecciona solo columnas numéricas excluyendo las que no quieres
cols_excluir <- c(1,2,3,9,10,11,12,13,14,15) # excluir
vivienda_num_sur <- aptos_sur[, sapply(aptos_sur, is.numeric)]
vivienda_num_sur <- vivienda_num_sur[, !(colnames(vivienda_num_sur) %in% colnames(aptos_sur)[cols_excluir])]
# Genera la correlación
chart.Correlation(vivienda_num_sur, histogram = TRUE, method = "pearson")
Al analizar la correlación entre las variables, se observa que el precio presenta una alta relación con el área construida (0,79), numero de baños (0,80) y con el número de parqueaderos (0,80), lo que sugiere que a mayor área , mayor numero de baños y mayor número de parqueaderos, también tiende a aumentar el precio de la propiedad igualmente y una correlación más baja con las habitaciones (0,46).
El área construida, por su parte, tiene una correlación moderada tanto con los baños (0,69) como con los parqueasderos (0,64). la relacion mas baja se presenta con el numero de habitaciones (0,64). El número de baños también muestra una correlación moderada con las habitaciones (0,51). Finalmente, llas habitaciones presentan las correlaciones más bajas con las demás variables, lo que indica que su cantidad no varía de manera tan significativa en función del tamaño de la propiedad.
Para el caso de las casas, se procede a identificar y analizar los valores atípicos mediante el uso del Z-Score
# Función para calcular z-score
z_score <- function(x) (x - mean(x, na.rm = TRUE)) / sd(x, na.rm = TRUE)
# Aplicar z-score a las variables clave
casas_norte <- casas_norte %>%
mutate(
z_preciom = z_score(preciom),
z_areaconst = z_score(areaconst),
z_habitaciones = z_score(habitaciones),
z_banios = z_score(banios),
z_parqueaderos = z_score(parqueaderos)
)
# Filtrar outliers univariados |z|>3 en precio o baños
outliers_uni <- casas_norte %>%
filter(abs(z_preciom) > 3 | abs(z_banios) > 3 | banios >= 8) %>%
select(id, estrato, preciom, areaconst, parqueaderos, banios, habitaciones, barrio)
# Mostrar en tabla bonita
outliers_uni %>%
kable(caption = "Outliers detectados en Casas Norte") %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE,
position = "center")
id | estrato | preciom | areaconst | parqueaderos | banios | habitaciones | barrio |
---|---|---|---|---|---|---|---|
1653 | 6 | 1590 | 530 | 6 | 5 | 4 | cristales |
802 | 4 | 470 | 128 | 4 | 8 | 10 | la merced |
3449 | 5 | 1500 | 400 | 2 | 3 | 4 | menga |
3284 | 6 | 1250 | 330 | 6 | 5 | 4 | menga |
315 | 6 | 1650 | 1500 | 4 | 5 | 3 | pance |
1296 | 6 | 1500 | 596 | 5 | 6 | 5 | pance |
181 | 6 | 1250 | 300 | 2 | 5 | 4 | pance |
2360 | 5 | 690 | 434 | 3 | 8 | 9 | prados del norte |
1121 | 3 | 400 | 300 | 2 | 8 | 8 | prados del norte |
752 | 3 | 650 | 510 | 2 | 8 | 10 | salomia |
3858 | 4 | 1650 | 734 | 2 | 5 | 10 | san vicente |
4274 | 5 | 1270 | 950 | 4 | 5 | 10 | santa monica |
489 | 6 | 1250 | 628 | 2 | 6 | 5 | santa teresita |
673 | 6 | 1600 | 825 | 8 | 6 | 8 | seminario |
4056 | 5 | 1600 | 942 | 4 | 4 | 10 | versalles |
1298 | 4 | 690 | 358 | 6 | 8 | 9 | vipasa |
A continuación, se presentan los posibles outliers, es decir, aquellos valores que no resultan del todo consistentes con el comportamiento observado en el mercado.
# Outliers sospechosos: baños desproporcionados respecto al área
outliers_banios_sospechosos <- casas_norte %>%
filter(banios >= 8 & areaconst < 400) %>%
select(id, estrato, preciom, areaconst, parqueaderos, banios, habitaciones, barrio)
# Mostrar en tabla bonita
outliers_banios_sospechosos %>%
kable(caption = "Outliers: Baños desproporcionados respecto al área en Casas Norte") %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE,
position = "center")
id | estrato | preciom | areaconst | parqueaderos | banios | habitaciones | barrio |
---|---|---|---|---|---|---|---|
802 | 4 | 470 | 128 | 4 | 8 | 10 | la merced |
1121 | 3 | 400 | 300 | 2 | 8 | 8 | prados del norte |
1298 | 4 | 690 | 358 | 6 | 8 | 9 | vipasa |
# Guardar los IDs de estos outliers
ids_outliers <- outliers_banios_sospechosos$id
Se identificaron tres casos que podrían deberse a errores de digitación. Dado que representan una proporción mínima de la muestra y no afectan de manera significativa la interpretación del modelo, se decidió eliminarlos
# Dataset limpio sin los outliers sospechosos
casas_norte_limpia <- casas_norte %>% filter(!id %in% ids_outliers)
# Modelo original con todos los datos
modelo_casas_original <- lm(preciom ~ areaconst + habitaciones + banios + parqueaderos,
data = casas_norte)
r2_original <- summary(modelo_casas_original)$adj.r.squared
# Modelo sin outliers
modelo_casas_limpio <- lm(preciom ~ areaconst + habitaciones + banios + parqueaderos,
data = casas_norte_limpia)
r2_limpio <- summary(modelo_casas_limpio)$adj.r.squared
# Comparación en tabla
library(kableExtra)
comparacion_modelos <- data.frame(
Modelo = c("Original", "Limpio (sin outliers)"),
R2_Ajustado = c(r2_original, r2_limpio),
Error_Residual = c(sigma(modelo_casas_original), sigma(modelo_casas_limpio))
)
comparacion_modelos %>%
kable(caption = "Comparación entre modelo original y modelo limpio") %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE,
position = "center",
font_size = 12)
Modelo | R2_Ajustado | Error_Residual |
---|---|---|
Original | 0.6080195 | 152.7368 |
Limpio (sin outliers) | 0.6083867 | 152.8890 |
La eliminación de los outliers no generó una mejora en el modelo, ya que el R² ajustado se mantiene similar en ambas estimaciones. Esto indica que la capacidad explicativa del modelo no cambió, es decir, las variables área construida, número de habitaciones, número de baños y parqueaderos explican aproximadamente el 60% de la variabilidad del precio.
casas_norte$cooks_d <- cooks.distance(modelo_casas_original)
n <- nrow(casas_norte)
umbral_cook <- 4/n
# Calcular número de observaciones del modelo
n_modelo <- length(casas_norte$cooks_d)
umbral_cook <- 4 / n_modelo
# Preparar data.frame para ggplot
cook_data <- data.frame(
ID = 1:n_modelo,
CooksD = casas_norte$cooks_d
)
# Gráfico de barras con línea de umbral
ggplot(cook_data, aes(x = ID, y = CooksD)) +
geom_col(fill = "steelblue") +
geom_hline(yintercept = umbral_cook, color = "red", linetype = "dashed") +
labs(title = "Distancia de Cook - Casas Norte",
x = "Observación",
y = "Cook's D") +
theme_minimal()
Conclusion
Se realizó un análisis exploratorio para detectar valores atípicos en las variables clave de las casas en la zona norte, identificando principalmente viviendas con un número de baños elevado (≥8) y área construida reducida (<400 m²). Se construyeron modelos de regresión lineal múltiple para predecir el precio de las viviendas, tanto con todos los datos como excluyendo estos outliers sospechosos. La comparación de los modelos mostró que el R² ajustado y el error residual permanecen prácticamente iguales al eliminar los outliers (R² ajustado: 0.608 vs. 0.6084; error residual: 152.74 vs. 152.89), indicando que estos casos extremos no ejercen una influencia significativa sobre el ajuste del modelo. Por tanto, se concluye que estos valores, aunque inusuales, representan observaciones reales y no distorsionan las relaciones principales entre las variables. El modelo destaca que los factores más relevantes para determinar el precio son el número de baños, el área construida y los parqueaderos, mientras que la cantidad de habitaciones tiene un efecto menor y negativo al mantener constantes las demás variables.
Revisamos el z score para los apartamentos con mas de 3 baños
# Función z-score (puedes dejarla como está si ya la definiste antes)
z_score <- function(x) (x - mean(x, na.rm = TRUE)) / sd(x, na.rm = TRUE)
# Aplicar z-score a las variables clave de los apartamentos
aptos_sur <- aptos_sur %>%
mutate(
z_preciom = z_score(preciom),
z_areaconst = z_score(areaconst),
z_habitaciones = z_score(habitaciones),
z_banios = z_score(banios),
z_parqueaderos = z_score(parqueaderos)
)
# Filtrar outliers univariados en precio y baños (ajusta umbrales según contexto)
outliers_uni_aptos <- aptos_sur %>%
filter(abs(z_preciom) > 3 | abs(z_banios) > 3 | banios >= 6) %>%
select(id, estrato, preciom, areaconst, parqueaderos, banios, habitaciones, barrio)
# Mostrar tabla de outliers
outliers_uni_aptos %>%
kable(caption = "Outliers detectados en Apartamentos Sur") %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE,
position = "center")
id | estrato | preciom | areaconst | parqueaderos | banios | habitaciones | barrio |
---|---|---|---|---|---|---|---|
5952 | 6 | 1750 | 342 | 3 | 5 | 4 | ciudad jardin |
6475 | 6 | 1561 | 399 | 3 | 4 | 3 | ciudad jardin |
3975 | 6 | 1240 | 222 | 3 | 5 | 4 | ciudad jardin |
6197 | 6 | 1700 | 290 | 3 | 4 | 3 | ciudad jardin |
5211 | 6 | 1350 | 439 | 4 | 6 | 4 | ciudad jardin |
5248 | 6 | 1150 | 344 | 4 | 5 | 5 | ciudad jardin |
5460 | 6 | 1150 | 346 | 2 | 6 | 5 | ciudad jardin |
6023 | 6 | 1150 | 464 | 4 | 6 | 5 | ciudad jardin |
5190 | 6 | 1600 | 345 | 3 | 6 | 3 | ciudad jardin |
6086 | 6 | 1500 | 240 | 3 | 5 | 6 | ciudad jardin |
5532 | 6 | 1150 | 344 | 4 | 5 | 4 | ciudad jardin |
6275 | 6 | 950 | 330 | 4 | 6 | 4 | ciudad jardin |
6868 | 3 | 370 | 300 | 3 | 6 | 5 | melendez |
6073 | 5 | 1250 | 251 | 4 | 5 | 4 | multicentro |
8227 | 6 | 1400 | 300 | 2 | 4 | 3 | normandia |
6510 | 6 | 1600 | 290 | 3 | 5 | 4 | pance |
6512 | 6 | 1750 | 290 | 3 | 4 | 3 | pance |
6887 | 6 | 1050 | 170 | 4 | 6 | 3 | pance |
4712 | 6 | 1100 | 220 | 4 | 4 | 4 | pance |
7346 | 6 | 1350 | 212 | 3 | 5 | 3 | pance |
3592 | 6 | 1150 | 222 | 2 | 4 | 3 | pance |
3785 | 6 | 1580 | 296 | 4 | 4 | 3 | pance |
5472 | 6 | 1590 | 310 | 3 | 4 | 3 | pance |
6937 | 6 | 1350 | 212 | 3 | 4 | 3 | pance |
7176 | 6 | 1150 | 344 | 1 | 6 | 4 | pance |
5404 | 6 | 980 | 250 | 4 | 7 | 4 | parcelaciones pance |
4682 | 6 | 1200 | 254 | 3 | 4 | 3 | santa teresita |
# Outliers sospechosos: baños desproporcionados respecto al área en apartamentos
outliers_banios_sospechosos_aptos <- aptos_sur %>%
filter(banios >= 5 & areaconst < 300) %>%
select(id, estrato, preciom, areaconst, parqueaderos, banios, habitaciones, barrio)
# Mostrar en tabla
outliers_banios_sospechosos_aptos %>%
kable(caption = "Outliers: Baños desproporcionados respecto al área en Apartamentos Sur") %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE,
position = "center")
id | estrato | preciom | areaconst | parqueaderos | banios | habitaciones | barrio |
---|---|---|---|---|---|---|---|
4415 | 5 | 650 | 130.00 | 1 | 5 | 3 | acopi |
4648 | 5 | 580 | 165.00 | 2 | 5 | 4 | canasgordas |
4374 | 5 | 600 | 200.00 | 2 | 5 | 3 | canasgordas |
4824 | 6 | 680 | 167.00 | 1 | 5 | 4 | canasgordas |
4373 | 6 | 560 | 141.00 | 3 | 5 | 4 | cataya real |
4478 | 6 | 650 | 160.00 | 2 | 5 | 3 | ciudad capri |
5941 | 5 | 700 | 138.00 | 2 | 5 | 4 | ciudad jardin |
6749 | 6 | 690 | 180.00 | 2 | 5 | 4 | ciudad jardin |
5691 | 6 | 695 | 180.00 | 2 | 5 | 4 | ciudad jardin |
5793 | 5 | 690 | 169.00 | 4 | 5 | 4 | ciudad jardin |
5845 | 6 | 580 | 130.00 | 2 | 5 | 4 | ciudad jardin |
5916 | 6 | 580 | 130.00 | 2 | 5 | 4 | ciudad jardin |
6603 | 6 | 750 | 166.00 | 2 | 5 | 3 | ciudad jardin |
5826 | 6 | 650 | 160.00 | 2 | 5 | 3 | ciudad jardin |
6576 | 6 | 660 | 210.00 | 4 | 5 | 3 | ciudad jardin |
4467 | 6 | 540 | 128.00 | 2 | 5 | 4 | ciudad jardin |
5842 | 6 | 850 | 187.00 | 3 | 5 | 4 | ciudad jardin |
3975 | 6 | 1240 | 222.00 | 3 | 5 | 4 | ciudad jardin |
5767 | 6 | 950 | 287.00 | 4 | 5 | 3 | ciudad jardin |
6614 | 6 | 850 | 168.00 | 3 | 5 | 4 | ciudad jardin |
3786 | 6 | 850 | 192.00 | 2 | 5 | 3 | ciudad jardin |
5881 | 6 | 650 | 223.00 | 3 | 5 | 3 | ciudad jardin |
6086 | 6 | 1500 | 240.00 | 3 | 5 | 6 | ciudad jardin |
4182 | 6 | 650 | 164.00 | 2 | 5 | 3 | ciudad jardin |
5690 | 6 | 850 | 191.80 | 2 | 5 | 4 | ciudad jardin |
5757 | 6 | 737 | 147.00 | 2 | 5 | 3 | ciudad jardin |
6539 | 6 | 850 | 186.00 | 3 | 5 | 4 | ciudad jardin |
5364 | 6 | 750 | 141.00 | 2 | 5 | 2 | ciudad jardin |
5259 | 5 | 300 | 155.00 | 1 | 5 | 4 | el ingenio |
4815 | 5 | 480 | 249.00 | 1 | 5 | 4 | el ingenio |
2189 | 5 | 350 | 153.00 | 1 | 5 | 4 | el lido |
5843 | 6 | 643 | 224.74 | 3 | 5 | 4 | juanambu |
6073 | 5 | 1250 | 251.00 | 4 | 5 | 4 | multicentro |
3113 | 6 | 760 | 220.00 | 2 | 5 | 3 | pance |
5591 | 6 | 450 | 120.00 | 2 | 5 | 3 | pance |
6463 | 6 | 760 | 160.00 | 3 | 5 | 4 | pance |
6934 | 6 | 600 | 127.00 | 3 | 5 | 3 | pance |
3813 | 6 | 750 | 200.00 | 3 | 5 | 4 | pance |
3814 | 6 | 750 | 191.00 | 3 | 5 | 4 | pance |
4319 | 6 | 750 | 164.00 | 2 | 5 | 4 | pance |
6470 | 6 | 600 | 200.00 | 2 | 5 | 4 | pance |
6545 | 6 | 580 | 132.00 | 2 | 5 | 4 | pance |
5624 | 6 | 600 | 230.00 | 2 | 5 | 4 | pance |
6510 | 6 | 1600 | 290.00 | 3 | 5 | 4 | pance |
6887 | 6 | 1050 | 170.00 | 4 | 6 | 3 | pance |
7147 | 6 | 900 | 165.00 | 3 | 5 | 3 | pance |
3810 | 6 | 700 | 149.00 | 2 | 5 | 4 | pance |
3937 | 6 | 700 | 156.00 | 2 | 5 | 4 | pance |
3939 | 6 | 700 | 160.00 | 2 | 5 | 4 | pance |
4362 | 6 | 603 | 133.00 | 2 | 5 | 3 | pance |
4428 | 6 | 615 | 162.00 | 2 | 5 | 4 | pance |
5415 | 6 | 595 | 133.00 | 2 | 5 | 3 | pance |
6371 | 6 | 490 | 114.00 | 2 | 5 | 4 | pance |
6462 | 6 | 700 | 160.00 | 3 | 5 | 4 | pance |
6613 | 6 | 845 | 187.00 | 2 | 5 | 4 | pance |
3933 | 6 | 800 | 160.00 | 2 | 5 | 3 | pance |
3936 | 6 | 800 | 160.00 | 2 | 5 | 3 | pance |
3941 | 6 | 690 | 160.00 | 2 | 5 | 4 | pance |
3942 | 6 | 630 | 150.00 | 2 | 5 | 3 | pance |
4105 | 6 | 600 | 148.00 | 2 | 5 | 4 | pance |
4110 | 6 | 600 | 148.00 | 3 | 5 | 3 | pance |
6159 | 6 | 810 | 164.00 | 3 | 5 | 3 | pance |
6720 | 6 | 836 | 187.00 | 3 | 5 | 3 | pance |
6939 | 6 | 590 | 127.00 | 2 | 5 | 3 | pance |
6964 | 6 | 560 | 127.00 | 2 | 5 | 3 | pance |
3802 | 6 | 710 | 158.00 | 2 | 5 | 3 | pance |
6445 | 6 | 860 | 187.00 | 3 | 5 | 3 | pance |
6901 | 6 | 594 | 133.00 | 2 | 5 | 4 | pance |
3825 | 6 | 700 | 160.00 | 2 | 5 | 4 | pance |
5598 | 6 | 650 | 138.00 | 3 | 5 | 4 | pance |
6612 | 6 | 830 | 187.00 | 3 | 5 | 4 | pance |
6938 | 6 | 580 | 131.00 | 2 | 5 | 3 | pance |
7346 | 6 | 1350 | 212.00 | 3 | 5 | 3 | pance |
6936 | 6 | 600 | 128.00 | 2 | 5 | 4 | pance |
3932 | 6 | 700 | 152.00 | 2 | 5 | 3 | pance |
3943 | 6 | 680 | 158.00 | 2 | 5 | 4 | pance |
3944 | 6 | 690 | 158.00 | 2 | 5 | 4 | pance |
4423 | 6 | 395 | 120.00 | 2 | 5 | 4 | pance |
4758 | 6 | 880 | 185.00 | 3 | 5 | 3 | pance |
5435 | 6 | 603 | 133.30 | 2 | 5 | 3 | pance |
6077 | 5 | 710 | 151.00 | 3 | 5 | 3 | pance |
6508 | 6 | 865 | 187.00 | 3 | 5 | 4 | pance |
6509 | 6 | 865 | 187.00 | 3 | 5 | 3 | pance |
6683 | 6 | 830 | 169.00 | 3 | 5 | 3 | pance |
6684 | 6 | 890 | 185.00 | 3 | 5 | 4 | pance |
6687 | 6 | 890 | 187.00 | 3 | 5 | 3 | pance |
5404 | 6 | 980 | 250.00 | 4 | 7 | 4 | parcelaciones pance |
6611 | 6 | 850 | 185.00 | 3 | 5 | 3 | parcelaciones pance |
4418 | 6 | 720 | 170.00 | 2 | 5 | 4 | parcelaciones pance |
3815 | 6 | 700 | 158.00 | 2 | 5 | 4 | parcelaciones pance |
La gran mayoria de datos pertenencen al estrato 6 con precios altos los cuales pueden ser reales pues pueden ser penthouses. sin embargo se presentan algunos casos los cuales no son congruentes con el mercado.
vamos a compapar los dos modelos con y sin estos datos para saber que tanto cambian nuestro coeficiente.
# Outliers estadísticos basados en el modelo preliminar
modelo <- lm(preciom ~ areaconst + habitaciones + banios + parqueaderos, data = aptos_sur)
# Residuos estandarizados
aptos_sur$residuos_std <- rstandard(modelo)
outliers_residuos <- aptos_sur %>% filter(abs(residuos_std) > 3)
# Distancia de Cook
aptos_sur$cooks_d <- cooks.distance(modelo)
n <- nrow(aptos_sur)
umbral_cook <- 4/n
outliers_cook <- aptos_sur %>% filter(cooks_d > umbral_cook)
# Leverage
aptos_sur$leverage <- hatvalues(modelo)
prom_leverage <- mean(aptos_sur$leverage)
outliers_leverage <- aptos_sur %>% filter(leverage > (2 * prom_leverage))
# IDs coincidentes = más problemáticos
ids_finales <- Reduce(intersect, list(outliers_residuos$id,
outliers_cook$id,
outliers_leverage$id))
outliers_finales <- aptos_sur %>%
filter(id %in% ids_finales) %>%
select(id, estrato, preciom, areaconst, parqueaderos, banios, habitaciones, barrio)
outliers_finales %>%
kable(caption = "Outliers finales (coincidencia de criterios)") %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE, position = "center")
id | estrato | preciom | areaconst | parqueaderos | banios | habitaciones | barrio |
---|---|---|---|---|---|---|---|
3479 | 6 | 620 | 480 | 1 | 5 | 5 | acopi |
5952 | 6 | 1750 | 342 | 3 | 5 | 4 | ciudad jardin |
6475 | 6 | 1561 | 399 | 3 | 4 | 3 | ciudad jardin |
6197 | 6 | 1700 | 290 | 3 | 4 | 3 | ciudad jardin |
5190 | 6 | 1600 | 345 | 3 | 6 | 3 | ciudad jardin |
6086 | 6 | 1500 | 240 | 3 | 5 | 6 | ciudad jardin |
4394 | 5 | 690 | 486 | 2 | 4 | 4 | el ingenio |
4952 | 5 | 650 | 600 | 2 | 4 | 5 | el ingenio |
6868 | 3 | 370 | 300 | 3 | 6 | 5 | melendez |
8227 | 6 | 1400 | 300 | 2 | 4 | 3 | normandia |
6510 | 6 | 1600 | 290 | 3 | 5 | 4 | pance |
6512 | 6 | 1750 | 290 | 3 | 4 | 3 | pance |
7346 | 6 | 1350 | 212 | 3 | 5 | 3 | pance |
3785 | 6 | 1580 | 296 | 4 | 4 | 3 | pance |
5472 | 6 | 1590 | 310 | 3 | 4 | 3 | pance |
6723 | 6 | 840 | 185 | 2 | 2 | 2 | pance |
4682 | 6 | 1200 | 254 | 3 | 4 | 3 | santa teresita |
6121 | 5 | 299 | 932 | 1 | 3 | 3 | valle del lili |
4990 | 5 | 350 | 174 | 4 | 3 | 4 | zona sur |
# Comparación de modelos con y sin outliers finales
# Base sin outliers finales
aptos_sur_sin_outliers <- aptos_sur %>%
filter(!(id %in% ids_finales))
# Modelo con outliers
modelo_con <- lm(preciom ~ areaconst + habitaciones + banios + parqueaderos,
data = aptos_sur)
# Modelo sin outliers
modelo_sin <- lm(preciom ~ areaconst + habitaciones + banios + parqueaderos,
data = aptos_sur_sin_outliers)
# Crear tabla comparativa de coeficientes
library(broom)
coef_con <- tidy(modelo_con) %>% mutate(modelo = "Con outliers")
coef_sin <- tidy(modelo_sin) %>% mutate(modelo = "Sin outliers")
comparacion_coef <- bind_rows(coef_con, coef_sin) %>%
select(modelo, term, estimate, std.error, statistic, p.value)
comparacion_coef %>%
kable(caption = "Comparación de coeficientes: Modelo con vs sin outliers finales") %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE, position = "center")
modelo | term | estimate | std.error | statistic | p.value |
---|---|---|---|---|---|
Con outliers | (Intercept) | -106.443755 | 12.8387946 | -8.290790 | 0.0000000 |
Con outliers | areaconst | 1.644518 | 0.0671905 | 24.475446 | 0.0000000 |
Con outliers | habitaciones | -18.631404 | 4.9874759 | -3.735638 | 0.0001937 |
Con outliers | banios | 67.329093 | 4.2270167 | 15.928277 | 0.0000000 |
Con outliers | parqueaderos | 108.342828 | 5.6502098 | 19.175010 | 0.0000000 |
Sin outliers | (Intercept) | -86.032842 | 9.3052868 | -9.245587 | 0.0000000 |
Sin outliers | areaconst | 2.467577 | 0.0694023 | 35.554695 | 0.0000000 |
Sin outliers | habitaciones | -17.755708 | 3.6122572 | -4.915405 | 0.0000010 |
Sin outliers | banios | 48.655795 | 3.1863415 | 15.270113 | 0.0000000 |
Sin outliers | parqueaderos | 67.247117 | 4.3504191 | 15.457618 | 0.0000000 |
# ---- Tabla de métricas de desempeño ----
metricas <- tibble(
Modelo = c("Con outliers", "Sin outliers"),
`R² ajustado` = c(summary(modelo_con)$adj.r.squared,
summary(modelo_sin)$adj.r.squared),
`Error residual (RSE)` = c(summary(modelo_con)$sigma,
summary(modelo_sin)$sigma)
)
metricas %>%
kable(caption = "Comparación de desempeño de los modelos") %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE, position = "center")
Modelo | R² ajustado | Error residual (RSE) |
---|---|---|
Con outliers | 0.7809839 | 103.99910 |
Sin outliers | 0.8577584 | 73.76921 |
Conclusion
Se realizó un análisis exploratorio para detectar valores atípicos en las variables clave de los apartamentos en la zona sur, identificando casos con precios inusuales y otros con un número de baños desproporcionado frente al área construida. Posteriormente, se construyeron modelos de regresión lineal múltiple para predecir el precio de los apartamentos, tanto incluyendo todos los datos como excluyendo los outliers más problemáticos. La comparación evidenció que al eliminar estos valores extremos, el R² ajustado pasó de 0.781 a 0.858 y el error residual estándar se redujo de 104 a 73.8, lo que confirma que dichos outliers distorsionaban el ajuste del modelo. Asimismo, se observó que sin los outliers el efecto del área construida se fortaleció, mientras que la influencia de baños y parqueaderos se ajustó a valores más razonables, y el número de habitaciones mantuvo un efecto negativo menor. Estos resultados justifican la exclusión de los outliers para obtener un modelo más representativo, estable y coherente con la lógica del mercado inmobiliario.
# Validación de supuestos para Casas
par(mfrow = c(2, 2))
plot(modelo_casas_limpio)
El análisis de los supuestos para el modelo de casas muestra que, aunque en general el ajuste es razonable, aún se observan algunas limitaciones: los errores no se distribuyen de forma completamente normal, la variabilidad de los residuos tiende a crecer en los valores más altos y persisten algunos casos particulares que influyen demasiado en los resultados. Aun así, el modelo sigue siendo útil para explicar el precio de las viviendas, aunque podría mejorarse aplicando ajustes como transformar la variable del precio o usar métodos que sean más resistentes a estas irregularidades.
# Validación de supuestos para aptos
par(mfrow = c(2, 2))
plot(modelo_sin)
En conclusión, el modelo aplicado a los apartamentos resulta útil porque logra explicar de manera general cómo influyen el área construida, el número de habitaciones, baños y parqueaderos en el precio. Sin embargo, al revisar los gráficos se ve que todavía quedan ciertos problemas: los resultados no se ajustan del todo a lo esperado y la variación de los errores aumenta cuando sube el precio estimado. Además, aparecen algunos apartamentos que tienen un peso demasiado grande en los cálculos y que podrían estar distorsionando los resultados. Esto significa que, aunque el modelo funciona, todavía se puede mejorar, ya sea ajustando los datos (por ejemplo, transformando algunas variables), agregando más información que explique mejor el precio o usando métodos que reduzcan el impacto de esos valores atípicos. Con estos ajustes, el modelo sería más preciso y confiable para entender y predecir los precios de los apartamentos.
# Definir características de Vivienda 1 (Casa en el norte)
vivienda_1 <- data.frame(
areaconst = 200,
banios = 2,
habitaciones = 4,
parqueaderos = 1
)
# Predicción de precio usando el modelo limpio
prediccion_1 <- predict(modelo_casas_limpio, vivienda_1)
print(paste("Precio estimado de la Vivienda 1:", round(prediccion_1, 2), "millones"))
## [1] "Precio estimado de la Vivienda 1: 271.82 millones"
Filtraremos las ofertas para la zona norte cumpliendo con las especificaciones dadas por el cliente (precio ≤ 350 millones, área ≥ 200 m², estrato 4 o 5, al menos 2 baños, 4 habitaciones y 1 parqueadero)
# Filtrar opciones en la base
opciones_norte <- casas_norte_limpia %>%
filter(preciom <= 350,
areaconst >= 200,
estrato %in% c(4,5),
banios >= 2,
habitaciones >= 4,
parqueaderos >= 1)
# Mostrar número de opciones
print(paste("Número de opciones disponibles en zona norte:", nrow(opciones_norte)))
## [1] "Número de opciones disponibles en zona norte: 30"
El cliente dispone de 30 opciones, representadas en el siguiente mapa. Al hacer clic en el punto de interés, se mostrarán las especificaciones correspondientes a cada casa
leaflet(opciones_norte) %>%
addTiles() %>%
addMarkers(lng = ~longitud, lat = ~latitud,
popup = ~paste("Precio:", preciom, "millones",
"<br>Área:", areaconst, "m²",
"<br>Habitaciones:", habitaciones,
"<br>Baños:", banios,
"<br>Parqueaderos:", parqueaderos,
"<br>Barrio:", barrio))
# Definir características de Vivienda 2 (Apartamento en el sur)
vivienda_2 <- data.frame(
areaconst = 300,
banios = 3,
habitaciones = 5,
parqueaderos = 3
)
# Predicción de precio con el modelo limpio de apartamentos
prediccion_2 <- predict(modelo_sin, vivienda_2)
print(paste("Precio estimado de la Vivienda 2:", round(prediccion_2, 2), "millones"))
## [1] "Precio estimado de la Vivienda 2: 913.17 millones"
Se filtran las ofertas para los apartamentos en la zona sur regun requemimientos del cliente (precio ≤ 850 millones, área ≥ 300 m², estrato 5 o 6, al menos 3 baños, 5 habitaciones y 3 parqueaderos).
# Filtrar opciones en la base
opciones_sur <- aptos_sur_sin_outliers %>%
filter(preciom <= 850,
areaconst >= 300,
estrato %in% c(5,6),
banios >= 3,
habitaciones >= 5,
parqueaderos >= 3)
# Mostrar número de opciones
print(paste("Número de opciones disponibles en zona sur:", nrow(opciones_sur)))
## [1] "Número de opciones disponibles en zona sur: 0"
En el caso del apartamento solicitado, con un área construida de 300 m², cinco habitaciones, tres baños, tres parqueaderos, ubicado en la zona sur y en estratos 5 o 6, con un presupuesto máximo de 850 millones, se encontró que no existen opciones disponibles en la base de datos que cumplan simultáneamente con todas estas características.
Este resultado sugiere que, en la zona sur, los apartamentos con dichas especificaciones suelen superar el rango de precio establecido. Por lo tanto, se plantean dos alternativas: 1. flexibilizar algunos de los criterios, por ejemplo considerar áreas ligeramente menores (a partir de 250 m²) o un número reducido de habitaciones (cuatro en lugar de cinco), lo que permitiría encontrar opciones dentro del presupuesto aprobado; o 2. mantener todas las condiciones definidas, en cuyo caso sería necesario ampliar el presupuesto, dado que los inmuebles con estas características tienden a superar los 850 millones.
De esta forma, el análisis permite orientar la búsqueda futura hacia alternativas realistas y alineadas con las posibilidades financieras del cliente.
El análisis realizado sobre las viviendas de la zona norte y los apartamentos de la zona sur permite obtener una visión integral del comportamiento del mercado inmobiliario en ambas áreas.
En primer lugar, se evidenció que tanto en casas como en apartamentos el precio se relaciona de manera positiva y significativa con el área construida, siendo esta la variable más determinante en la explicación de su valor de mercado. Asimismo, factores como el número de baños y parqueaderos refuerzan la explicación del precio, mientras que el número de habitaciones mostró una menor relevancia, e incluso un efecto negativo cuando se mantienen constantes las demás variables.
En el caso de las casas (zona norte), los modelos de regresión lineal múltiple mostraron que la exclusión de valores atípicos no tuvo un impacto relevante en el ajuste del modelo (R² ajustado estable en torno al 0,61). Esto indica que, aunque se detectaron casos inusuales, estos no distorsionan significativamente la capacidad explicativa del modelo, lo que sugiere que corresponden a observaciones reales y válidas en el mercado.
Por otro lado, en los apartamentos (zona sur), la exclusión de outliers sí generó una mejora sustancial en el desempeño del modelo (R² ajustado pasó de 0,78 a 0,86 y el error residual se redujo). Esto evidencia que algunos registros distorsionaban la relación entre variables, y que al depurarlos se obtiene un modelo más coherente con la lógica inmobiliaria, reforzando la influencia del área construida y moderando el efecto de baños y parqueaderos.
La validación de los supuestos estadísticos evidenció que ambos modelos son útiles para explicar el precio de los inmuebles; sin embargo, presentan ciertas limitaciones, como la presencia de heterocedasticidad y observaciones influyentes. Esto sugiere la conveniencia de considerar ajustes adicionales, ya sea mediante transformaciones de variables, incorporación de más factores explicativos (como ubicación, antigüedad del inmueble o acabados) o el uso de métodos más robustos frente a valores atípicos.
Finalmente, desde una perspectiva aplicada, el estudio permitió orientar la búsqueda de vivienda para el cliente. En el caso específico del apartamento solicitado en la zona sur (300 m², 5 habitaciones, 3 baños, 3 parqueaderos, presupuesto máximo de 850 millones), no se encontraron opciones disponibles en la base de datos que cumplieran simultáneamente con todas las condiciones. Esto llevó a plantear dos caminos: flexibilizar algunos criterios de búsqueda (por ejemplo, área ligeramente menor o reducción en el número de habitaciones) o ampliar el presupuesto para ajustarse a la realidad del mercado.
En conjunto, el análisis confirma que el precio de los inmuebles en Cali está fuertemente condicionado por el área construida y las características complementarias, pero también que el mercado presenta dispersión importante y casos atípicos que deben tratarse cuidadosamente para obtener modelos estadísticos más estables y representativos.
Codigo utilizado para limpieza y geolocalizacion de datos
# librerías necesarias
library(paqueteMODELOS)
library(tidyverse)
library(ggplot2)
library(plotly)
library(sf)
library(leaflet)
library(GGally)
library(car)
library(dplyr)
library(summarytools)
library(gridExtra)
library(PerformanceAnalytics)
library(pander)
library(stringi)
library(gt)
library(RColorBrewer)
library(cluster)
library(psych)
library(knitr)
library(kableExtra)
# carga de datos
devtools::install_github(“centromagis/paqueteMODELOS”, force =TRUE)
data(“vivienda”)
#ELIMINAR DUPLICADOS
vivienda <- vivienda %>% distinct()
# Eliminar filas con NA en la variable id
vivienda <- vivienda %>%
filter(!is.na(id))
# Calcular la mediana de parqueaderos por tipo de vivienda
mediana_parqueaderos <- vivienda %>%
filter(tipo %in% c(“Casa”, “Apartamento”)) %>%
mutate(parqueaderos = as.numeric(as.character(parqueaderos))) %>%
group_by(tipo) %>%
summarise(mediana_parq = median(parqueaderos, na.rm = TRUE))
# Imputar valores NA en parqueaderos según la mediana por tipo
vivienda <- vivienda %>%
mutate(parqueaderos = as.numeric(as.character(parqueaderos))) %>%
mutate(parqueaderos = ifelse(
is.na(parqueaderos) & tipo == “Casa”,
mediana_parqueaderos$mediana_parq[mediana_parqueaderos$tipo == “Casa”],
parqueaderos)) %>%
mutate(parqueaderos = ifelse(
is.na(parqueaderos) & tipo == “Apartamento”,
mediana_parqueaderos$mediana_parq[mediana_parqueaderos$tipo == “Apartamento”],
parqueaderos))
# se elimina la variable piso pues no sera necesaria para nuestro analisis
vivienda_final <- vivienda %>% select(-piso)
# Convertir la variable estrato y id en factor
vivienda_final$estrato <- as.character(vivienda_final$estrato)
vivienda_final$id <- as.character(vivienda_final$id)
#estandarizar barrios
vivienda_final <- vivienda_final %>%
mutate(barrio = barrio %>%
# todo minúscula
tolower() %>%
# quitar espacios al inicio/final
trimws() %>%
# normalizar caracteres acentuados y tildes
stringi::stri_trans_general(“Latin-ASCII”) %>%
# reemplazar dobles espacios
gsub(“\\s+”, ” “, .)
)
# Paso 1: limpiar codificación
vivienda_final <- vivienda_final %>%
mutate(barrio = barrio %>%
tolower() %>%
trimws() %>%
stri_trans_general(“Latin-ASCII”) %>%
gsub(“\\s+”, ” “, .)
)
# Paso 2: unificar nombres equivalentes
vivienda_final <- vivienda_final %>%
mutate(barrio = case_when(
barrio %in% c(“agua blanca”, “aguablanca”) ~ “aguablanca”,
barrio %in% c(“alferez real”, “alferez real i”, “hacienda alferez real”,“alf√(c)rez real”) ~ “alferez real”,
barrio %in% c(“alfonso lopez”, “alfonso lopez i”) ~ “alfonso lopez”,
barrio %in% c(“ciudad jardin”, “ciudad jardin pance”) ~ “ciudad jardin”,
barrio %in% c(“ciudad melendez”, “ciudad melendez”, “ciudad melendez”,“mel√(c)ndez”) ~ “ciudad melendez”,
barrio %in% c(“melendez”, “melendez”) ~ “melendez”,
barrio %in% c(“valle de lili”, “valle del lili”) ~ “valle del lili”,
barrio %in% c(“pampa linda”, “pampalinda”) ~ “pampalinda”,
barrio %in% c(“santa fe”, “santafe”) ~ “santa fe”,
barrio %in% c(“tequendama”, “tequendema”) ~ “tequendama”,
barrio %in% c(“siete de agosto”, “7 de agosto”, “barrio 7de agosto”) ~ “siete de agosto”,
barrio %in% c(“nueva base”, “la nueva base”) ~ “nueva base”,
barrio %in% c(“flora”, “la flora”, “laflora”, “norte la flora”, “urbanizacion la flora”) ~ “la flora”,
barrio %in% c(“el ingenio”, “ingenio”, “el ingenio i”, “el ingenio ii”, “el ingenio iii”, “el ingenio 3”) ~ “el ingenio”,
TRUE ~ barrio
))
vivienda_final <- vivienda_final %>%
mutate(barrio = case_when(
barrio == “base a√(c)rea” ~ “base aerea”,
barrio == “cali canto” ~ “calicanto”,
barrio == “ciudad mel√(c)ndez” ~ “ciudad melendez”,
barrio == “el caney” ~ “caney”,
barrio == “el tr√(c)bol” ~ “el trebol”,
barrio == “jamundi alfaguara” ~ “jamundi”,
barrio == “juanamb√∫” ~ “juanambu”,
barrio == “las am√(c)ricas” ~ “las americas”,
TRUE ~ barrio
))
#geolocalizacion
#validacion de coordenadas
df <- vivienda_final %>%
mutate(
latitud = as.numeric(latitud),
longitud = as.numeric(longitud)
) %>%
filter(!is.na(latitud), !is.na(longitud))
# (Opcional pero útil) Filtrar a un bbox aproximado de Cali para limpiar outliers
bbox_cali <- list(lat_min = 3.25, lat_max = 3.60, lon_min = -76.70, lon_max = -76.30)
df <- df %>%
filter(
dplyr::between(latitud, bbox_cali$lat_min, bbox_cali$lat_max),
dplyr::between(longitud, bbox_cali$lon_min, bbox_cali$lon_max)
)
# Crear objeto sf en WGS84
pts_wgs84 <- st_as_sf(df, coords = c(“longitud”, “latitud”), crs = 4326, remove = FALSE)
# Transformar a UTM 18N (metros)
pts_utm <- st_transform(pts_wgs84, 32618)
# Extraer coordenadas en metros (Easting, Northing) y guardarlas en el data frame
coords_m <- st_coordinates(pts_utm)
df <- df %>%
mutate(x_m = coords_m[,1], # Este-Oeste
y_m = coords_m[,2]) # Norte-Sur
set.seed(123) # Reproducible
km <- kmeans(df[, c(“x_m”,“y_m”)], centers = 5, nstart = 50)
df$zona_cluster <- km$cluster
centroides <- df %>%
group_by(zona_cluster) %>%
summarise(lat_media = mean(latitud),
lon_media = mean(longitud),
.groups = “drop”)
# Crear diccionario de correspondencia
cluster_a_zona <- centroides %>%
arrange(lat_media) %>% # de menor a mayor latitud
mutate(zona_nombre = case_when(
row_number() == 1 ~ “Zona Sur”,
row_number() == 2 ~ “Zona Centro”,
row_number() == 3 ~ “Zona Oriente”,
row_number() == 4 ~ “Zona Occidente”,
row_number() == 5 ~ “Zona Norte”
)) %>%
select(zona_cluster, zona_nombre) %>%
distinct(zona_cluster, .keep_all = TRUE) # 👈 clave para evitar duplicados
# Unir diccionario
f <- df %>%
left_join(cluster_a_zona, by = “zona_cluster”) %>%
rename(zona_cluster_nombre = zona_nombre)
# Diccionario fijo
cluster_a_zona <- tibble(
zona_cluster = c(1, 2, 3, 4, 5),
zona_cluster_nombre = c(“Zona Oriente”, “Zona Centro”, “Zona Occidente”, “Zona Sur”, “Zona Norte”)
)
# Asegurar tipos
df$zona_cluster <- as.integer(df$zona_cluster)
cluster_a_zona$zona_cluster <- as.integer(cluster_a_zona$zona_cluster)
# Unir diccionario
df <- df %>%
left_join(cluster_a_zona, by = “zona_cluster”)
ggplot(df, aes(x = longitud, y = latitud, color = zona_cluster_nombre)) +
geom_point(alpha = 0.7, size = 2) +
scale_color_brewer(palette = “Set1”) + # colores categóricos
theme_minimal() +
labs(
title = “Zonas geográficas según cluster (fijo)”,
x = “Longitud”,
y = “Latitud”,
color = “Zona”
)
viviendas_geo <- df