El mercado de bienes raíces en Cali ha experimentado un notable crecimiento en los últimos años, impulsado por diversos factores como el aumento de la población, la inversión extranjera directa y el desarrollo de nuevos proyectos inmobiliarios. Esto se evidencia en las cifras de ventas del sector, que alcanzaron los $6700 millones en 2022 y los $6100 millones en 2023. Se proyecta que esta tendencia positiva continúe en los años venideros, generando un impacto significativo en la economía regional y permitiendo un desarrollo dinámico en diversos sectores.
A continuación, se presenta el desarrollo de un análisis estadístico descriptivo de un conjunto de datos con información de una serie de viviendas en diferentes zonas de Cali, haciendo uso además de herramientas de visualización que generen valor en los datos suministrados y permitan, a los interesados, tomar decisiones asertivas según tendencias y patrones.
Realizar un análisis descriptivo de los datos entregados por la empresa B&C con el fin de impulsar la toma de decisiones de esta a partir de la identificación de tendencias y patrones
Revisar la estructura de la base de datos, identificando sus variables, tipos de datos e información irrelevante para el análisis.
Implementar estrategias de limpieza y transformación de datos que permitan que permitan sacar el mayor valor posible a la base inicial.
Generar un análisis estadístico enfocado en la variable precio que presente resultados enfocados en la toma de decisiones de la empresa.
A continuación, se presentan las estrategias implementadas en pro de cumplir los objetivos planteados inicialmente:
Para la realización del proyecto se usaron librerías enfocadas en el análisis estadístico, estas van desde algunas útiles para describir la estructura de las bases, hasta otras que permiten realizar gráficas y pruebas de hipótesis.
# Llamado de librerias
devtools::install_github("dgonxalex80/paqueteMET")
library(paqueteMET)
library(dplyr)
library(summarytools)
library(ggplot2)
library(GGally)
require(mice)
library(mice)
library(VIM)
library(corrplot)
Teniendo la base de datos, lo primero en realizarse fue un sondeo inicial tanto de la estructura de la tabla como la cantidad de registros y una revisión estadística de algunas variables.
Se encontraron 8330 registros y un total de 13 columnas.
# Obtencion de la base de datos
df = vivienda_faltantes
glimpse(df)
## Rows: 8,330
## Columns: 13
## $ id <dbl> 8312, 8311, 8307, 8296, 8297, 8298, 8299, 8300, 8286, 8287, …
## $ zona <chr> "Zona Oeste", "Zona Oeste", "Zona Oeste", "Zona Sur", "Zona …
## $ piso <dbl> 4, 1, NA, 2, NA, NA, 2, NA, NA, 2, 1, NA, 6, NA, 2, 8, NA, 6…
## $ estrato <dbl> 6, 6, 5, 3, 5, 5, 6, 5, 5, 5, 5, 3, 6, 3, 5, 6, 5, 6, 6, 6, …
## $ preciom <dbl> 1300, 480, 1200, 220, 330, 1350, 305, 480, 275, 285, 310, 17…
## $ areaconst <dbl> 318, 300, 800, 150, 112, 390, 125, 280, 74, 120, 166, 155, 1…
## $ parquea <dbl> 2, 1, 4, 1, 2, 8, 2, 4, 1, 2, 2, NA, 2, NA, 1, 2, 2, 2, 4, 1…
## $ banios <dbl> 4, 4, 7, 2, 4, 10, 3, 4, 2, 4, 4, 4, 3, 2, 2, 3, 3, 5, 7, 1,…
## $ habitac <dbl> 2, 4, 5, 4, 3, 10, 3, 4, 3, 3, 3, 6, 3, 3, 2, 4, 3, 4, 3, 2,…
## $ tipo <chr> "Apartamento", "Casa", "Casa", "Casa", "Casa", "Casa", "Apar…
## $ barrio <chr> "arboleda", "normandía", "miraflores", "el guabal", "bella s…
## $ longitud <dbl> -76576, -76571, -76568, -76565, -76565, -76565, -76565, -765…
## $ latitud <dbl> 3454.00000, 3454.00000, 3455.00000, 3417.00000, 3408.00000, …
Del precio podemos notar que la distribución de los valores es asimétrica pues la media es mayor a la mediana de los datos, esto puede indicar una concentración de valores atípicos en los precios altos.
summary(df$preciom)
## Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
## 58.0 220.0 330.0 434.2 540.0 1999.0 2
Lo más relevante de la variable piso es la cantidad elevada de valores nulos que contiene, esto podría ser indicativo de que es recomendable no usarla, aunque es necesario realizar un par de análisis más para tomar dicha decisión.
summary(df$piso)
## Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
## 1.000 2.000 3.000 3.772 5.000 12.000 2641
summary(df$estrato)
## Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
## 3.000 4.000 5.000 4.634 5.000 6.000 3
summary(df$areaconst)
## Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
## 30 80 123 175 229 1745 3
La variable parqueadero también muestra una gran cantidad de valores nulos que deben ser revisados minuciosamente para determinar la relevancia de esta columna en el análisis.
summary(df$parquea)
## Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
## 1.000 1.000 2.000 1.836 2.000 10.000 1606
summary(df$banios)
## Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
## 0.000 2.000 3.000 3.112 4.000 10.000 3
summary(df$habitac)
## Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
## 0.000 3.000 3.000 3.605 4.000 10.000 3
Luego de conocer la estructura se decide eliminar 3 variables del dataset:
Id: Al ser una etiqueta, no proporciona información relevante al análisis
Longitud y Latitud: Estas variables por si solas no aportan mucho a un análisis descriptivo, para el caso en cuestión aumentarían la complejidad del análisis de forma innecesaria.
# Eliminar las variables latitud, longitud e id
df <- subset(df, select = -c(latitud, longitud, id))
Inicialmente vemos gráficamente la distribución de los valores nulos en toda la tabla. Podemos observar que hay 3 valores que no tienen información así que podrían ser depurados sin tener impactos relevantes. Por otro lado, las variables parqueadero y piso son las que mayor porcentaje de nulos tiene. Se procede a depurar los 3 valores, ver la nueva distribución y graficar el porcentaje de nulidad.
# Idenfificación de datos nullos
#df %>%
#summarise_all(~ sum(is.na(.)))
md.pattern(df, rotate.names = TRUE)
## preciom zona estrato areaconst banios habitac tipo barrio parquea piso
## 4812 1 1 1 1 1 1 1 1 1 1
## 1912 1 1 1 1 1 1 1 1 1 0
## 877 1 1 1 1 1 1 1 1 0 1
## 726 1 1 1 1 1 1 1 1 0 0
## 1 1 0 0 0 0 0 0 0 0 0
## 2 0 0 0 0 0 0 0 0 0 0
## 2 3 3 3 3 3 3 3 1606 2641
##
## 4812 0
## 1912 1
## 877 1
## 726 2
## 1 9
## 2 10
## 4270
# Eliminar filas con valores faltantes en cualquier variable
df <- df[rowSums(is.na(df[, c("zona", "estrato", "areaconst", "banios", "habitac", "tipo", "barrio", "parquea", "piso")])) != length(c("zona", "estrato", "areaconst", "banios", "habitac", "tipo", "barrio", "parquea", "piso")), ]
# Idenfificación de datos nullos
md.pattern(df, rotate.names = TRUE)
## zona estrato preciom areaconst banios habitac tipo barrio parquea piso
## 4812 1 1 1 1 1 1 1 1 1 1
## 1912 1 1 1 1 1 1 1 1 1 0
## 877 1 1 1 1 1 1 1 1 0 1
## 726 1 1 1 1 1 1 1 1 0 0
## 0 0 0 0 0 0 0 0 1603 2638
##
## 4812 0
## 1912 1
## 877 1
## 726 2
## 4241
Algunos textos hablan de descartar variables que tienen más de un 20% de nulidad sin tener que recurrir a la imputación. Como se observa en la gráfica, tanto parqueadero como piso tienden a cumplir con estas características. Aun así, posteriormente se realizan pruebas de hipótesis sobre ambas variables para determinar el impacto que tienen sobre el conjunto de datos.
porcentaje_nulos <- colMeans(is.na(df)) * 100
print(porcentaje_nulos)
## zona piso estrato preciom areaconst parquea banios habitac
## 0.00000 31.68008 0.00000 0.00000 0.00000 19.25063 0.00000 0.00000
## tipo barrio
## 0.00000 0.00000
# Crear un dataframe para el gráfico
df_plot <- data.frame(
columna = names(porcentaje_nulos),
porcentaje_nulos = porcentaje_nulos
)
# Graficar el porcentaje de valores nulos
ggplot(df_plot, aes(x = columna, y = porcentaje_nulos)) +
geom_bar(stat = "identity", fill = "skyblue") +
labs(title = "Porcentaje de Valores Nulos por Columna", x = "Columna", y = "Porcentaje de Valores Nulos") +
geom_text(aes(label = paste(round(porcentaje_nulos, 2), "%")), vjust = -0.5) +
theme(legend.position = "none",
plot.title = element_text(hjust = 0.5),
panel.background = element_rect(fill = "white",
size = 0.5,
linetype = 'dotted')
)
Este proceso busca homogeneizar registros que significan lo mismo, pero que fueron cargados con algunas diferencias en su contenido.
# Estandarización de campos tipo string:
# tipo
df$tipo <- tolower(df$tipo)
df$tipo[df$tipo=="apartamento"] <- "Apartamento"
df$tipo[df$tipo=="apto"] <- "Apartamento"
df$tipo[df$tipo=="casa"] <- "Casa"
# barrio
df$barrio <- tolower(df$barrio)
Para revisar qué tanto impacto tienen las variables con altos porcentajes de valores nulos sobre el resto del dataset y dar un posible entendimiento del porqué de su nulidad, se realizan pruebas de hipótesisi sobre una variable categórica y otra continua.
Cuando se realiza una prueba t-Student para revisar las relaciones estadísticas entre las medias de la variable parqueadero y precio, se observan diferencias entre la población con nulos y la que no los tiene. Es decir, si existe una relación y es recomendable no imputar datos porque se vería una variación importante.
na_parquea = is.na(df$parquea)
t.test(df$preciom~na_parquea)
##
## Welch Two Sample t-test
##
## data: df$preciom by na_parquea
## t = 24.246, df = 3105.4, p-value < 2.2e-16
## alternative hypothesis: true difference in means between group FALSE and group TRUE is not equal to 0
## 95 percent confidence interval:
## 167.4200 196.8809
## sample estimates:
## mean in group FALSE mean in group TRUE
## 469.3183 287.1678
# Calcular el coeficiente de variación para la variable preciom con parquea
cv_con_parquea <- sd(df$preciom[!is.na(df$parquea)]) / mean(df$preciom[!is.na(df$parquea)]) * 100
# Calcular el coeficiente de variación para la variable preciom sin parquea
cv_sin_parquea <- sd(df$preciom[is.na(df$parquea)]) / mean(df$preciom[is.na(df$parquea)]) * 100
# Imprimir los resultados
cat("Cv con parquea null:", cv_con_parquea, "- Tot observaciones: ",length(df$preciom[!is.na(df$parquea)]), "\n")
## Cv con parquea null: 71.47443 - Tot observaciones: 6724
cat("Cv sin parquea null:", cv_sin_parquea, "- Tot observaciones: ",length(df$preciom[is.na(df$parquea)]), "\n")
## Cv sin parquea null: 87.85446 - Tot observaciones: 1603
na_parquea = is.na(df$parquea)
tabla_parqu = table(df$tipo, na_parquea)
chisq.test(tabla_parqu)
##
## Pearson's Chi-squared test with Yates' continuity correction
##
## data: tabla_parqu
## X-squared = 41.913, df = 1, p-value = 9.545e-11
Respecto a la variable tipo, el mayor impacto de nulidad se encuentra en las casas, podría inferirse un valor nulo en parqueadero equivaldría a no tener parqueadero. Pero se opta por no tener en cuenta la columna en el resto del análisis.
tbl_parquea <- table(df$tipo, na_parquea)
prop.table(tbl_parquea)
## na_parquea
## FALSE TRUE
## Apartamento 0.50882671 0.10435931
## Casa 0.29866699 0.08814699
El análisis de nulidad respecto a la variable piso se hace en contra del área construida, se observa también que existe una relación y es recomendable no imputar datos.
# Calcular el coeficiente de variación para la variable preciom con parquea
cv_con_piso <- sd(df$areaconst[!is.na(df$piso)]) / mean(df$areaconst[!is.na(df$piso)]) * 100
# Calcular el coeficiente de variación para la variable preciom sin parquea
cv_sin_parquea <- sd(df$areaconst[is.na(df$piso)]) / mean(df$areaconst[is.na(df$piso)]) * 100
# Imprimir los resultados
cat("Cv con piso null:", cv_con_parquea, "- Tot observaciones: ",length(df$areaconst[!is.na(df$piso)]), "\n")
## Cv con piso null: 71.47443 - Tot observaciones: 5689
cat("Cv sin piso null:", cv_sin_parquea, "- Tot observaciones: ",length(df$areaconst[is.na(df$piso)]), "\n")
## Cv sin piso null: 81.27873 - Tot observaciones: 2638
Según el tipo de viviendas, podría inferirse que el gran impacto de nulidad respecto a las casas puede darse porque, al ser normalmente un primer piso, no reportan el dato.
na_piso = is.na(df$piso)
tabla_piso = table(df$tipo, na_piso)
chisq.test(tabla_piso)
##
## Pearson's Chi-squared test with Yates' continuity correction
##
## data: tabla_piso
## X-squared = 128.18, df = 1, p-value < 2.2e-16
prop.table(tabla_piso)
## na_piso
## FALSE TRUE
## Apartamento 0.4470998 0.1660862
## Casa 0.2360994 0.1507145
Para obtener la matriz de correlaciones de las variables cuantitativas, se omiten entonces tanto el precio y el parqueadero por su gran porcentaje de nulos.
Se observa entonces la gran relación positiva que existe en el precio respecto al estrato, al área construida y la cantidad de baños. Es decir, con el aumento de cualquiera de estas variables el precio tiende a aumentar.
c_cuant <- df[sapply(df, is.numeric)]
# Eliminar las variables de piso y `parquea para la correlación`
c_cuant <- subset(c_cuant, select = -c(piso, parquea))
v <- cor(c_cuant)
corrplot(v, method = "circle")
Teniendo en cuenta todo el proceso de limpieza y transformación aplicado a la base, se presentan los resultados atados a cada uno de los análisis realizados.
El área construida presenta una distribución sesgada a la derecha con la mayoría de las propiedades con áreas no mayores a los 200 metros cuadrados. Esto tiene mucho sentido en un país donde el gran porcentaje de las viviendas no superan los 100 metros.
# Histograma de la variable area
hist(df$areaconst,
main = "Distribución del área construida en metros",
xlim=c(0, 1600),
las=1,
xlab = "Área(m2)",
ylab = "Frecuencia",
col = "#AFEEEE") # Cambiar el color de las barras a azul)
Los precios también se distribuyen con una cola positiva y tiene sentido si se relacionan con las casas, mayor área suele traducirse como mayor precio de una vivienda.
# Histograma de la variable precio
hist(df$preciom,
main = "Distribución de precio de las viviendas",
xlim=c(0, 2000),
las=1,
xlab = "Precio en Millones de Pesos",
ylab = "Frecuencia",
col = "#E0FFFF")
Ahora pues, cuando se grafican tanto el precio como el área construida mediante dispersiones, se corrobora lo comentado inicialmente, la gran mayoría de propiedades se concentran en áreas no mayores a los 200 metros y el precio aumenta con los metros. Aun así, se observan algunos valores atípicos con casas “pequeñas” a precios muy sobre la media o, por el contrario, casas enormes a precios muy bajos. Para conocer más a fondo este comportamiento es necesario analizar otros factores como el estrato y la zona en la que se ubican las viviendas.
# Scatter Plot del precio de las viviendas por área
ggplot(data = df, aes(x = areaconst, y = preciom)) +
geom_point() +
labs(x = "Area Construida (m2)", y = "Precio (millones)") +
ggtitle("Área Construida vs Precio de las Viviendas")+
theme(plot.title = element_text(hjust = 0.5),
panel.background = element_rect(fill = "white",
linewidth = 0.5,
linetype = 'dotted')
)
Se observa que sólo el 17% de las viviendas registradas son de estrato 3 que a su vez es el más bajo identificado. Eso indica que se está trabajando con un conjunto de propiedades de estratos altos en su mayoría con una media de precios más alta que el promedio caleño. Además, el porcentaje más alto se registra como apartamentos de estrato 5, esto también, al analizar las distribuciones de zonas, indicará donde se encuentra la mayor concentración de este tipo de propiedades.
# Grafico de barras de la variable estratos
df$estrato <- factor(df$estrato)
ggplot(df, aes(x = estrato, fill = estrato)) +
geom_bar(fill = "#87cefa") +
labs(title = "Viviendas por Estrato Socioeconómico",
x = "Piso",
y = "Cantidad") +
geom_text(aes(label = after_stat(count)),
stat = "count",
colour = "black") +
theme(legend.position = "none",
plot.title = element_text(hjust = 0.5),
panel.background = element_rect(fill = "white",
size = 0.5,
linetype = 'dotted')
)
Se observa que sólo el 17% de las viviendas registradas son de estrato 3 que a su vez es el más bajo identificado. Eso indica que se está trabajando con un conjunto de propiedades de estratos altos en su mayoría con una media de precios más alta que el promedio caleño. Además, el porcentaje más alto se registra como apartamentos de estrato 5, esto también, al analizar las distribuciones de zonas, indicará donde se encuentra la mayor concentración de este tipo de propiedades.
# Grafico de barras del tipo de vivienda diferenciado por estrato
ggplot(df, aes(x = tipo, fill = factor(estrato))) +
geom_bar(position = "dodge") +
labs(x = "Tipo de Vivienda",
y = "Cantidad",
title = "Comparacion de Tipo de Vivienda por Estrato en Cali") +
theme(plot.title = element_text(hjust = 0.5),
panel.background = element_rect(fill = "white",
size = 0.5,
linetype = 'dotted'))+
scale_fill_discrete(name = "Estrato")
Se observa entonces que más de la mitad de las muestras se encuentran en la zona sur de Cali. Puede inferirse que, al recordar la distribución de los estratos, esta zona tiende a tener una mayor cantidad de propiedades con precios elevados y estratos más altos, un nicho muy delimitado.
df_torta_zona <- df %>%
group_by(zona) %>% # Variable a transformar
count() %>%
ungroup() %>%
mutate(perc = `n` / sum(`n`)) %>%
arrange(perc) %>%
mutate(labels = scales::percent(perc))
ggplot(df_torta_zona, aes(x = "", y = perc, fill = zona)) +
geom_col() +
geom_text(aes(label = labels),
position = position_stack(vjust = 0.5)) +
guides(fill = guide_legend(title = "Zona de la Vivienda")) +
coord_polar(theta = "y") +
ggtitle("Distribución Porcentual de la zona la de Vivienda") +
theme_void()
Con este diagrama de cajas se pueden resolver todas las incógnitas sobre las zonas con precios más altos en sus propiedades. Se observa que la zona oeste es por mucho la que tiene la muestra de viviendas con mayores precios. Por otro lado, aunque las zonas sur y norte tiene un precio más cercano al promedio general de todo el dataset, son también las que mayor cantidad de datos atípicos superiores a su media tienen. Es decir, tienen muchas propiedades que suelen ser más “caras” de lo normal en su zona.
# Diagrama de cajas de la distribución de precios de vivienda por zona
ggplot(df, aes(x = zona, y = preciom)) +
geom_boxplot(fill = "skyblue") +
labs(x = "Zona",
y = "Precio",
title = "Precio de las viviendas por zona") +
theme(legend.position = "none",
plot.title = element_text(hjust = 0.5),
panel.background = element_rect(fill = "white")
)
Por último, se observa que en promedio las casas suelen ser más costosas que los apartamentos con diferencia. Aun así, y como se determinó al inicio del análisis, el precio sigue una distribución sesgada a la derecha y se esperaba una gran cantidad de outliers en la base.
# Diagrama de cajas de la distribución de precios de vivienda por zona
ggplot(df, aes(x = tipo, y = preciom)) +
geom_boxplot(fill = "#AFEEEE") +
labs(x = "Tipo",
y = "Precio",
title = "Precio de las viviendas por tipo") +
theme(legend.position = "none",
plot.title = element_text(hjust = 0.5),
panel.background = element_rect(fill = "white")
)
A medida que se ahondó más en los datos, se observaron una serie de comportamientos interesantes y que merecen la pena un mayor análisis para comprender a fondo el porqué se estos. Por ejemplo, viviendas de más de 1000 m2 con precios de 250 millones; estos valores son demasiado atípicos y superan el posible análisis que permiten los datos actuales. ¿Es posible que no todas las propiedades fueses casas y apartamentos, sino que también se incluyeran lotes vacíos? Esto podría dar explicaciones, además, a las propiedades que no registraban baños, habitaciones, pisos o parqueaderos.
En general, las propiedades más apetecidas por los compradores son las que se componen de 3 baños y entre 3 y 4 habitaciones. Esto puede ayudar a la compañía en gran medida a focalizar sus negocios en los segmentos que más se mueven.
En general, el nicho de viviendas registradas corresponde a personas con ingresos altos y medios, esto se observa claramente en que más del 80% de las propiedades pertenecían a estratos mayores al 3.
Al realizar análisis estadísticos de conjuntos de datos es supremamente necesario empaparse del contexto y la necesidad, inicialmente Podemos inferir cosas que a simple vista parecen muy obvias, pero, al profundizar en el análisis, el resultado puede cambiar, como fue la inferencia inicial de que las casas más caras se encontraban en la zona sur por tener la mayor cantidad de muestras, cosa que se refutó al ver desde otros puntos de vista, la distribución de los precios y las zonas.
La falta de datos en sectores de amplio crecimiento y con poder adquisitivo emergente plantea desafíos importantes para B&C. Sin embargo, mediante el análisis detallado de los datos disponibles, podrán comprender mejor las necesidades de vivienda de la población y adaptar sus estrategias comerciales en consecuencia, ya sea enfocándose en ventas o arrendamientos, y decidiendo si deben continuar operando en áreas competitivas con precios elevados o expandir su negocio hacia áreas emergentes con alta demanda.