1. Introducción

En esta actividad se realiza un análisis estadístico multidimensional para identificar patrones, relaciones y segmentaciones relevantes sobre una base de datos que contiene información sobre diversas propiedades residenciales disponibles en el mercado. A partir del análisis realizado se pretende ofrecer a una empresa inmobiliaria orientación sobre la toma de decisiones relacionadas a la compra, venta y valoración de las propiedades. En este proceso se aplicarán técnicas como Análisis de Componentes Principales, Análisis de Conglomerados y Análisis de Correspondencia, de los cuales se espera obtener información que proporcione ventajas competitivas de optimización e inversión en la dinámica del mercado inmobiliario en la ciudad.


2. Análisis Exploratorio de Datos y Preprocesamiento

En este caso utilizaremos la base de datos vivienda, la cual es importada desde paqueteMODELOS. La siguiente es una muestra aleatoria de la base de datos.

muestra <- vivienda %>% sample_n(5)
kable(muestra, "html") %>%
  kable_styling("striped", full_width = FALSE) %>%
  row_spec(0, bold = TRUE)
id zona piso estrato preciom areaconst parqueaderos banios habitaciones tipo barrio longitud latitud
5783 Zona Sur 04 5 240 87 1 3 3 Apartamento multicentro -76.53821 3.37845
744 Zona Norte 04 4 158 77 1 2 3 Apartamento prados del norte -76.50227 3.40102
6633 Zona Sur 02 6 365 108 2 3 3 Apartamento ciudad jardín -76.54387 3.35730
2410 Zona Norte 11 5 390 110 2 3 3 Apartamento la flora -76.52003 3.48960
5878 Zona Sur 04 6 410 120 2 3 3 Apartamento ciudad jardín -76.53900 3.36400

Se importó un dataframe con 13 variables: 3 cualitativas, 9 cuantitativas y 1 como identificador del registro. Las tres variables cualitativas son nominales, y la variable identificador puede ser descartada para el análisis.

Inicialmente el dataframe cuenta con 8322 registros.

Iniciamos haciendo un conteo de los datos faltantes en todo el dataframe y los agrupamos para cada variable

faltantes <- colSums(is.na(vivienda)) %>%
                 as.data.frame() 
conteo <- kable(faltantes, "html", caption = "Conteo de faltantes") %>%
  kable_styling("striped", full_width = FALSE) %>%
  row_spec(0, bold = TRUE)
conteo
Conteo de faltantes
.
id 3
zona 3
piso 2638
estrato 3
preciom 2
areaconst 3
parqueaderos 1605
banios 3
habitaciones 3
tipo 3
barrio 3
longitud 3
latitud 3
gg_miss_var(vivienda) 

A partir de la tabla conteo de faltantes podemos identificar que hay 3 registros que no presentan información, los cuales serán eliminados de la base de datos. Los atributos piso y paqueadero son los que presentan datos faltantes, los cuales serán imputados.

viviendas_1 <- subset(vivienda, !is.na(id))
  1. Variable Piso

Primero analizaremos la información de la variable piso para determinar como realizar la imputación.

viviendas_1$piso <- as.integer(viviendas_1$piso)
resumen_piso <- summarytools::descr(viviendas_1$piso)
kable(resumen_piso, "html", caption = "Resumen de Piso") %>%
  kable_styling("striped", full_width = FALSE) %>%
  row_spec(0, bold = TRUE)
Resumen de Piso
piso
Mean 3.7709360
Std.Dev 2.6148024
Min 1.0000000
Q1 2.0000000
Median 3.0000000
Q3 5.0000000
Max 12.0000000
MAD 1.4826000
IQR 3.0000000
CV 0.6934094
Skewness 1.2795930
SE.Skewness 0.0324813
Kurtosis 1.0542476
N.Valid 5684.0000000
Pct.Valid 68.3255199
par(mfrow = c(1, 2))

histograma <- hist(viviendas_1$piso, 
  las=1,
  main = "Distribucion de Piso",
  xlab="Piso",
  ylab = "Frecuencia",
  col =c("lightblue")
)

box <- boxplot(viviendas_1$piso,  
  names = "Piso", 
  main = "Diagrama de Caja - Piso", 
  col = c("lightblue"), 
  xlab="Piso",
  ylab = "Valores",
  border = "black"
)

Al identificar que los datos atípicos tienen una frecuencia relevante, y que no hay un dato específico que pueda reemplazar los valores nulos, se imputarán los datos a partir de comparaciones de las variables zona, estrato, banios, habitaciones y tipo.

Este proceso se realizará creando dos dataframe, uno con los registros que tienen datos de piso y otro con registro que no tienen datos de piso. Se utiliza la función left_join para comprar cada uno de estos dataframes en las variables zona, estrato, banios, habitaciones y tipo, generando un nuevo dataframe con los registros que coincidan en estas variables. Ya que el dataframe de coincidencias es many-to-many, en la comparación se obtendrán varios registros para cada registro comparado, por lo que se deberá realizar otra validación adicional. Para la validación adicional se utiliza la variable preciom, ya que al estudiar la correlación de ésta variable con la variable piso, se presenta una menor correlación negativa que la que hay entre piso y areaconst, que era la otra opción a tener en cuenta para una segunda comparación.

