1 Introducción

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.

2 Metodología

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,:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

3 Análisis exploratorio de datos (EDA)

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

3.1 Vivienda 1 (Casa-Zona Norte)

casas_norte <- viviendas_geo %>%
  filter(tipo == "Casa" & zona_cluster_nombre == "Zona Norte")

3.1.1 Mapa

leaflet(casas_norte) %>%
  addTiles() %>%  # mapa base
  addMarkers(
    lng = ~longitud,
    lat = ~latitud,
    popup = ~paste("Precio:", preciom, "millones")
  )

3.1.2 Resumen de datos

#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]
1. 3
2. 4
3. 5
4. 6
194(37.0%)
110(21.0%)
199(37.9%)
22(4.2%)
525 (100.0%) 0 (0.0%)
2 preciom [numeric]
Mean (sd) : 402.3 (244)
min ≤ med ≤ max:
100 ≤ 350 ≤ 1650
IQR (CV) : 270 (0.6)
138 distinct values 525 (100.0%) 0 (0.0%)
3 areaconst [numeric]
Mean (sd) : 244 (165.5)
min ≤ med ≤ max:
30 ≤ 219 ≤ 1500
IQR (CV) : 198 (0.7)
204 distinct values 525 (100.0%) 0 (0.0%)
4 parqueaderos [numeric]
Mean (sd) : 2.1 (1.2)
min ≤ med ≤ max:
1 ≤ 2 ≤ 10
IQR (CV) : 1 (0.6)
1:132(25.1%)
2:311(59.2%)
3:31(5.9%)
4:28(5.3%)
5:9(1.7%)
6:8(1.5%)
7:2(0.4%)
8:2(0.4%)
9:1(0.2%)
10:1(0.2%)
525 (100.0%) 0 (0.0%)
5 banios [numeric]
Mean (sd) : 3.4 (1.4)
min ≤ med ≤ max:
0 ≤ 3 ≤ 8
IQR (CV) : 2 (0.4)
0:4(0.8%)
1:15(2.9%)
2:131(25.0%)
3:145(27.6%)
4:123(23.4%)
5:65(12.4%)
6:32(6.1%)
7:5(1.0%)
8:5(1.0%)
525 (100.0%) 0 (0.0%)
6 habitaciones [numeric]
Mean (sd) : 4.5 (1.7)
min ≤ med ≤ max:
0 ≤ 4 ≤ 10
IQR (CV) : 2 (0.4)
0:5(1.0%)
2:12(2.3%)
3:130(24.8%)
4:186(35.4%)
5:85(16.2%)
6:38(7.2%)
7:28(5.3%)
8:24(4.6%)
9:8(1.5%)
10:9(1.7%)
525 (100.0%) 0 (0.0%)
7 barrio [character]
1. la flora
2. villa del prado
3. el bosque
4. vipasa
5. san vicente
6. la merced
7. prados del norte
8. brisas de los
9. salomia
10. zona norte
[ 84 others ]
101(19.2%)
35(6.7%)
29(5.5%)
28(5.3%)
25(4.8%)
23(4.4%)
22(4.2%)
16(3.0%)
16(3.0%)
16(3.0%)
214(40.8%)
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

3.1.3 Boxplots

# 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

3.1.4 Diagrama de dispersión

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.

3.1.5 Relacion entre variables

# 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.

3.2 Vivienda 2 (Apartamento-Zona Sur)

aptos_sur <- viviendas_geo %>%
  filter(tipo == "Apartamento" & zona_cluster_nombre == "Zona Sur")

3.2.1 Mapa

leaflet(aptos_sur) %>%
  addTiles() %>%  # mapa base
  addMarkers(
    lng = ~longitud,
    lat = ~latitud,
    popup = ~paste("Precio:", preciom, "millones")
  )

3.2.2 Resumen

#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]
1. 3
2. 4
3. 5
4. 6
58(3.6%)
606(37.1%)
553(33.9%)
415(25.4%)
1632 (100.0%) 0 (0.0%)
2 preciom [numeric]
Mean (sd) : 329.4 (222.2)
min ≤ med ≤ max:
78 ≤ 260 ≤ 1750
IQR (CV) : 190 (0.7)
281 distinct values 1632 (100.0%) 0 (0.0%)
3 areaconst [numeric]
Mean (sd) : 99.8 (56.9)
min ≤ med ≤ max:
40 ≤ 85 ≤ 932
IQR (CV) : 50 (0.6)
217 distinct values 1632 (100.0%) 0 (0.0%)
4 parqueaderos [numeric]
Mean (sd) : 1.4 (0.6)
min ≤ med ≤ max:
1 ≤ 1 ≤ 4
IQR (CV) : 1 (0.5)
1:1096(67.2%)
2:447(27.4%)
3:61(3.7%)
4:28(1.7%)
1632 (100.0%) 0 (0.0%)
5 banios [numeric]
Mean (sd) : 2.6 (1)
min ≤ med ≤ max:
0 ≤ 2 ≤ 7
IQR (CV) : 1 (0.4)
0:3(0.2%)
1:54(3.3%)
2:932(57.1%)
3:380(23.3%)
4:159(9.7%)
5:95(5.8%)
6:8(0.5%)
7:1(0.1%)
1632 (100.0%) 0 (0.0%)
6 habitaciones [numeric]
Mean (sd) : 2.9 (0.6)
min ≤ med ≤ max:
0 ≤ 3 ≤ 6
IQR (CV) : 0 (0.2)
0:4(0.2%)
1:9(0.6%)
2:324(19.9%)
3:1098(67.3%)
4:189(11.6%)
5:7(0.4%)
6:1(0.1%)
1632 (100.0%) 0 (0.0%)
7 barrio [character]
1. valle del lili
2. ciudad jardin
3. pance
4. caney
5. el ingenio
6. acopi
7. melendez
8. mayapan las vegas
9. multicentro
10. parcelaciones pance
[ 36 others ]
704(43.1%)
210(12.9%)
176(10.8%)
167(10.2%)
139(8.5%)
40(2.5%)
33(2.0%)
21(1.3%)
21(1.3%)
17(1.0%)
104(6.4%)
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

3.2.3 Boxplots

# 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

3.2.4 Diagrama de dispersión

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.

3.2.5 Relacion entre variables

# 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.

4 Estimación de modelos de regresión lineal múltiple (Detección y tratamiento de valores atípicos)

4.1 Vivienda 1 (Casa-Zona Norte)

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")
Outliers detectados en Casas Norte
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")
Outliers: Baños desproporcionados respecto al área en Casas Norte
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)
Comparación entre modelo original y modelo limpio
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.

4.2 Vivienda 2 (Apartamento-Zona Sur)

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")
Outliers detectados en Apartamentos Sur
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")
Outliers: Baños desproporcionados respecto al área en Apartamentos Sur
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")
Outliers finales (coincidencia de criterios)
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")
Comparación de coeficientes: Modelo con vs sin outliers finales
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")
Comparación de desempeño de los modelos
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.

5 Validación de supuestos del modelo

5.1 Vivienda 1 (Casa-Zona Norte)

# 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.

5.2 Vivienda 2 (Apartamento-Zona Sur)

# 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.

6 Predicciones y Recomendaciones

6.1 Vivienda 1 (Casa-Zona Norte)

6.1.1 Predicción de precio estimado

# 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"

6.1.2 Opciones para vivienda 1

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

6.1.3 Mapa

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))

6.2 Vivienda 2 (Apartamento-Zona Sur)

6.2.1 Predicción de precio estimado

# 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"

6.2.2 Opciones para vivienda 2

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.

7 Conclusiones

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.

8 Anexos

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