cor.test(viviendas_1$piso, viviendas_1$areaconst)
## 
##  Pearson's product-moment correlation
## 
## data:  viviendas_1$piso and viviendas_1$areaconst
## t = -16.021, df = 5682, p-value < 0.00000000000000022
## alternative hypothesis: true correlation is not equal to 0
## 95 percent confidence interval:
##  -0.2326420 -0.1828922
## sample estimates:
##        cor 
## -0.2079016
cor.test(viviendas_1$piso, viviendas_1$preciom)
## 
##  Pearson's product-moment correlation
## 
## data:  viviendas_1$piso and viviendas_1$preciom
## t = -1.1103, df = 5682, p-value = 0.2669
## alternative hypothesis: true correlation is not equal to 0
## 95 percent confidence interval:
##  -0.04071050  0.01127397
## sample estimates:
##         cor 
## -0.01472822
viviendas_1_sinpiso <- subset(viviendas_1, is.na(piso))
viviendas_1_conpiso <- subset(viviendas_1, !is.na(piso))
matched_rows <- left_join(viviendas_1_sinpiso, viviendas_1_conpiso, by = c("zona", "estrato", "banios", "habitaciones", "tipo"))
no_pisos <- viviendas_1_sinpiso
for (i in 1:nrow(no_pisos)) {
  row <- no_pisos[i, ]
  matched <- matched_rows[matched_rows$id.x==row$id,]
  if (nrow(matched)>0) {
    matched$diferencia = 0
    for (j in 1:nrow(matched)) {
      row_match <- matched[j, ]
      diff <- abs(row$preciom-row_match$preciom.y)
      matched[j, ]$diferencia = diff
    }
    final_match <- matched[matched$diferencia==min(matched$diferencia),]
    piso = final_match[1,]$piso.y
    no_pisos[i, ]$piso = piso
  } 
}

En la comparación con la variable preciom, se toma el registro que tenga mayor cercanía en el dato de precio en el registro sin información de piso, y así se obtiene un dato de piso aproximado.

nuevos_pisos <- subset(no_pisos,!is.na(piso))
for (i in 1:nrow(nuevos_pisos)) {
    row <- nuevos_pisos[i, ]
    viviendas_1[viviendas_1$id==row$id,]$piso = row$piso
}
viviendas_2 <- viviendas_1[!is.na(viviendas_1$piso),]

Los registro que no lograron imputarse en la comparación son eliminados, dejando una nueva base de datos con 8126 registros.

  1. Variable Parqueadero

El proceso de imputación de datos para la variable parquea es idéntico al proceso utilizado en la imputación de datos de la variable piso.

resumen_parquea <- summarytools::descr(viviendas_2$parqueaderos)
kable(resumen_parquea, "html", caption = "Resumen de Piso") %>%
  kable_styling("striped", full_width = FALSE) %>%
  row_spec(0, bold = TRUE)
Resumen de Piso
parqueaderos
Mean 1.8314471
Std.Dev 1.1158874
Min 1.0000000
Q1 1.0000000
Median 2.0000000
Q3 2.0000000
Max 10.0000000
MAD 1.4826000
IQR 1.0000000
CV 0.6092927
Skewness 2.3056212
SE.Skewness 0.0300828
Kurtosis 8.1627126
N.Valid 6627.0000000
Pct.Valid 81.5530396
par(mfrow = c(1, 2))

histograma <- hist(viviendas_2$parqueaderos, 
  las=1,
  main = "Distribucion de Parqueadero",
  xlab="Piso",
  ylab = "Frecuencia",
  col =c("lightblue")
)

box <- boxplot(viviendas_2$parqueaderos,  
  names = "Piso", 
  main = "Diagrama de Caja - Parqueadero", 
  col = c("lightblue"), 
  xlab="Parqueadero",
  ylab = "Valores",
  border = "black"
)

Aquí se realiza el mismo proceso de crear dos dataframe, uno con los registro que tiene datos de parqueadero y otro con los que no tienen datos de parqueadero, validando coincidencias en las mismas variables: zona, estrato, banios, habitac y tipo.

De igual manera la validación adicional se hace con la variable preciom, que en este caso sí presenta una mayor correlación con la variable parquea, en comparación a la correlación que hay entre parquea y areaconst.

cor.test(viviendas_2$parqueaderos, viviendas_2$areaconst)
## 
##  Pearson's product-moment correlation
## 
## data:  viviendas_2$parqueaderos and viviendas_2$areaconst
## t = 59.544, df = 6625, p-value < 0.00000000000000022
## alternative hypothesis: true correlation is not equal to 0
## 95 percent confidence interval:
##  0.5745186 0.6058923
## sample estimates:
##       cor 
## 0.5904284
cor.test(viviendas_2$parqueaderos, viviendas_2$preciom)
## 
##  Pearson's product-moment correlation
## 
## data:  viviendas_2$parqueaderos and viviendas_2$preciom
## t = 77.978, df = 6625, p-value < 0.00000000000000022
## alternative hypothesis: true correlation is not equal to 0
## 95 percent confidence interval:
##  0.6790262 0.7041418
## sample estimates:
##       cor 
## 0.6917932
viviendas_2_sinparque <- subset(viviendas_2, is.na(parqueaderos))
viviendas_2_conparque <- subset(viviendas_2, !is.na(parqueaderos))
matched_parquea <- left_join(viviendas_2_sinparque, viviendas_2_conparque, by = c("zona", "estrato", "banios", "habitaciones", "tipo"))
no_parquea <- viviendas_2_sinparque
for (i in 1:nrow(no_parquea)) {
  row <- no_parquea[i, ]
  matched_p <- matched_parquea[matched_parquea$id.x==row$id,]
  if (nrow(matched_p)>0) {
    matched_p$diferencia = 0
    for (j in 1:nrow(matched_p)) {
      row_match <- matched_p[j, ]
      diff <- abs(row$preciom-row_match$preciom.y)
      matched_p[j, ]$diferencia = diff
    }
    final_match <- matched_p[matched_p$diferencia==min(matched_p$diferencia),]
    parqueaderos = final_match[1,]$parqueaderos.y
    no_parquea[i, ]$parqueaderos = parqueaderos
  } 
}

Igualmente se toma el registro que tenga mayor cercanía en el dato de precio en el registro sin información de parqueadero para obtener el dato de parqueaderos.

nuevos_parquea <- subset(no_parquea,!is.na(parqueaderos))
for (i in 1:nrow(nuevos_parquea)) {
    row <- nuevos_parquea[i, ]
    viviendas_2[viviendas_2$id==row$id,]$parqueaderos = row$parqueaderos
}
viviendas_3 <- viviendas_2[!is.na(viviendas_2$parqueaderos),]

Se eliminan los registros sin datos de parqueadero obteniendo una base de datos final con 8013 registros completos.

Las tres variables categóricas están en escala nominal. La variable zona, tipo y barrio se pueden resumir en las siguientes tablas de frecuencia.

viviendas_4 <- viviendas_3
zonas_freq <- viviendas_4 %>%
  group_by(zona) %>%
  summarize(conteo = n()) %>%
  arrange(desc(conteo))
zonas_freq$freq_rel <- zonas_freq$conteo / sum(zonas_freq$conteo)

tipo_freq <- viviendas_4 %>%
  group_by(tipo) %>%
  summarize(conteo = n()) %>%
  arrange(desc(conteo))
tipo_freq$freq_rel <- tipo_freq$conteo / sum(tipo_freq$conteo)

barrio_freq <- viviendas_4 %>%
  group_by(barrio) %>%
  summarize(conteo = n()) %>%
  arrange(desc(conteo))
barrio_freq$freq_rel <- barrio_freq$conteo / sum(barrio_freq$conteo)

kable(zonas_freq, "html", caption = "Frecuencias Zona") %>%
  kable_styling("striped", full_width = FALSE) %>%
  row_spec(0, bold = TRUE)
Frecuencias Zona
zona conteo freq_rel
Zona Sur 4653 0.5806814
Zona Norte 1809 0.2257581
Zona Oeste 1153 0.1438912
Zona Oriente 320 0.0399351
Zona Centro 78 0.0097342
kable(tipo_freq, "html", caption = "Frecuencias Tipo") %>%
  kable_styling("striped", full_width = FALSE) %>%
  row_spec(0, bold = TRUE)
Frecuencias Tipo
tipo conteo freq_rel
Apartamento 5026 0.6272308
Casa 2987 0.3727692
kable(head(barrio_freq, 15), "html", caption = "Frecuencia de barrios") %>%
  kable_styling("striped", full_width = FALSE) %>%
  row_spec(0, bold = TRUE)
Frecuencia de barrios
barrio conteo freq_rel
valle del lili 1007 0.1256708
ciudad jardín 513 0.0640210
pance 405 0.0505429
la flora 364 0.0454262
santa teresita 260 0.0324473
el caney 208 0.0259578
el ingenio 201 0.0250842
la hacienda 164 0.0204667
normandía 154 0.0192188
los cristales 150 0.0187196
el limonar 133 0.0165980
acopi 132 0.0164732
prados del norte 124 0.0154749
el refugio 118 0.0147261
aguacatal 102 0.0127293

Las frecuencias de esta variables también podemos observarlas gráficamente de la siguiente manera.

PieChart(zona, hole=0 ,values="%", data=viviendas_4, fill="blues", main="Porcentajes Zona", values_size=1)

PieChart(tipo, hole=0 ,values="%", data=viviendas_4, fill="blues", main="Porcentajes Tipo", values_size=1)

barrio_freq_1 <- viviendas_4 %>%
  group_by(barrio) %>%
  summarize(conteo = n()) %>%
  arrange(desc(conteo))
barrio_freq_2 <- barrio_freq_1[barrio_freq_1$conteo>70,]
barrio_freq_3 <- barrio_freq_1[barrio_freq_1$conteo<=70,]
barrio_freq_t <- rbind(barrio_freq_2, c("OTROS", sum(barrio_freq_3$conteo)))
barrio_freq_t$conteo <- as.integer(barrio_freq_t$conteo)


ggplot(barrio_freq_t, aes(x = barrio, y = conteo, fill = barrio)) +
geom_bar(stat = "identity", width = 0.7) +
geom_text(aes(label = conteo), vjust = -0.5, size = 3) + 
labs(title = "Distribucion por Barrios",
x = "Barrio",
y = "Conteo") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 90, vjust = 0.5, hjust=1)) +
theme(legend.position = "none") 

Claramente se puede observar que el mayor mercado inmobiliario se da en en la Zona Sur, específicamente en los barrios Valle del Lili y Ciudad Jardín, y el tipo vivienda más ofertada son los apartamentos con un 63% de los registros.

Ahora podemos analizar la variable preciom con la variable categórica tipo para comparar a detalle los valores de las viviendas para estas dos categorías.

ggplot(viviendas_4, aes(y=preciom, x=tipo))+
geom_jitter(color="#034A94", size=1, alpha=0.9) +
aes(color=paleta6)+
labs(title = "Tipo de vivienda - Precio de venta",
y= "Precio (millones)",
x= "Tipo")

ggplot(viviendas_4, aes(x = tipo, y = preciom, fill = tipo)) +
geom_boxplot() +
labs(title = "Distribucion deprecio por tipo de vivienda",
x = "Tipo",
y = "Precio (millones)") +
scale_fill_manual(values = c("#f4d35e", "#ee964b")) +
theme_minimal()

Podemos hacer el mismo procedimiento para observar la variable preciom con la variable categórica zona.

ggplot(viviendas_4, aes(y=preciom, x=zona))+
geom_jitter(color="#034A94", size=1, alpha=0.9) +
aes(color=paleta6)+
labs(title = "Zona - Precio de venta",
y= "Precio (millones)",
x= "Zona")

ggplot(viviendas_4, aes(x = zona, y = preciom, fill = zona)) +
geom_boxplot() +
labs(title = "Distribucion del precio por zona",
x = "Zona",
y = "Precio (millones)") +
scale_fill_manual(values = c("#f95738","#ee964b", "#f4d35e", "#faf0ca", "#0d3b66")) +
theme_minimal()

Tomando la variable preciom como base de comparación, debido a que estamos en un análisis descriptivo de mercado, también sería de ayuda encontrar la relación que existe entre esta y las variables estrato, cantidad de baños, cantidad de habitaciones y cantidad de parqueaderos. Primero revisaremos la distribución de frecuencias de estas variables cuantitativas discretas.

hist_par = ggplot(viviendas_4, aes(x = parqueaderos)) +
geom_histogram(bins = 10, fill = "lightblue", color = "white", alpha = 1) +
labs(title = "Distribucion de Parqueaderos",
  x = "Parqueaderos",
  y = "Frecuencia") +
theme_minimal() +
facet_wrap(~ tipo) 

hist_ban = ggplot(viviendas_4, aes(x = banios)) +
geom_histogram(bins = 11, fill = "lightblue", color = "white", alpha = 1) +
labs(title = "Distribucion de Banios",
  x = "Banios",
  y = "Frecuencia") +
theme_minimal() +
facet_wrap(~ tipo) 

hist_habi = ggplot(viviendas_4, aes(x = habitaciones)) +
geom_histogram(bins = 11, fill = "lightblue", color = "white", alpha = 1) +
labs(title = "Distribucion de Habitaciones",
  x = "Habitaciones",
  y = "Frecuencia") +
theme_minimal() +
facet_wrap(~ tipo) 

hist_estra = ggplot(viviendas_4, aes(x = estrato)) +
geom_histogram(bins = 7, fill = "lightblue", color = "white", alpha = 1) +
labs(title = "Distribucion de Estrato",
  x = "Estrato",
  y = "Frecuencia") +
theme_minimal() +
facet_wrap(~ tipo) 

ggplotly(hist_par)
ggplotly(hist_ban)
ggplotly(hist_habi)
ggplotly(hist_estra)

También podemos ver la correlación que hay entre estas variables y la variable preciom referente al precio del inmueble.

viviendas_num <- subset(viviendas_4, select = c("preciom", "areaconst", "estrato", "parqueaderos", "banios", "habitaciones"))
matriz_cor <- cor(viviendas_num)
kable(matriz_cor, "html", caption = "Tabla de correlaciones de variables") %>%
  kable_styling("striped", full_width = FALSE) %>%
  row_spec(0, bold = TRUE)
Tabla de correlaciones de variables
preciom areaconst estrato parqueaderos banios habitaciones
preciom 1.0000000 0.6902987 0.6225161 0.6875771 0.6979994 0.2748459
areaconst 0.6902987 1.0000000 0.2923154 0.5764458 0.6701126 0.5370090
estrato 0.6225161 0.2923154 1.0000000 0.4400703 0.4615877 -0.0619810
parqueaderos 0.6875771 0.5764458 0.4400703 1.0000000 0.5792035 0.2773853
banios 0.6979994 0.6701126 0.4615877 0.5792035 1.0000000 0.5842138
habitaciones 0.2748459 0.5370090 -0.0619810 0.2773853 0.5842138 1.0000000

Es de destacar que exista correlación significativa entre el precio y el área construida. De igual forma es importante la correlación entre el precio y el estrato, el número de parqueaderos y el número de baños.

Ahora nos enfocamos en las variables área construida y precio.

ggplot(viviendas_4, aes(x = areaconst, y = preciom)) +
  geom_point(position = position_jitter(width = 0.2), color = "#034A94") +
  #facet_wrap(~ tipo) + 
  stat_smooth(method = "loess" , formula =y ~ x) +
  labs(title = "Dispercion precio - area - tipo", x = "Area Construida", y = "Precio (millones)") +
  theme(axis.text.x = element_text(angle = 90, vjust = 0.5, hjust=1)) +
  theme_minimal()

En la dispersión de datos área construída y precio, se observa en general un comportamiento natural de que a mayor área construida mayor precio de venta, y eso se puede ver en el crecimiento casi lineal de la mayoría de los registros. Pero también se ven datos atípicos de viviendas con precios bajos para áreas construidas grandes.

Para complementar el análisis segmentado de viviendas por zona, estrato y tipo, podemos revisar indicadores de centro de precio y área construída para cada zona, estrato y tipo.

calcular_moda <- function(x) {
  tabla_frec <- table(x)
  moda <- as.numeric(names(tabla_frec)[which.max(tabla_frec)])
  return(moda)
}

resumen <- viviendas_4 %>%
              group_by(zona, tipo, estrato) %>%
              summarize(promedio_precio  = mean(preciom),
                        mediana_precio = median(preciom),
                        moda_precio = calcular_moda(preciom),
                        promedio_area = mean(areaconst),
                        mediana_area = median(areaconst),
                        moda_area = calcular_moda(areaconst)) %>%
              arrange(zona)

#datatable(resumen) 
kable(resumen, "html", caption = "Indicadores de centro agrupados por zona - tipo - estrato") %>%
  kable_styling("striped", full_width = FALSE) %>%
  row_spec(0, bold = TRUE)
Indicadores de centro agrupados por zona - tipo - estrato
zona tipo estrato promedio_precio mediana_precio moda_precio promedio_area mediana_area moda_area
Zona Centro Apartamento 3 153.0000 120.0 120 86.95200 84.0 84
Zona Centro Apartamento 4 208.6667 166.0 150 99.83333 108.0 120
Zona Centro Casa 3 309.9394 287.5 350 196.52273 167.0 150
Zona Centro Casa 4 450.0000 450.0 450 228.00000 228.0 228
Zona Norte Apartamento 3 119.1329 119.0 120 60.23565 59.0 60
Zona Norte Apartamento 4 209.7300 190.0 160 82.70717 73.0 60
Zona Norte Apartamento 5 347.7647 320.0 320 112.28621 100.0 100
Zona Norte Apartamento 6 649.1429 592.5 950 176.77616 163.0 227
Zona Norte Casa 3 228.8929 200.0 170 155.37694 120.0 120
Zona Norte Casa 4 407.3517 380.0 330 250.14379 258.0 120
Zona Norte Casa 5 540.3106 480.0 350 322.13333 296.0 300
Zona Norte Casa 6 799.0000 780.0 850 378.71290 304.0 298
Zona Oeste Apartamento 3 149.8000 128.0 98 67.44000 60.0 60
Zona Oeste Apartamento 4 233.9286 175.0 165 85.99911 66.5 61
Zona Oeste Apartamento 5 494.8261 420.5 350 142.31443 117.5 110
Zona Oeste Apartamento 6 780.6144 655.0 1400 193.06158 180.0 125
Zona Oeste Casa 3 487.2857 400.0 299 252.44357 256.5 55
Zona Oeste Casa 4 451.5500 395.0 395 285.10000 268.0 114
Zona Oeste Casa 5 693.2857 650.0 1200 368.70408 300.0 487
Zona Oeste Casa 6 1012.3922 920.0 1200 383.58235 370.0 400
Zona Oriente Apartamento 3 114.8163 113.0 113 80.69265 62.0 60
Zona Oriente Apartamento 4 262.5000 262.5 240 87.00000 87.0 84
Zona Oriente Apartamento 5 105.0000 105.0 105 60.00000 60.0 60
Zona Oriente Casa 3 239.5918 230.0 350 212.82397 179.0 90
Zona Oriente Casa 4 265.0000 265.0 265 162.00000 162.0 162
Zona Sur Apartamento 3 138.6615 127.0 115 66.39169 60.0 60
Zona Sur Apartamento 4 203.1381 187.0 150 75.48757 70.0 60
Zona Sur Apartamento 5 292.2556 280.0 250 101.42870 90.0 90
Zona Sur Apartamento 6 593.6182 580.0 650 149.34848 136.0 130
Zona Sur Casa 3 291.8025 270.0 350 202.28025 196.0 200
Zona Sur Casa 4 387.2961 350.0 450 218.06529 200.0 200
Zona Sur Casa 5 527.6209 470.0 450 261.89950 240.0 300
Zona Sur Casa 6 995.6551 900.0 850 379.69162 330.0 300

Así pudimos ver diferentes indicadores y visualizaciones estadísticas del conjunto de datos sobre el comercio inmobiliario, caracterizando y resaltando comportamientos y valores desde una perspectiva de analitica descriptiva.

3. Análisis de Componentes Principales

Se realiza Análisis de Componentes Principales (PCA) para reducir la dimensionalidad del conjunto de datos, escogiendo los componentes que expliquen la mayor cantidad de varianza, lo cuales son una combinación lineal de las variables originales.

Se toman las variables numéricas del conjunto de datos que no tiene datos faltantes: piso, estrato, preciom, areaconst, parqueaderos, banios, habitaciones.

viviendas_5 <- viviendas_4 %>% select(piso,estrato,preciom,areaconst,parqueaderos,banios,habitaciones)

Es necesario realizar una normalización del conjunto de datos con el fin de evitar afectaciones o sesgos en las estimaciones.

viviendas_5_z= scale(viviendas_5)
kable(head(viviendas_5_z), "html") %>%
  kable_styling("striped", full_width = FALSE) %>%
  row_spec(0, bold = TRUE)
piso estrato preciom areaconst parqueaderos banios habitaciones
-0.2506099 -1.6307671 -0.5548732 -0.7231613 -0.6863173 -0.0701159 1.7996874
-1.0401272 -1.6307671 -0.3409172 -0.3665318 -0.6863173 -0.8035864 -0.4229011
-0.6453686 -1.6307671 -0.2492217 0.3467271 0.2285822 -0.8035864 0.3179617
-0.6453686 -0.6474473 -0.0963960 0.7746824 1.1434816 1.3968252 -0.4229011
-1.0401272 0.3358725 -0.5243081 -0.5805095 -0.6863173 -0.8035864 -0.4229011
-1.0401272 0.3358725 -0.5854384 -0.6019073 -0.6863173 -0.0701159 -0.4229011

Se realiza la estimación de los Componentes Principales.

pca_result <- prcomp(viviendas_5_z)
pca_scores <- as.data.frame(pca_result$rotation)
kable(pca_scores, "html") %>%
  kable_styling("striped", full_width = FALSE) %>%
  row_spec(0, bold = TRUE)
PC1 PC2 PC3 PC4 PC5 PC6 PC7
piso 0.0777397 -0.5737330 0.7916306 0.1818656 -0.0430778 0.0361065 0.0432842
estrato -0.3097066 -0.5373852 -0.2531937 -0.5561080 0.1106928 0.4627736 0.1245933
preciom -0.4677010 -0.2171763 -0.0903084 0.0718681 -0.2840432 -0.2562475 -0.7579091
areaconst -0.4462483 0.1854727 0.0580706 0.2096016 -0.6967934 0.2567932 0.4095039
parqueaderos -0.4196444 -0.1116545 -0.1763923 0.6686756 0.5477983 0.1316973 0.1255525
banios -0.4657343 0.0648996 0.1902629 -0.3306012 0.1993783 -0.6764841 0.3687818
habitaciones -0.2952062 0.5327319 0.4799708 -0.2283565 0.2825865 0.4218489 -0.2978588

En este caso se desarrollan algunas gráficas para explicar la estimación de los componentes principales y para determinar la cantidad de varianza explicada por cada uno.

fviz_eig(pca_result, addlabels = TRUE)

El primer componente principal explica el \(50.8\%\) de la varianza contenida en el conjunto de datos, y el segundo componente principal explica el \(19.9\%\) de la varianza, completando un total de \(70.7\%\) de varianza explicada entre los dos primeros componentes. Los dos primeros componentes principales explican la mayor parte de la variabilidad del conjunto de datos.

Ahora podemos ver la contribución de las variables en cada componente principal.

par(mfrow = c(1, 2))
fviz_contrib(pca_result, choice = "var", axes = 1, top = 10) # PC1

fviz_contrib(pca_result, choice = "var", axes = 2, top = 10) # PC1

kable(data.frame(get_pca_var(pca_result)$contrib[,1:3]), "html") %>%
  kable_styling("striped", full_width = FALSE) %>%
  row_spec(0, bold = TRUE)
Dim.1 Dim.2 Dim.3
piso 0.6043468 32.916959 62.6678964
estrato 9.5918157 28.878284 6.4107060
preciom 21.8744247 4.716554 0.8155599
areaconst 19.9137585 3.440012 0.3372197
parqueaderos 17.6101385 1.246672 3.1114232
banios 21.6908471 0.421196 3.6199987
habitaciones 8.7146687 28.380323 23.0371960

Las variables preciom, banios y areaconst son las que tienen un mayor porcentaje de contribución en el CP1, mientras que las variables piso, estrato y habitaciones presentan mayor influencia en el CP2. De esto se podría decir que las variables banios y areaconst afectan en mayor proporción la variabilidad de los precios de los inmuebles.

También podemos observar gráficas de contribución de las variables en el plano de dos dimensiones.

fviz_pca_var(pca_result,
col.var = "contrib", # Color by contributions to the PC
gradient.cols = c("#FF7F00",  "#034D94"),
repel = TRUE     # Avoid text overlapping
)

fviz_pca_ind(pca_result, geom.ind = "point", 
             col.ind = "#FF7256", 
             axes = c(1, 2), 
             pointsize = 1.5)

El gráfico en el plano de las dimensiones de los componentes principales nos permite validar la contribución de cada variable y la dirección de los vectores propios. El gráfico en el plano de las dimensiones de los componentes principales nos permite validar la contribución de cada variable y la dirección de los vectores propios. Se observa que las variable preciom, banios, areaconst y parqueaderos estan juntas, lo que significa que ejercen influencia en la variabilidad del mismo componente, en este caso el componente principal 1. La longitud del vector hace referencia a la significancia en la representación de la varianza en el componente principal.

precios <- rbind(viviendas_5[5654,], 
viviendas_5[283,])
precios <- as.data.frame(precios)
rownames(precios) = c("5654","283")

casos1 <- rbind(pca_result$x[5654,1:2],pca_result$x[283,1:2]) # CP1
rownames(casos1) = c("5654","283")
casos1 <- as.data.frame(casos1)

fviz_pca_ind(pca_result, col.ind = "#DEDEDE", gradient.cols = c("#00AFBB", "#E7B800", "#FC4E07")) +
geom_point(data = casos1, aes(x = PC1, y = PC2), color = c("red","#00AFBB"), size = 3)

Este gráfico permite observar la dirección en el plano: el punto azul representa el inmueble más barato en el conjunto de datos, mientras que el punto rojo representa el inmueble más costoso.

4. Análisis de Conglomerados

Por medio de este análisis se realizan agrupaciones de registros similares en función de sus características, con el fin de generar grupos homogéneos de registros dentro de la base de datos de ventas de inmuebles.

En este caso se realiza un análisis de conglomerados jerárquico aglomerativo con distancias euclidianas iniciando con cuatro agrupaciones \(k=4\), y se utiliza el mismo conjunto de datos normalizado que se utilizó en el análisis de componentes principales. Para realizar este análisis se seleccionan las variables: piso, estrato, preciom, areaconst, parqueaderos, banios y habitaciones.

viviendas_5_z = as.data.frame(viviendas_5_z)
dist <- dist(viviendas_5_z, method="euclidean")
# Cluster jerarquico 
hc <- hclust(dist, method='complete')
# Determinación de cada registro, definimos 4 clusters
clusters <- cutree(hc, k=4)
# Asignamos los clusters
assigned_cluster <- viviendas_5_z %>% mutate(cluster = as.factor(clusters))

Aquí podemos ver con un gráfico de dispersión como quedan categorizados los registros en un plano entre las variables preciom y areaconst, las cuales son variables de gran interés

ggplot(assigned_cluster, aes(x = areaconst, y = preciom, color = cluster)) +
  geom_point(size = 2, alpha = 0.5) +
  geom_text(aes(label = cluster), vjust = -.8) +
  theme_classic()

También podemos observar un Dendograma

plot(hc, cex = 0.6, main = "Dendograma de Viviendas", las=1,
ylab = "Distancia euclidiana", xlab = "Grupos")
rect.hclust(hc, k = 4, border = 2:5)

Debido a que el gráfico de un Dendograma no proporciona mucha información, podemos hacer el conteo de los registros que pertenecen a cada cluster y también podemos ver un gráfico de dispersión de los clusters en función de las agrupaciones realizadas.

cluster_counts <- table(clusters)
kable(cluster_counts, "html") %>%
  kable_styling("striped", full_width = FALSE) %>%
  row_spec(0, bold = TRUE)
clusters Freq
1 7851
2 59
3 14
4 89
fviz_cluster(list(data = viviendas_5, cluster = clusters), 
             geom = "point", 
             ellipse.type = "convex",
             ggtheme = theme_minimal())

Validamos el número óptimo de clusters midiendo el índice de Silhouette promedio para valorar la mejor alternativa en la elección del número de conglomerados.

silhouette_res  = c()
for(i in 2:6){
    dist_ev <- dist(viviendas_5_z, method = 'euclidean')
    hc_ev <- hclust(dist_ev, method = 'complete')
    cluster_ev <- cutree(hc_ev, k = i)
    # Calcular el coeficiente de Silhouette
    sil <- silhouette(cluster_ev, dist(viviendas_5_z))
    sil_avg <- mean(sil[,3])
    silhouette_res  = c(silhouette_res, sil_avg)
}
sil_df <- data.frame(K = c(2,3,4,5,6),silhouette_value = silhouette_res)
kable(sil_df, "html") %>%
  kable_styling("striped", full_width = FALSE) %>%
  row_spec(0, bold = TRUE)
K silhouette_value
2 0.5398479
3 0.5074854
4 0.4988204
5 0.2367314
6 0.2366606

La tabla anterior indica que los mejores resultados de agrupación se dan cuando \(k=2\), lo que significa que solo deberían haber dos clusters de agrupamiento.

Realizaremos un último análisis de conglomerados con \(k=2\) y observaremos las gráficas de representación de los los clusters.

clusters_final <- cutree(hc, k=2)
assigned_cluster_final <- viviendas_5_z %>% mutate(cluster = as.factor(clusters_final))
ggplot(assigned_cluster_final, aes(x = areaconst, y = preciom, color = cluster)) +
  geom_point(size = 2, alpha = 0.5) +
  geom_text(aes(label = cluster), vjust = -.8) +
  theme_classic()

cluster_counts_fin <- table(clusters_final)
kable(cluster_counts_fin, "html") %>%
  kable_styling("striped", full_width = FALSE) %>%
  row_spec(0, bold = TRUE)
clusters_final Freq
1 7910
2 103
fviz_cluster(list(data = viviendas_5, cluster = clusters_final), 
             geom = "point", 
             ellipse.type = "convex",
             ggtheme = theme_minimal())

Finalmente obtenemos dos grupos en donde cada uno de los registros de cada grupo presentan similitudes importantes en sus características que ayudarán a la inmobiliaria a definir las estrategias de mercado específicas para cada grupo.

Al conjunto de datos original se le puede agregar una columna que identifique a cada registro con el cluster al cual pertenece, siendo esta columna una etiqueta de análisis.

viviendas_5$cluster <- clusters_final
muestra_ale <- viviendas_5[sample(nrow(viviendas_5), 10), ]
kable(muestra_ale, "html") %>%
  kable_styling("striped", full_width = FALSE) %>%
  row_spec(0, bold = TRUE)
piso estrato preciom areaconst parqueaderos banios habitaciones cluster
3 5 750 433.00 1 5 10 1
6 6 850 190.00 3 4 3 1
3 4 600 420.00 4 5 5 1
9 5 220 85.00 2 2 3 1
2 4 225 130.00 1 3 3 1
1 3 62 61.00 1 1 2 1
3 6 850 350.00 4 3 3 1
3 4 560 500.00 4 8 8 1
3 4 245 72.00 1 2 3 1
8 5 246 159.38 2 4 4 1
viviendas_5$tipo <- viviendas_4$tipo
viviendas_5$zona <- viviendas_4$zona
tabla_5_1 <- table(viviendas_5$cluster, viviendas_5$tipo)
tabla_5_2 <- table(viviendas_5$cluster, viviendas_5$zona)
barplot(tabla_5_1, beside = FALSE, col = c("skyblue", "pink"),
        legend = TRUE, xlab = "Tipo", ylab = "Frecuencia")

barplot(tabla_5_2, beside = FALSE, col = c("skyblue", "pink"),
        legend = TRUE, xlab = "Zona", ylab = "Frecuencia")

Con el gráfico de dispersión entre preciom y areaconst, junto con los gráficos de barras de distribución para zona y para tipo, podemos concluir que el cluster 2 hacer referencia a inmuebles que son únicamente de tipo Casa, que tienen un gran área construida, que generalmente presentan precios altos y que se encuentran en la Zona Sur, Zona Oriente y Zona Norte.

5. Análisis de Correspondencia

En este análisis intentaremos presentar posibles asociaciones entre variables categóricas, definiendo patrones o estructuras definidas en los datos. Se analizarán las variables categóricas zona, estrato y tipo. La variable barrio se excluye debido a la gran cantidad de posibles resultados.

viviendas_6 <- viviendas_4 %>% select(zona,estrato,tipo)
kable(head(viviendas_6), "html") %>%
  kable_styling("striped", full_width = FALSE) %>%
  row_spec(0, bold = TRUE)
zona estrato tipo
Zona Oriente 3 Casa
Zona Oriente 3 Casa
Zona Oriente 3 Casa
Zona Sur 4 Casa
Zona Norte 5 Apartamento
Zona Norte 5 Apartamento

Iniciamos haciendo el análisis de correspondencia simple entre zona y estrato construyendo una tabla cruzada. También validamos la independencia entre las variables mediante una prueba Chi-squared.

viviendas_ca_1 <- viviendas_6 %>% select(zona,estrato)
viviendas_ca_1$estrato <- as.factor(viviendas_ca_1$estrato)
tabla_cruz_1 <- table(viviendas_ca_1$zona, viviendas_ca_1$estrato)
colnames(tabla_cruz_1) <- c("Estrato 3", "Estrato 4", "Estrato 5", "Estrato 6" )
kable(tabla_cruz_1, "html") %>%
  kable_styling("striped", full_width = FALSE) %>%
  row_spec(0, bold = TRUE)
Estrato 3 Estrato 4 Estrato 5 Estrato 6
Zona Centro 71 7 0 0
Zona Norte 527 382 757 143
Zona Oeste 39 76 279 759
Zona Oriente 316 3 1 0
Zona Sur 352 1596 1670 1035
chisq.test(tabla_cruz_1)
## 
##  Pearson's Chi-squared test
## 
## data:  tabla_cruz_1
## X-squared = 3852.4, df = 12, p-value < 0.00000000000000022

El resultado de la prueba Chi-squared indica que se debe rechazar la hipótesis de independencia entre las variables zona y estrato, lo que significa que existe entre ellas algún grado de relación. Ahora ejecutamos el análisis de correspondencia para observar el resultado gráficamente.

resultados_ca_1 <- CA(tabla_cruz_1)

Del gráfico resultante del análisis de correspondencia entre zona y estrato podemos destacar lo siguiente: La Zona Oriente en su gran mayoría son estrato 4, la Zona Oeste es estrato 6, Zona Sur y Zona Norte tienen estrato 5 y estrato 4.

Finalmente medimos el grado de representatividad del proceso calculando los valores de la varianza acumulada.

fviz_screeplot(resultados_ca_1, addlabels = TRUE, ylim = c(0, 80))+ggtitle("")+
ylab("Porcentaje de varianza explicado") + xlab("Ejes")

La gráfica indica que el primer componente explica el \(69.5\%\) de la varianza, y los dos primeros componentes resumen un \(97.5\%\) de los datos.

En este caso se realiza el proceso anterior para las variables zona y tipo.

viviendas_ca_2 <- viviendas_6 %>% select(zona,tipo)
tabla_cruz_2 <- table(viviendas_ca_2$zona, viviendas_ca_2$tipo )
kable(tabla_cruz_2, "html") %>%
  kable_styling("striped", full_width = FALSE) %>%
  row_spec(0, bold = TRUE)
Apartamento Casa
Zona Centro 11 67
Zona Norte 1173 636
Zona Oeste 1019 134
Zona Oriente 52 268
Zona Sur 2771 1882
chisq.test(tabla_cruz_1)
## 
##  Pearson's Chi-squared test
## 
## data:  tabla_cruz_1
## X-squared = 3852.4, df = 12, p-value < 0.00000000000000022
resultados_ca_2 <- CA(tabla_cruz_2)
resultados_ca_2$eig
##       eigenvalue percentage of variance cumulative percentage of variance
## dim 1 0.09016511                    100                               100

Al analizar la variable tipo el resultado solo genera una dimensión que explica el \(100\%\) de la varianza, por lo cual no es posible graficar la relación en el plano de dos dimensiones. Esto es debido a que la variable tipo solo tiene dos valores posibles.

Finalmente realizaremos un análisis de correspondencia múltiple para analizar las tres variables en conjunto.

viviendas_7 <- viviendas_6
viviendas_7$estrato <- as.factor(viviendas_7$estrato)
resultados_mca <- MCA(viviendas_7)

De este análisis podemos concluir que los inmuebles de tipo Casa están más relacionados con la Zona Norte, y que los inmuebles de tipo Apartamento están más relacionados a las Zona Sur y a su vez al estrato 5.

5. Conclusiones

  1. El análisis de componentes principales permitió reducir la dimensionalidad de las variables numéricas en únicamente dos componentes que contribuyen en la mayoría de la explicación de la variabilidad de los datos. Esto nos ayuda a observar qué variables presentan mayor relevancia en la información contenida en el conjunto de datos y en las definiciones de las estrategias relacionadas al mercado inmobiliario en la ciudad. Esta actividad nos permitió visualizar los datos en dos dimensiones, observando patrones, influencia y relaciones entre las variables, particularmente se observa como las variables preciom, banios y areaconst se relacionan e influyen en la varianza general del conjunto de datos de ventas de inmuebles.

  2. El análisis de conglomerados sirvió para agrupar registros que presentan características similares. Este proceso nos permitió identificar cual es el número adecuado de clusters en los cuales se pueden agrupar los datos, y así mismo validar cómo se realiza el agrupamiento. A partir de este agrupamiento se pueden generar estrategias de mercado específicas para cada cluster, identificando también diferentes características que sobresalen en cada grupo. En este caso hay características muy particulares para los inmuebles de tipo casa que se ubican en la zona sur de la ciudad; en general son inmuebles que presentan una gran área construída, que hacen parte del mismo barrio y que sus precios son variados. Para estos casos la estrategia de mercado debe ajustarse a las características y a las necesidades, asegurando así un método exitoso de venta de los inmuebles agrupados.

  3. En el análisis de correspondencia se asociaron registros basándose en las definiciones de las variables categóricas. En este caso se pudo hacer un buen análisis entre las variables zona y estrato, que permitió identificar que hay cierto grado de relación entre ellas. En efecto se determinó que la Zona Oeste se relaciona con el estrato 5, que el estrato 4 y 5 está relacionado con la Zona Sur, y que la Zona Oriente y la Zona Centro podrían relacionarse al estrato 3. Visto desde una perspectiva de mercado inmobiliario, esta segmentación de datos ayuda a enfocar diferentes esfuerzos a diferentes grupos, los cuales tendrán diferentes comportamientos en el precio de venta del inmueble.