Introducción

Una empresa inmobiliaria líder en una gran ciudad está buscando comprender en profundidad el mercado de viviendas urbanas para tomar decisiones estratégicas más informadas. La empresa posee una base de datos extensa que contiene información detallada sobre diversas propiedades residenciales disponibles en el mercado. Se requiere realizar un análisis holístico de estos datos para identificar patrones, relaciones y segmentaciones relevantes que permitan mejorar la toma de decisiones en cuanto a la compra, venta y valoración de propiedades.

Alcance del trabajo

Retos:

Realizar un análisis integral y multidimensional de la base de datos para obtener una comprensión del mercado inmobiliario urbano. Se requiere:

  • Análisis de Componentes Principales: Reducir la dimensionalidad del conjunto de datos y visualizar la estructura de las variables en componentes principales para identificar características clave que influyen en la variación de precios y oferta del mercado.

  • Análisis de Conglomerados: Agrupar las propiedades residenciales en segmentos homogéneos con características similares para entender la oferta en diferentes partes de la ciudad y en diferentes estratos socioeconómicos.

  • Análisis de Correspondencia: Examinar la relación entre las variables categóricas (tipo de vivienda, zona y barrio), para identificar patrones de comportamiento de la oferta.

  • Visualización de resultados: Presentar gráficos, mapas y otros recursos visuales para comunicar los hallazgos.

El informe final debe incluir análisis detallados de los resultados obtenidos, las conclusiones clave y las recomendaciones específicas para guiar las decisiones estratégicas de la empresa inmobiliaria.

1. Análisis de componentes principales PCA

1.1. Análisis exploratorio

  • Revisión de variables
## [1] "El dataframe cargado tiene las siguientes dimensiones: 8322 x 13"
head(vivienda)

El dataframe tiene 8322 registros y 13 variables. En primera instancia se evidencia que existen algunos valores faltantes en varias columnas.

A continuación se describen cada una de las variables contenidas en la base:

  • id - Identificador único de la vivienda

  • zona - Zona de la ciudad (Centro/Sur/Oeste/Oriente/Norte)

  • piso - Piso en que está ubicada la vivienda (entre 1 y 12 pisos)

  • estrato - Estrato socioeconómico del sector de la vivienda (3 al 6. No se presentan registros de 1 y 2)

  • preciom - Precio en millones de pesos (entre 58 y 1999 millones)

  • areaconst - Área construida en metros cuadrados (entre 30 y 1745 m cuadrados)

  • parqueaderos - Número de parqueaderos que tiene la vivienda(entre 1 y 10)

  • banios - Número de baños que tiene la vivienda (entre 0 y 10)

  • habitaciones - Número de habitaciones que tiene la vivienda (entre 0 y 10)

  • tipo - Tipo de vivienda (Casa/Apartamento).

  • barrio - Barrio donde está ubicada la vivienda.

  • longitud - Coordenada de longitud donde está ubicada la vivienda

  • latitud - Coordenada de latitud donde está ubicada la vivienda

En la revisión y reconocimiento preliminar de la base de datos, se identificó que algunas de esas variables no se encontraban estadarizadas durante la captura de la data, por lo que se hace necesario realizar la limpieza y depuración de los registros.

  • Resumen estadístico:
summary(vivienda)
##        id           zona               piso              estrato     
##  Min.   :   1   Length:8322        Length:8322        Min.   :3.000  
##  1st Qu.:2080   Class :character   Class :character   1st Qu.:4.000  
##  Median :4160   Mode  :character   Mode  :character   Median :5.000  
##  Mean   :4160                                         Mean   :4.634  
##  3rd Qu.:6240                                         3rd Qu.:5.000  
##  Max.   :8319                                         Max.   :6.000  
##  NA's   :3                                            NA's   :3      
##     preciom         areaconst       parqueaderos        banios      
##  Min.   :  58.0   Min.   :  30.0   Min.   : 1.000   Min.   : 0.000  
##  1st Qu.: 220.0   1st Qu.:  80.0   1st Qu.: 1.000   1st Qu.: 2.000  
##  Median : 330.0   Median : 123.0   Median : 2.000   Median : 3.000  
##  Mean   : 433.9   Mean   : 174.9   Mean   : 1.835   Mean   : 3.111  
##  3rd Qu.: 540.0   3rd Qu.: 229.0   3rd Qu.: 2.000   3rd Qu.: 4.000  
##  Max.   :1999.0   Max.   :1745.0   Max.   :10.000   Max.   :10.000  
##  NA's   :2        NA's   :3        NA's   :1605     NA's   :3       
##   habitaciones        tipo              barrio             longitud     
##  Min.   : 0.000   Length:8322        Length:8322        Min.   :-76.59  
##  1st Qu.: 3.000   Class :character   Class :character   1st Qu.:-76.54  
##  Median : 3.000   Mode  :character   Mode  :character   Median :-76.53  
##  Mean   : 3.605                                         Mean   :-76.53  
##  3rd Qu.: 4.000                                         3rd Qu.:-76.52  
##  Max.   :10.000                                         Max.   :-76.46  
##  NA's   :3                                              NA's   :3       
##     latitud     
##  Min.   :3.333  
##  1st Qu.:3.381  
##  Median :3.416  
##  Mean   :3.418  
##  3rd Qu.:3.452  
##  Max.   :3.498  
##  NA's   :3

Los datos de vivienda ofertados pertenecen a los estratos socioeconómicos más altos (entre 3 y 6).Dado que la mediana es 5, se identifica que la mayoría de las propiedades pertenecen a ese estrato. Adicionalmente existiendo 3 valores faltantes, podría requerir imputación basada en otras variables como zona o barrio.

El preciom, que representa el precio de las propiedades en millones, es de aproximadamente 433.9 millones en su media, con un mínimo de 58 millones y un máximo de 1999 millones, indicando una gran variabilidad en los precios de las propiedades. Adicionalmente, dado que la mediana es 330 millones, pero la media es 433.9 millones, se infiere que hay propiedades con precios muy altos que sesgan la media hacia arriba.Finalmente, hay 2 valores faltantes, que son pocos pero críticos, ya que el precio es una variable clave.

A nivel del área construida (en metros cuadrados), se observa un rango que va desde 30 m² hasta 1745 m². Dado que la media de 174.9 m² es mayor que la mediana es 123 m², sugiere la presencia de propiedades con áreas construidas muy grandes que sesgan la media. Adicionalmente hay 3 valores faltantes que deben revisarse y posiblemente imputarse.

La mayoría de las viviendas tienen entre 2 baños, 3 habitaciones y 3 parqueaderos.


  • Tipos de datos:
str(vivienda)
## spc_tbl_ [8,322 × 13] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
##  $ id          : num [1:8322] 1147 1169 1350 5992 1212 ...
##  $ zona        : chr [1:8322] "Zona Oriente" "Zona Oriente" "Zona Oriente" "Zona Sur" ...
##  $ piso        : chr [1:8322] NA NA NA "02" ...
##  $ estrato     : num [1:8322] 3 3 3 4 5 5 4 5 5 5 ...
##  $ preciom     : num [1:8322] 250 320 350 400 260 240 220 310 320 780 ...
##  $ areaconst   : num [1:8322] 70 120 220 280 90 87 52 137 150 380 ...
##  $ parqueaderos: num [1:8322] 1 1 2 3 1 1 2 2 2 2 ...
##  $ banios      : num [1:8322] 3 2 2 5 2 3 2 3 4 3 ...
##  $ habitaciones: num [1:8322] 6 3 4 3 3 3 3 4 6 3 ...
##  $ tipo        : chr [1:8322] "Casa" "Casa" "Casa" "Casa" ...
##  $ barrio      : chr [1:8322] "20 de julio" "20 de julio" "20 de julio" "3 de julio" ...
##  $ longitud    : num [1:8322] -76.5 -76.5 -76.5 -76.5 -76.5 ...
##  $ latitud     : num [1:8322] 3.43 3.43 3.44 3.44 3.46 ...
##  - attr(*, "spec")=List of 3
##   ..$ cols   :List of 13
##   .. ..$ id          : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   .. ..$ zona        : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
##   .. ..$ piso        : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
##   .. ..$ estrato     : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   .. ..$ preciom     : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   .. ..$ areaconst   : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   .. ..$ parqueaderos: list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   .. ..$ banios      : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   .. ..$ habitaciones: list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   .. ..$ tipo        : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
##   .. ..$ barrio      : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_character" "collector"
##   .. ..$ longitud    : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   .. ..$ latitud     : list()
##   .. .. ..- attr(*, "class")= chr [1:2] "collector_double" "collector"
##   ..$ default: list()
##   .. ..- attr(*, "class")= chr [1:2] "collector_guess" "collector"
##   ..$ delim  : chr ";"
##   ..- attr(*, "class")= chr "col_spec"
##  - attr(*, "problems")=<externalptr>

Se cuenta con 9 variables numéricas y 4 de tipo character. Entendiendo el concepto de cada variable, se evidencia que “piso” está como CHR pero realmente corresponde a un número, por tanto se debe proceder a cambiar el tipo a NUM para facilitar los cálculos y poder realizar comparaciones.


  • Revisión de datos faltantes:
faltantes <- colSums(is.na(vivienda))
print(faltantes)
##           id         zona         piso      estrato      preciom    areaconst 
##            3            3         2638            3            2            3 
## parqueaderos       banios habitaciones         tipo       barrio     longitud 
##         1605            3            3            3            3            3 
##      latitud 
##            3

Todos los atributos tienen datos faltantes, sin embargo en el caso de piso y parqueaderos el volumen es considerablemente mayor (2.638 y 1.605, respectivamente).

# 1. Identificar si existen registros con datos faltantes en todos los atributos
faltantes_todos_atributos <- vivienda[, !names(vivienda) %in% c("piso", "parqueaderos")]

indices_na <- which(rowSums(is.na(faltantes_todos_atributos)) > 0)

registros_na <- vivienda[indices_na, ]
print(registros_na)
## # A tibble: 3 × 13
##      id zona  piso  estrato preciom areaconst parqueaderos banios habitaciones
##   <dbl> <chr> <chr>   <dbl>   <dbl>     <dbl>        <dbl>  <dbl>        <dbl>
## 1    NA <NA>  <NA>       NA      NA        NA           NA     NA           NA
## 2    NA <NA>  <NA>       NA      NA        NA           NA     NA           NA
## 3    NA <NA>  <NA>       NA     330        NA           NA     NA           NA
## # ℹ 4 more variables: tipo <chr>, barrio <chr>, longitud <dbl>, latitud <dbl>

1.2. Limpieza de datos

  • Convertir variable “piso” a numérica.
# Convertir la variable 'piso' de character a numeric
vivienda$piso <- as.numeric(vivienda$piso)

# Verificar la conversión
str(vivienda$piso)
##  num [1:8322] NA NA NA 2 1 1 1 1 2 2 ...
  • Eliminar registros donde todos los atributos están ausentes
# Eliminar los registros donde 'id' es NA
vivienda2 <- vivienda[!is.na(vivienda$id), ]
dim(vivienda2)
## [1] 8319   13

Se aplica este código pues en la consulta anterior se evidenciaron 3 registros que tienen todos los atributos en NULO. Una vez eliminados los 3 registros, queda un dataframe de 8319 registros. Posteriormente se procede a calcular e imputar la MODA para ‘piso’ y ‘parqueadero’ en cada combinación de ‘tipo’, ‘zona’, y ‘estrato’.

  • Imputar LA MODA a PISO Y PARQUEADERO
library(dplyr)

# Función para calcular la moda
moda <- function(x) {
  uniq_x <- unique(x[!is.na(x)])
  uniq_x[which.max(tabulate(match(x, uniq_x)))]
}

# Calculo de la moda por grupo
moda_imputacion <- vivienda2 %>%
  group_by(tipo, zona, estrato) %>%
  summarise(
    moda_piso = moda(piso),
    moda_parqueaderos = moda(parqueaderos),
    .groups = 'drop'
  )

# Unir los resultados de moda con los datos originales
vivienda2 <- vivienda2 %>%
  left_join(moda_imputacion, by = c("tipo", "zona", "estrato"))

# Imputar los valores faltantes con la moda y Eliminar las columnas auxiliares
vivienda2 <- vivienda2 %>%
  mutate(
    piso = ifelse(is.na(piso), moda_piso, piso),
    parqueaderos = ifelse(is.na(parqueaderos), moda_parqueaderos, parqueaderos)
  ) %>%
  select(-moda_piso, -moda_parqueaderos) 
  • Identificar filas con al menos un valor NA
# Identificar filas con al menos un valor NA
registros_na <- which(rowSums(is.na(vivienda2)) > 0)

# Ver las filas con datos faltantes
vivienda2[registros_na, ]

Dado que con la imputación de la moda por grupos, aun permanecen algunos registros con NA, pudo ocurrir que se tuvieran:

  • Grupos sin valores no faltantes: Si en un grupo específico (definido por las combinaciones de tipo, zona, y estrato), todas las observaciones de piso o parqueaderos son NA, entonces la función moda no puede calcular la moda porque no tiene valores válidos para elegir.

  • Valores únicos que son NA: Si un grupo tiene todos los valores de piso o parqueaderos como NA, la imputación con la moda no se puede realizar porque no hay un valor no-NA con el cual reemplazar.

Dado lo anterior, se decide eliminar los registros con NA, pues es la opción más sencilla y segura, teniendo en cuenta que los NA restantes son muy pocos respecto al total de registros.

  • Eliminar filas con datos faltantes
# Eliminar filas con datos faltantes
vivienda2 <- na.omit(vivienda2)
#confirmación de la inexistencia de NA
columnas_na <- colSums(is.na(vivienda2))
print(columnas_na)
##           id         zona         piso      estrato      preciom    areaconst 
##            0            0            0            0            0            0 
## parqueaderos       banios habitaciones         tipo       barrio     longitud 
##            0            0            0            0            0            0 
##      latitud 
##            0
dims <- dim(vivienda2)
mensaje2 <- paste("Despues de eliminar y/o imputar los NA, se tienen las siguientes dimensiones:", paste(dims, collapse = " x "))
print(mensaje2)
## [1] "Despues de eliminar y/o imputar los NA, se tienen las siguientes dimensiones: 8312 x 13"
  • Eliminación de Atributos

Se eliminan la columnas ID, Longitud y Latitud ya que no son reelevantes para el objetivo. Debido que este informe no tiene por objeto profudizar en un análisis via georeferenciación.

vivienda2 <- vivienda2 %>% select(-id, -longitud, -latitud)
  • Revisión de registros en cero
# Contar cuántos registros tienen valor cero en cada variable
cero_counts <- sapply(vivienda2, function(x) sum(x == 0, na.rm = TRUE))
print(cero_counts)
##         zona         piso      estrato      preciom    areaconst parqueaderos 
##            0            0            0            0            0            0 
##       banios habitaciones         tipo       barrio 
##           45           66            0            0

Dado que no es factible que una vivienda cuente con cero baños y habitaciones, se procede a imputar la moda.

  • Imputar la MODA a Baños y Habitaciones
# Función para calcular la moda
calculate_mode <- function(x) {
  unique_x <- unique(x)
  unique_x[which.max(tabulate(match(x, unique_x)))]
}

# Calcular la moda  sin contar los ceros
mode_habitaciones <- calculate_mode(vivienda2$habitaciones[vivienda2$habitaciones != 0])
mode_banios <- calculate_mode(vivienda2$banios[vivienda2$banios != 0])

# Imputar la moda en 'habitaciones' y 'banios' donde el valor es 0
vivienda2$habitaciones[vivienda2$habitaciones == 0] <- mode_habitaciones
vivienda2$banios[vivienda2$banios == 0] <- mode_banios
cero_counts <- sapply(vivienda2, function(x) sum(x == 0, na.rm = TRUE))
print(cero_counts)
##         zona         piso      estrato      preciom    areaconst parqueaderos 
##            0            0            0            0            0            0 
##       banios habitaciones         tipo       barrio 
##            0            0            0            0
  • Normalización de los barrios.
# Se corrigen todos los nombres de los barrios y se llevan a minúscula
vivienda2$barrio <- tolower(vivienda2$barrio)

# Se eliminan las tildes
vivienda2$barrio <- iconv(vivienda2$barrio, to = "ASCII//TRANSLIT")

# Se hace limpieza de los nombres de los barrios
vivienda2 <- vivienda2 %>%
  mutate(barrio = ifelse(barrio == "agua blanca", "aguablanca", barrio),
         barrio = ifelse(barrio == "alf?crez real", "alferez real", barrio),
         barrio = ifelse(barrio == "alfonso lopez i", "alfonso lopez", barrio),
         barrio = ifelse(barrio == "arboledas", "arboleda", barrio),
         barrio = ifelse(barrio == "cali bella", "calibella", barrio),
         barrio = ifelse(barrio == "cali canto", "calicanto", barrio),
         barrio = ifelse(barrio == "calicanto viii", "calicanto", barrio),
         barrio = ifelse(barrio == "ciudad mel?cndez", "ciudadela melendez", barrio),
         barrio = ifelse(barrio == "ciudad melendez", "ciudadela melendez", barrio),
         barrio = ifelse(barrio == "ciudadela pasoancho", "ciudadela paso ancho", barrio),
         barrio = ifelse(barrio == "el tr?cbol", "el trebol", barrio),
         barrio = ifelse(barrio == "ingenio i", "el ingenio i", barrio),
         barrio = ifelse(barrio == "ingenio", "el ingenio", barrio),         
         barrio = ifelse(barrio == "ingenio ii", "el ingenio ii", barrio),
         barrio = ifelse(barrio == "el ingenio 3", "el ingenio iii", barrio),         
         barrio = ifelse(barrio == "laflora", "la flora", barrio),
         barrio = ifelse(barrio == "las am?cricas", "las americas", barrio),
         barrio = ifelse(barrio == "las vegas de", "las vegas", barrio),
         barrio = ifelse(barrio == "los alamos", "alamos", barrio),    
         barrio = ifelse(barrio == "alborada", "la alborada", barrio), 
         barrio = ifelse(barrio == "alcazares", "los alcazares", barrio),         
         barrio = ifelse(barrio == "mel?cndez", "melendez", barrio),
         barrio = ifelse(barrio == "pampalinda", "pampa linda", barrio),
         barrio = ifelse(barrio == "portales de comfandi", "portada de comfandi", barrio),
         barrio = ifelse(barrio == "rep??blica de israel", "republica de israel", barrio),
         barrio = ifelse(barrio == "rincon de la", "rincon de salomia", barrio),
         barrio = ifelse(barrio == "san judas", "san judas tadeo", barrio),
         barrio = ifelse(barrio == "tequendeme", "tequendama", barrio),
         barrio = ifelse(barrio == "valle de lili", "valle del lili", barrio),
         barrio = ifelse(barrio == "tequendema", "tequendama", barrio),
         barrio = ifelse(barrio == "barrio 7de agosto", "7 de agosto", barrio),
         barrio = ifelse(barrio == "barrio el recuerdo", "el recuerdo", barrio),                  barrio = ifelse(barrio == "barrio eucaristico", "eucaristico", barrio),
         barrio = ifelse(barrio == "barrio obrero", "obrero", barrio),
         barrio = ifelse(barrio == "barrio tranquilo y", "tranquilo", barrio),  
         barrio = ifelse(barrio == "base a?crea", "base aerea", barrio), 
         barrio = ifelse(barrio == "juanamb??", "juanambu", barrio),  
         barrio = ifelse(barrio == "la riviera", "la rivera", barrio),     
         barrio = ifelse(barrio == "flora", "la flora", barrio),  
         barrio = ifelse(barrio == "cristales", "los cristales", barrio), 
         barrio = ifelse(barrio == "normandia west point", "normandia", barrio),         
         barrio = ifelse(barrio == "zona norte los", "zona norte", barrio))

Aun después de realizar diversas imputaciones para normalizar los nombres de barrios, se determina que no es una variable confiable para el análisis debido que los registros no se cargaron estandarizadamente, por tanto hay aun información poco confiable. De preferencia se debe utilizar la variable zona.

# Mostrar los nombres únicos de los barrios
#nombres_barrios <- unique(vivienda2$barrio)
#print(nombres_barrios)

1.3. Gráficos principales

  • Cantidad viviendas por TIPO
# Calcular la tabla y la suma total
numero_viviendas_tipo <- table(vivienda2$tipo)
suma_total_tipo_vivienda <- sum(numero_viviendas_tipo)

# Crear el gráfico de barras
barplot(
  numero_viviendas_tipo, 
  col = c("#0d3b66", "#f4d35e"),
  xlab = "Tipo de Vivienda", 
  ylab = "Cantidad de Viviendas",
  main = "Distribución de Viviendas por Tipo",
  ylim = c(0, max(numero_viviendas_tipo) * 1.2), # Ajustar el límite superior para dejar espacio a las etiquetas
  border = "white", # Añadir bordes blancos a las barras para mejor visualización
  las = 1, # Girar etiquetas del eje Y para mejor legibilidad
  cex.names = 0.9 # Ajustar el tamaño de las etiquetas en el eje X
)

# Añadir las etiquetas de texto encima de las barras
for (i in seq_along(numero_viviendas_tipo)) {
  text(
    x = i, 
    y = numero_viviendas_tipo[i] + max(numero_viviendas_tipo) * 0.05, # Colocar el texto ligeramente por encima de las barras
    label = paste(numero_viviendas_tipo[i], sprintf("(%.1f%%)", numero_viviendas_tipo[i] / suma_total_tipo_vivienda * 100)), 
    cex = 0.8, # Ajustar el tamaño del texto
    col = "black" # Establecer el color del texto
  )
}

Se evidencia que la mayoría de las viviendas ofertadas son de tipo apartamento.

  • Cantidad viviendas por ZONA
# Calcular la tabla y la suma total
numero_viviendas_zona <- table(vivienda2$zona)
suma_total_zona_vivienda <- sum(numero_viviendas_zona) # Se suman las viviendas por zona

barplot(
  numero_viviendas_zona, 
  col = c("#0d3b66", "red", "skyblue", "darkgreen", "#f4d35e"),
  xlab = "Zona", 
  ylab = "Cantidad de Viviendas",
  main = "Distribución de Viviendas por Zona",
  ylim = c(0, max(numero_viviendas_zona) * 1.2), # Ajustar el límite superior 
  border = "white", # Añadir bordes blancos a las barras 
  las = 1, # Girar etiquetas del eje Y para mejor legibilidad
  cex.names = 0.9 # Ajustar el tamaño de las etiquetas en el eje X
)
for (i in seq_along(numero_viviendas_zona)) 
  { # Añadir las etiquetas de texto encima de las barras
  text(
    x = i, 
    y = numero_viviendas_zona[i] + max(numero_viviendas_zona) * 0.05, # ubicar el texto ligeramente por encima de las barras
    label = paste(numero_viviendas_zona[i], sprintf("(%.1f%%)", numero_viviendas_zona[i] / suma_total_zona_vivienda * 100)), 
    cex = 0.8, # Ajustar el tamaño del texto
    col = "black" # Establecer el color del texto
  )
}

Se evidencia que la mayoría de las viviendas ofertadas (el pareto) están focalizadas en las zonas sur y norte.

Al fusionar los os gráficos anteriores, encontramos la ditribución de tipos de vivienda por Zona:

  • Tipos de Vivienda por ZONA
p =ggplot(vivienda2, aes(x = zona, fill = tipo)) +
  geom_bar(position = "stack", width = 0.7) +
  labs(title = "Distribucion por Zona",
       x = "Zona de Cali",
       y = "Tipo de Vivienda") +
  scale_fill_manual(values = c("#0d3b66", "#f4d35e")) +
  theme_minimal() +
  theme(legend.position = "top", legend.title = element_blank())
print(p)

En el gráfico anterior se evidencia que en las zonas Sur, Norte y Oeste, que es donde están focalizadas la mayoría de ofertas de vivienda, en % mayor de participación de esa oferta lo componen los apartamentos, mientras que en la Zona Oriente y Centro, la mayoría de las ofertas son casas.

  • Cantidad de viviendas por estrato
numero_viviendas_estrato <- table(vivienda2$estrato)
suma_total_estrato_vivienda <- sum(numero_viviendas_estrato) 

barplot(
        numero_viviendas_estrato, 
        col = c("#0d3b66", "red", "skyblue", "darkgreen", "#f4d35e"),
        xlab = "Estrato", 
        ylab = "Cantidades",
        main = "Viviendas por Estrato",
        ylim = c(0, max(numero_viviendas_estrato) * 1.2), 
        border = "white", # Añadir bordes blancos a las barras 
       las = 1, # Girar etiquetas del eje Y para mejor legibilidad
       cex.names = 0.9 # Ajustar el tamaño de las etiquetas en el eje X
)
for (i in seq_along(numero_viviendas_estrato)) 
  {
  text(i, 
       y = numero_viviendas_estrato[i] + max(numero_viviendas_estrato) * 0.05,
       label = paste(numero_viviendas_estrato[i], sprintf("(%.1f%%)",          
  numero_viviendas_estrato[i] / suma_total_estrato_vivienda * 100)), 
    cex = 0.8, # Ajustar el tamaño del texto
    col = "black" # Establecer el color del texto
  )
}

  • PRECIO PROMEDIO por TIPO
promedio_valor_tipo <- tapply(vivienda2$preciom, vivienda2$tipo, mean)

barplot_heights <- barplot(
  promedio_valor_tipo,
  col = c("#0d3b66", "#f4d35e"), # Colores mejorados
  xlab = "Tipo de Vivienda", # Etiqueta más descriptiva
  ylab = "Precio Promedio (Millones)", # Unidad incluida
  main = "Precio Promedio por Tipo de Vivienda", # Título más claro
  ylim = c(0, max(promedio_valor_tipo) * 1.1), # Ajuste de los límites del eje Y para mejor visualización
  border = "white", # Sin borde en las barras
  las = 1 # Rotar las etiquetas del eje Y para mejor legibilidad
)

text(
  barplot_heights,
  promedio_valor_tipo,
  labels = sprintf("%.0f", promedio_valor_tipo),
  pos = 3, # Colocar etiquetas encima de las barras
  col = "black",
  cex = 0.8,
  font = 2 # Negrita para mejor legibilidad
)
abline(h = 0, col = "gray50", lwd = 2)

  • Precio promedio por zona
promedio_valor_viviendas_zona <- tapply(vivienda2$preciom, vivienda2$zona, mean)


barplot_heights <- barplot(
  promedio_valor_viviendas_zona,
  col = c("#0d3b66", "red", "skyblue", "darkgreen", "#f4d35e"), # Colores mejorados
  xlab = "Zona", # Etiqueta descriptiva
  ylab = "Precio Promedio (Millones)", # Unidad incluida
  main = "Precio Promedio de las Viviendas por Zona", # Título más claro
  ylim = c(0, max(promedio_valor_viviendas_zona) * 1.1), # Ajuste de los límites del eje Y para mejor visualización
  border = "white", # Sin borde en las barras
  las = 1 # Rotar las etiquetas del eje Y para mejor legibilidad
)
text(
  barplot_heights,
  promedio_valor_viviendas_zona,
  labels = sprintf("%.0f", promedio_valor_viviendas_zona),
  pos = 3, # Colocar etiquetas encima de las barras
  col = "black",
  cex = 0.8,
  font = 2 # Negrita para mejor legibilidad
)
abline(h = 0, col = "gray50", lwd = 2)

  • Precio máximo por zona
precio_maximo_viviendas_zona <- tapply(vivienda2$preciom, vivienda2$zona, max)

barplot_heights <- barplot(
  precio_maximo_viviendas_zona,
  col = c("#0d3b66", "red", "skyblue", "darkgreen", "#f4d35e"), # Colores consistentes
  xlab = "Zona", # Etiqueta descriptiva
  ylab = "Precio Máximo (Millones)", # Unidad incluida en la etiqueta del eje Y
  main = "Precio Máximo de las Viviendas por Zona", # Título más claro
  ylim = c(0, max(precio_maximo_viviendas_zona) * 1.1), # Ajuste del eje Y para incluir las etiquetas
  border = "white", # Borde blanco para un aspecto limpio
  las = 1 # Rotación de etiquetas del eje Y para mejor legibilidad
)
text(
  barplot_heights,
  precio_maximo_viviendas_zona,
  labels = sprintf("%.0f", precio_maximo_viviendas_zona),
  pos = 3, # Colocar etiquetas encima de las barras
  col = "black",
  cex = 0.8,
  font = 2 # Negrita para mayor legibilidad
)
abline(h = 0, col = "gray50", lwd = 2)

  • Relación Área construida - Precio
# Gráfico de dispersión entre 'areaconst' y 'preciom' con color arcoíris
ggplot(vivienda2, aes(x = areaconst, y = preciom, color = areaconst)) +
  geom_point() +
  scale_color_gradient(low = "blue", high = "red") +
  ggtitle("Área construida vs precio")

El gráfico muestra una relación positiva entre las variables, lo cual es esperado pues en la medida que aumenta el área construida, debería aumentar también el precio. La tendencia es lineal, con una pendiente positiva. Existe una alta dispersión de los puntos alrededor de la línea de tendencia.

1.4. Identificación de Componentes

A continuación se realiza el análisis de los componentes principales:

# seleccionar solo las columnas numéricas:
data_numeric <- vivienda2[, sapply(vivienda2, is.numeric)]

# estandarizar los datos:
data_scaled <- scale(data_numeric)

# análisis de componentes principales:
componentes_principales <- prcomp(data_scaled, center = TRUE, scale. = TRUE)

# Se visualian los datos:
summary(componentes_principales)
## Importance of components:
##                           PC1    PC2    PC3    PC4     PC5     PC6     PC7
## Standard deviation     1.8810 1.1828 0.9026 0.6848 0.60107 0.48302 0.42969
## Proportion of Variance 0.5054 0.1999 0.1164 0.0670 0.05161 0.03333 0.02638
## Cumulative Proportion  0.5054 0.7053 0.8217 0.8887 0.94029 0.97362 1.00000
  • La desviación estándar de un componente principal indica la cantidad de variabilidad que este captura del conjunto de datos. En este caso, el primer componente principal (PC1) captura la mayor cantidad de variabilidad (1.8810), mientras que cada componente posterior captura una cantidad decreciente de la variabilidad restante.
library(dplyr)

# Proporción de varianza explicada
varianza_explicada_2 <- summary(componentes_principales)$importance[2, ]

# Crear el scree plot
barplot(varianza_explicada_2, 
        main = "Proporcion de Varianza Explicada por Componentes Principales",
        xlab = "Componentes Principales", 
        ylab = "Proporcion de Varianza Explicada",
        col = "skyblue")

  • La proporción de la varianza indica cuánto de la varianza total en los datos originales es capturada por cada componente principal. El primer componente (PC1) captura el 50.54% de la variabilidad total, lo que significa que es el componente más importante. Los dos primeros componentes (PC1 y PC2) juntos capturan alrededor del 70.53% de la variabilidad total, lo que sugiere que estos dos componentes son los más significativos para capturar la mayor parte de los datos.
# ggplot2 para una visualización más detallada. Visualizar los scores de las observaciones en los primeros dos componentes principales
scores <- as.data.frame(componentes_principales$x)
scores$tipo <- vivienda2$tipo

ggplot(scores, aes(x = PC1, y = PC2, color = tipo)) +
  geom_point(alpha = 0.7) +
  labs(title = "PCA: Primeros dos Componentes Principales",
       x = "Componente Principal 1",
       y = "Componente Principal 2") +
  theme_minimal()

id_componentes_principales <- componentes_principales$rotation

# Ver las cargas para los primeros componentes
id_componentes_principales[, 1:6]  # Muestra las cargas para PC1 y PC2
##                     PC1        PC2         PC3         PC4         PC5
## piso          0.1233824 -0.5097239  0.83952589  0.12449399  0.03649808
## estrato      -0.3031274 -0.5522896 -0.23506651 -0.56801182 -0.04239433
## preciom      -0.4670897 -0.2232240 -0.04891294  0.08987985  0.30763839
## areaconst    -0.4468059  0.1841179  0.07504580  0.28400769  0.65286066
## parqueaderos -0.4186572 -0.1891176 -0.12913065  0.61745514 -0.60532938
## banios       -0.4608633  0.0906164  0.20718848 -0.36719310 -0.17824463
## habitaciones -0.2956577  0.5544840  0.41510706 -0.23883770 -0.27893975
##                      PC6
## piso          0.04547167
## estrato       0.45141705
## preciom      -0.23569101
## areaconst     0.24661643
## parqueaderos  0.10384991
## banios       -0.67461139
## habitaciones  0.46032568

Las variables más importantes para PC1 son el precio, la cantidad de baños, el área construída y la cantidad de parqueaderos (de mayor a menor relevancia).

# Visualizar las cargas de las variables en los primeros dos componentes principales
biplot(componentes_principales, scale = 0,
       main = "Biplot Componentes Principales",
       xlab = "Componente Principal 1",
       ylab = "Componente Principal 2",
       col = c("grey", "darkorange"))

Con el gráfico anterior, se evidencia que la variable piso tiene una carga positiva hacia el componente principal (PC1), así que mientras esta variable incrementa se genera un aumento en el primer componente principal. En cuanto a la variable habitaciones, a diferencia de piso, cuenta con una carga negativa frente a PC1, por lo tanto, si esta variable incrementa, el valor de PC1 disminuye.

Por otro lado, en cuanto a PC2, las variables área construída y baño cuentan con una carga similar hacia dicho componente, generando un crecimiento entre ambas; así que se podría decir que si el área construída incrementa, también incrementará el número de baños, disminuyendo el valor del componente principal (PC2).

# Visualizar las cargas de las variables en los primeros dos componentes principales
loadings <- as.data.frame(componentes_principales$rotation)
loadings$variable <- rownames(loadings)


ggplot(loadings, aes(x = PC1, y = PC2, label = variable)) +
  geom_text(aes(label = variable), hjust = 1, vjust = 1) +
 geom_point() +
  labs(title = "Cargas de las Variables en los Primeros Dos Componentes Principales",
       x = "Componente Principal 1",
      y = "Componente Principal 2") +
 theme_minimal()

1.5. Reducción de la dimensionalidad

Teniendo en cuenta que los dos primeros PC están capturando el 70.54% de los datos, y que esto representa un buen balance entre dimensionalidad y cantidad de información, se decide conservar solo PC1 y PC2.

data_reduced_2 <- predict(componentes_principales)[, 1:2]  
head(data_reduced_2) 
##             PC1       PC2
## [1,]  0.7682545 2.4501983
## [2,]  1.4621422 1.2339515
## [3,]  0.5040696 1.5534352
## [4,] -1.1622918 0.4647239
## [5,]  1.0522392 0.1630549
## [6,]  0.7636810 0.2369515
# Extraer las cargas de los componentes principales del resultado del PCA
id_componentes_principales <- componentes_principales$rotation

# Seleccionar las cargas de los dos primeros componentes principales (PC1 a PC2)
cargas_componentes_principales <- id_componentes_principales[, 1:2]

# Mostrar las cargas para los primeros cuatro componentes principales
print(cargas_componentes_principales)
##                     PC1        PC2
## piso          0.1233824 -0.5097239
## estrato      -0.3031274 -0.5522896
## preciom      -0.4670897 -0.2232240
## areaconst    -0.4468059  0.1841179
## parqueaderos -0.4186572 -0.1891176
## banios       -0.4608633  0.0906164
## habitaciones -0.2956577  0.5544840
  if (!require(factoextra)) install.packages("factoextra")
  library(factoextra)
  
  # Visualizar variables en función de contribución a 1ros componentes principales
  fviz_pca_var(componentes_principales,
                col.var = "contrib", # Color por contribuciones a los PC
                gradient.cols = c("#FF7F00", "#034D94"), # Colores del gradiente
                repel = TRUE, # Evitar superposición de texto
                axes = c(1, 2) # Cambiar los ejes a los primeros dos componentes principales
                )

2. Análisis de Conglomerados:

2.1. Determinar número óptimo de clusters

Agrupar las propiedades residenciales en segmentos homogéneos con características similares para entender las dinámicas de las ofertas específicas en diferentes partes de la ciudad y en diferentes estratos socioeconómicos.

# Cargar los datos
if (!exists("vivienda_modificado2"))
  
variables_clustering <- select(vivienda2, estrato, preciom, areaconst, banios, habitaciones, parqueaderos)


# Seleccionar las variables relevantes para el clustering
variables_clustering <- select(vivienda2, estrato, preciom, areaconst, banios, habitaciones, parqueaderos)

# Escalar las variables si es necesario
variables_clustering_scaled <- scale(variables_clustering)

# Determinar el número óptimo de clusters usando el método del codo
wcss <- numeric(10)
for (i in 1:10) {
  kmeans_fit <- kmeans(variables_clustering_scaled, centers = i, nstart = 10)
  wcss[i] <- kmeans_fit$tot.withinss
}

# Graficar el método del codo
plot(1:10, wcss, type = "b", pch = 19, frame = FALSE, 
     xlab = "Numero de clusters (k)", ylab = "WCSS")

Antes de aplicar el algoritmo K-Means, es necesario determinar el número óptimo de clústeres para agrupar las viviendas. En este caso se utilizó el método del codo (Elbow Method) para encontrar el número óptimo de clústeres que explica la mayor varianza en los datos.

El gráfico de la curva del codo representa la Suma de Cuadrados Internos (WCSS) en función del número de clústeres. La WCSS es una métrica que mide la dispersión interna de los puntos dentro de cada clúster. La idea es identificar el “codo”, es decir, el punto en el cual el aumento en el número de clústeres deja de reducir significativamente la WCSS.

fviz_nbclust(x = variables_clustering_scaled, FUNcluster = kmeans, method = "wss", k.max = 10, 
             diss = get_dist(variables_clustering_scaled, method = "euclidean"), nstart = 50)
## Warning: did not converge in 10 iterations
## Warning: did not converge in 10 iterations

2.2. Resultado del clustering

  • Cinco componentes principales
library(cluster)
require(cluster)

pam.res <- pam(variables_clustering_scaled, 5)

fviz_cluster(pam.res, geom = "point", ellipse.type = "norm", show.clust.cent = TRUE, star.plot = TRUE) + labs(title = "Resultado del clustering 5 PC") + theme_bw()

  • Cuatro componentes principales
library(cluster)

require(cluster)

pam.res <- pam(variables_clustering_scaled, 4)

fviz_cluster(pam.res, geom = "point", ellipse.type = "norm", show.clust.cent = TRUE, star.plot = TRUE) + labs(title = "Resultado del clustering 4 PC") + theme_bw()

  • Tres componentes principales
library(cluster)

require(cluster)

pam.res <- pam(variables_clustering_scaled, 3)

fviz_cluster(pam.res, geom = "point", ellipse.type = "norm", show.clust.cent = TRUE, star.plot = TRUE) + labs(title = "Resultado del clustering 3 PC") + theme_bw()

  • Dos componentes principales
library(cluster)

require(cluster)

pam.res <- pam(variables_clustering_scaled, 2)

fviz_cluster(pam.res, geom = "point", ellipse.type = "norm", show.clust.cent = TRUE, star.plot = TRUE) + labs(title = "Resultado del clustering 2 PC") + theme_bw()

Con las graficas anteriores, se evidencia la gran cantidad de datos que recogen los dos primeros componentes principales (PC1 & PC2); por lo tanto, a través de este método se confirma nuevamente que los 2 primeros componentes son suficientes para la captura de los datos.

2.3. Análisis de correlación

correlation_matriz <- cor(variables_clustering_scaled)

correlation_df <- as.data.frame(as.table(correlation_matriz))

colnames(correlation_df) <- c("Var1", "Var2", "Correlation")

# Convertir las correlaciones a porcentaje
correlation_df$Correlation_Percentage <- round(correlation_df$Correlation * 100, 1)

# Graficar la matriz de correlación como un mapa de calor con los valores de correlación en porcentaje
ggplot(data = correlation_df, aes(x = Var1, y = Var2, fill = Correlation)) +
  geom_tile() +
  scale_fill_gradient2(low = "green", high = "red", mid = "white", midpoint = 0) +
  theme_minimal() +
  labs(title = "Matriz de Correlación") +
  geom_text(aes(label = paste0(Correlation_Percentage, "%")), color = "black", size = 3)  # Agregar valores de correlación

Se evidencia que el precio mantiene una correlación importante con parqueaderos, baños, área construida e incluso con el estrato.

2.4. Gráfico para los clusters

k_optimo <- 4

set.seed(444)

modelo_kmeans <- kmeans(variables_clustering_scaled, centers = k_optimo, nstart = 10)

cluster_labels <- as.factor(modelo_kmeans$cluster)

vivienda_limpia_con_cluster <- mutate(vivienda2, cluster = cluster_labels)

# Visualizar los clusters en función de algunas variables relevantes
ggplot(vivienda_limpia_con_cluster, aes(x = estrato, y = preciom, color = cluster)) +
  geom_point() +
  labs(title = "Clusters de Propiedades Residenciales",
       x = "Estrato Socioeconómico",
       y = "Precio de la Propiedad") +
  theme_minimal() +
  scale_color_discrete(name = "Cluster")

ggplot(vivienda_limpia_con_cluster, aes(x = estrato, fill = cluster)) +
  geom_density(alpha = 0.5) +
  labs(title = "Distribución de Estrato por Cluster", x = "estrato", y = "Densidad") +
  theme_minimal()

ggplot(vivienda_limpia_con_cluster, aes(x = preciom, fill = cluster)) +
  geom_density(alpha = 0.5) +
  labs(title = "Distribución de Precios por Cluster", x = "Precio", y = "Densidad") +
  theme_minimal()

ggplot(vivienda_limpia_con_cluster, aes(x = areaconst, fill = cluster)) +
  geom_density(alpha = 0.5) +
  labs(title = "Distribución de Área construída por Cluster", x = "areaconst", y = "Densidad") +
  theme_minimal()

Area construída tiene una fuerte densidad en viviendas con área construída hasta 125 metros en el cluster 1. En cuanto al cluster 2, la densidad es baja, sin embargo, se visualiza un alto desempeño en áreas construídas entre 120 y 270 metros.

ggplot(vivienda_limpia_con_cluster, aes(x = banios, fill = cluster)) +
  geom_density(alpha = 0.5) +
  labs(title = "Distribución de Baños por Cluster", x = "banios", y = "Densidad") +
  theme_minimal()

Por otro lado, en la variable baños la densidad más alta para el cluster 1, oscila entre 2 y 2.5 baños; en cambio, el cluster 2 tiene una densidad alta entre 4 y 5 baños.

ggplot(vivienda_limpia_con_cluster, aes(x = habitaciones, fill = cluster)) +
  geom_density(alpha = 0.5) +
  labs(title = "Distribución de Habitaciones por Cluster", x = "Habitaciones", y = "Densidad") +
  theme_minimal()

El Cluster 1 tiene una alta densidad entre 2.5 y 3 habitaciones.

ggplot(vivienda_limpia_con_cluster, aes(x = parqueaderos, fill = cluster)) +
  geom_density(alpha = 0.5) +
  labs(title = "Distribución de Parqueaderos por Cluster", x = "Parqueaderos", y = "Densidad") +
  theme_minimal()

El cluster 1 tiene una alta densidad cuando se tiene 1 parqueadero.

3. Análisis de Correspondencia

Examinar la relación entre las variables categóricas (tipo de vivienda, zona y barrio), para identificar patrones de comportamiento de la oferta en mercado inmobiliario.

library(FactoMineR)

library(ggplot2)

library(factoextra)

library(Rcpp)

library(pander)
## 
## Adjuntando el paquete: 'pander'
## The following object is masked from 'package:GGally':
## 
##     wrap
library(corrplot)
## corrplot 0.92 loaded
library(gridExtra)

datos_inicial <- subset(vivienda2, select = c("barrio", "zona", "tipo",  "estrato"))

Barrio <- datos_inicial$barrio
Zona <- datos_inicial$zona
Tipo <- datos_inicial$tipo
Estrato <- datos_inicial$estrato

Datos <- cbind(datos_inicial, Barrio, Zona, Tipo, Estrato)

Datos[, 1:4] <- NULL

summary(Datos)
##     Barrio              Zona               Tipo              Estrato     
##  Length:8312        Length:8312        Length:8312        Min.   :3.000  
##  Class :character   Class :character   Class :character   1st Qu.:4.000  
##  Mode  :character   Mode  :character   Mode  :character   Median :5.000  
##                                                           Mean   :4.633  
##                                                           3rd Qu.:5.000  
##                                                           Max.   :6.000
F1<-ggplot(Datos, aes(x=Zona)) + geom_bar(fill= "red")
F2<-ggplot(Datos, aes(x=Tipo)) + geom_bar(fill= "#FFD4A5")
F3<-ggplot(Datos, aes(x=Barrio)) + geom_bar(fill= "green")
F4<-ggplot(Datos, aes(x=Estrato)) + geom_bar(fill= "orange")
F5 <- grid.arrange(F1,F2,F3,F4, nrow = 4)

## 3.1. Relación barrio y zona

library(FactoMineR)
tabla_barrio_zona <- table(Datos$Barrio, Datos$Zona)
head(tabla_barrio_zona, n=10)
##                  
##                   Zona Centro Zona Norte Zona Oeste Zona Oriente Zona Sur
##   20 de julio               0          0          0            3        0
##   3 de julio                0          0          0            0        1
##   7 de agosto               0          0          0            1        0
##   acopi                     0        157          0            0        1
##   aguablanca                0          0          0            2        1
##   aguacatal                 0          0        108            0        1
##   alameda                  11          0          0            0        5
##   alameda del rio           0          3          0            0        0
##   alamos                    0         15          0            0        0
##   alf?Crez real             0          0          0            0        5
chisq.test(tabla_barrio_zona)
## 
##  Pearson's Chi-squared test
## 
## data:  tabla_barrio_zona
## X-squared = 29300, df = 1428, p-value < 2.2e-16
library(FactoMineR)
library(factoextra)
library(gridExtra)
resultados_barrio_zona <- CA(tabla_barrio_zona)

library(factoextra)
eig.val <- get_eigenvalue(resultados_barrio_zona)
eig.val
##       eigenvalue variance.percent cumulative.variance.percent
## Dim.1  0.9622200         27.29711                    27.29711
## Dim.2  0.9287627         26.34796                    53.64507
## Dim.3  0.8963060         25.42720                    79.07227
## Dim.4  0.7377001         20.92773                   100.00000
fviz_screeplot(resultados_barrio_zona, addlabels = TRUE, ylim = c(0, 30))+ggtitle("")+
  ylab("Porcentaje de varianza explicado") + xlab("Ejes")

3.2. Relación barrio y tipo

library(FactoMineR)
tabla_barrio_tipo <- table(Datos$Barrio, Datos$Tipo)
head(tabla_barrio_tipo, n=10)
##                  
##                   Apartamento Casa
##   20 de julio               0    3
##   3 de julio                0    1
##   7 de agosto               0    1
##   acopi                    88   70
##   aguablanca                1    2
##   aguacatal                98   11
##   alameda                   4   12
##   alameda del rio           2    1
##   alamos                   12    3
##   alf?Crez real             4    1
chisq.test(tabla_barrio_tipo)
## 
##  Pearson's Chi-squared test
## 
## data:  tabla_barrio_tipo
## X-squared = 2411.1, df = 357, p-value < 2.2e-16
library(FactoMineR)
library(factoextra)
library(gridExtra)
resultados_barrio_tipo <- CA(tabla_barrio_tipo)
library(factoextra)
eig.val <- get_eigenvalue(resultados_barrio_tipo)
eig.val
##       eigenvalue variance.percent cumulative.variance.percent
## Dim.1  0.2900752              100                         100
fviz_screeplot(resultados_barrio_tipo, addlabels = TRUE, ylim = c(0, 30))+ggtitle("")+
  ylab("Porcentaje de varianza explicado") + xlab("Ejes")
## `geom_line()`: Each group consists of only one observation.
## ℹ Do you need to adjust the group aesthetic?

3.3. Relación zona y tipo

library(FactoMineR)
tabla_zona_tipo <- table(Datos$Zona, Datos$Tipo)
head(tabla_zona_tipo, n=10)
##               
##                Apartamento Casa
##   Zona Centro           21   98
##   Zona Norte          1198  722
##   Zona Oeste          1029  169
##   Zona Oriente          61  288
##   Zona Sur            2787 1939
chisq.test(tabla_zona_tipo)
## 
##  Pearson's Chi-squared test
## 
## data:  tabla_zona_tipo
## X-squared = 695.36, df = 4, p-value < 2.2e-16
library(FactoMineR)
library(factoextra)
library(gridExtra)
resultados_zona_tipo <- CA(tabla_zona_tipo)
library(factoextra)
eig.val <- get_eigenvalue(resultados_zona_tipo)
eig.val
##       eigenvalue variance.percent cumulative.variance.percent
## Dim.1 0.08365695              100                         100
fviz_screeplot(resultados_zona_tipo, addlabels = TRUE, ylim = c(0, 30))+ggtitle("")+
  ylab("Porcentaje de varianza explicado") + xlab("Ejes")
## `geom_line()`: Each group consists of only one observation.
## ℹ Do you need to adjust the group aesthetic?

3.5. Relación zona y estrato

library(FactoMineR)
tabla_zona_estrato <- table(Datos$Zona, Datos$Estrato)
head(tabla_zona_estrato, n=10)
##               
##                   3    4    5    6
##   Zona Centro   105   14    0    0
##   Zona Norte    572  407  769  172
##   Zona Oeste     54   84  290  770
##   Zona Oriente  340    8    1    0
##   Zona Sur      382 1616 1685 1043
chisq.test(tabla_zona_estrato)
## 
##  Pearson's Chi-squared test
## 
## data:  tabla_zona_estrato
## X-squared = 3867.7, df = 12, p-value < 2.2e-16
library(FactoMineR)
library(factoextra)
library(gridExtra)
resultados_zona_estrato <- CA(tabla_zona_estrato)

library(factoextra)
eig.val <- get_eigenvalue(resultados_zona_estrato)
eig.val
##       eigenvalue variance.percent cumulative.variance.percent
## Dim.1 0.32644555        70.155929                    70.15593
## Dim.2 0.12764789        27.432619                    97.58855
## Dim.3 0.01122083         2.411452                   100.00000
fviz_screeplot(resultados_zona_estrato, addlabels = TRUE, ylim = c(0, 30))+ggtitle("")+
  ylab("Porcentaje de varianza explicado") + xlab("Ejes")

3.6. Relación tipo y estrato

library(FactoMineR)
tabla_tipo_estrato <- table(Datos$Tipo, Datos$Estrato)
head(tabla_tipo_estrato, n=10)
##              
##                  3    4    5    6
##   Apartamento  639 1404 1763 1290
##   Casa         814  725  982  695
chisq.test(tabla_tipo_estrato)
## 
##  Pearson's Chi-squared test
## 
## data:  tabla_tipo_estrato
## X-squared = 224.45, df = 3, p-value < 2.2e-16
library(FactoMineR)
library(factoextra)
library(gridExtra)
resultados_tipo_estrato <- CA(tabla_tipo_estrato)
library(factoextra)
eig.val <- get_eigenvalue(resultados_tipo_estrato)
eig.val
##       eigenvalue variance.percent cumulative.variance.percent
## Dim.1  0.0270036              100                         100
fviz_screeplot(resultados_tipo_estrato, addlabels = TRUE, ylim = c(0, 30))+ggtitle("")+
  ylab("Porcentaje de varianza explicado") + xlab("Ejes")
## `geom_line()`: Each group consists of only one observation.
## ℹ Do you need to adjust the group aesthetic?

4. Visualización de resultados:

Presentar gráficos, mapas y otros recursos visuales para comunicar los hallazgos de manera clara y efectiva a la dirección de la empresa.

4.1. Gráfico de líneas con línea de tendencia

# Gráfico de líneas con línea de tendencia
ggplot(vivienda2, aes(x = areaconst, y = preciom)) +
  geom_line(color = "blue", size = 1) +  
  geom_smooth(method = "lm", color = "red", se = FALSE) +  
  labs(title = "Precio Medio en Función del Área Construida", x = "Área Construida", y = "Precio Medio") +
  theme_minimal()
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
## `geom_smooth()` using formula = 'y ~ x'

Se evidencia que el precio promedio está en función del tamaño de las viviendas, puesto que a mayor tamaño resultan ser más costosas.

# Gráfico de líneas con línea de tendencia
ggplot(vivienda2, aes(x = estrato, y = preciom)) +
  geom_line(color = "blue", size = 1) +  
  geom_smooth(method = "lm", color = "red", se = FALSE) + 
  labs(title = "Precio Medio en función del Estrato", x = "Estrato", y = "Precio Medio") +
  theme_minimal()
## `geom_smooth()` using formula = 'y ~ x'

Con el gráfico anterior, se evidencia el precio promedio por cada una de los estratos; la línea roja, representa la tendencia de los precios a través del método de regresión linea. De esta forma, se evidencia qeu que el incremento real del valor de las viviendas de los estratos 5 al 6, está en línea con la ergresión lineal, a diferencia del precio promedio de las viviendas de estrato 4 a 5.

# Gráfico de líneas con línea de tendencia

ggplot(vivienda2, aes(x = areaconst, y = habitaciones)) +
  geom_line(color = "blue", size = 1) +  
  geom_smooth(method = "lm", color = "red", se = FALSE) +  
  labs(title = "Número de Habitaciones en Función del Área Construida", 
       x = "Área Construida (m²)", 
       y = "Número de Habitaciones") +
  theme_minimal()
## `geom_smooth()` using formula = 'y ~ x'

# Gráfico de líneas con línea de tendencia

ggplot(vivienda2, aes(x = banios, y = habitaciones)) +
  geom_line(color = "blue", size = 1) +  
  geom_smooth(method = "lm", color = "red", se = FALSE) +  
  labs(title = "Número de habitaciones en Función al número de baños", 
       x = "Número de baños", 
       y = "Número de Habitaciones") +
  theme_minimal()
## `geom_smooth()` using formula = 'y ~ x'

# Gráfico de líneas con línea de tendencia

ggplot(vivienda2, aes(x = areaconst, y = piso)) +
  geom_line(color = "blue", size = 1) +  
  geom_smooth(method = "lm", color = "red", se = FALSE) +  
  labs(title = "Número de Pisos en Función del Área Construida - total viviendas", x = "Área Construida (m²)", y = "Número de Pisos") +
  theme_minimal()
## `geom_smooth()` using formula = 'y ~ x'

Con el gráfico anterior, se evidencia que entre menor área construída se cuenta con más pisos.

Teniendo en cuenta que esto podría generar un sesgo en la interpretación y toma de decisiones por parte de la empresa, se desagrega el gráfico a nivel de viviendas tipo casa y viviendas tipo apartamento.

# Filtrar los datos para tipo "Casa" y graficar
ggplot(data = subset(vivienda2, tipo == "Casa"), aes(x = areaconst, y = piso)) +
  geom_line(color = "blue", size = 1) +  
  geom_smooth(method = "lm", color = "red", se = FALSE) +  
  labs(title = "Número de Pisos en Función del Área Construida - casas", x = "Área Construida (m²)", y = "Número de Pisos") +
  theme_minimal()
## `geom_smooth()` using formula = 'y ~ x'

# Filtrar los datos para tipo "Casa" y graficar
ggplot(data = subset(vivienda2, tipo == "Apartamento"), aes(x = areaconst, y = piso)) +
  geom_line(color = "blue", size = 1) +  
  geom_smooth(method = "lm", color = "red", se = FALSE) +  
  labs(title = "Número de Pisos en Función del Área Construida - apartamento", x = "Área Construida (m²)", y = "Número de Pisos") +
  theme_minimal()
## `geom_smooth()` using formula = 'y ~ x'

Se evidencia que para las viviendas tipo casa, el area construída no afecta el número de pisos, ya que mantienen una media muy estable. En cuanto a las viviendas tipo apartamento, el área construída si genera un cambio en el número de pisos, mostrando una tendencia incremental leve.

4.2. Gráficos de proporción

  • por Estrato
# Contar el número de viviendas por estrato
estrato_count <- as.data.frame(table(vivienda2$estrato))
colnames(estrato_count) <- c("estrato", "count")

# Calcular el porcentaje
estrato_count <- estrato_count %>%
  mutate(percentage = count / sum(count) * 100)

# Crear gráfico de pastel con porcentajes
ggplot(estrato_count, aes(x = "", y = count, fill = estrato)) +
  geom_bar(width = 1, stat = "identity") +
  coord_polar(theta = "y") +
  geom_text(aes(label = sprintf("%.1f%%", percentage)), position = position_stack(vjust = 0.5), color = "white") +
  scale_fill_manual(values = c("#1f77b4", "#aec7e8", "#d62728", "#ff7f0e", "#ffbb78")) +
  labs(title = "Proporción de Estratos", x = "", y = "") +
  theme_void()

  • por Piso
# Contar el número de pisos
piso_count <- as.data.frame(table(vivienda2$piso))
colnames(piso_count) <- c("piso", "count")

# Calcular el porcentaje
piso_count <- piso_count %>%
  mutate(percentage = count / sum(count) * 100)

num_colors <- length(unique(piso_count$piso))

# Crear gráfico de pastel con porcentajes
ggplot(piso_count, aes(x = "", y = count, fill = as.factor(piso))) +
  geom_bar(width = 1, stat = "identity") +
  coord_polar(theta = "y") +
  geom_text(aes(label = sprintf("%.1f%%", percentage)), 
            position = position_stack(vjust = 0.5), 
            color = "white") +
  scale_fill_manual(values = RColorBrewer::brewer.pal(num_colors, "Set3")) +
  labs(title = "Proporción de Propiedades por Piso", x = "", y = "") +
  theme_void()

  • por Cantidad de Baños
# Contar el número de baños
banios_count <- as.data.frame(table(vivienda2$banios))
colnames(banios_count) <- c("banios", "count")

banios_count <- banios_count %>%
  mutate(percentage = count / sum(count) * 100)

num_colors <- length(unique(banios_count$banios))

ggplot(banios_count, aes(x = "", y = count, fill = as.factor(banios))) +
  geom_bar(width = 1, stat = "identity") +
  coord_polar(theta = "y") +
  geom_text(aes(label = sprintf("%.1f%%", percentage)), 
            position = position_stack(vjust = 0.5), 
            color = "white") +
  scale_fill_manual(values = RColorBrewer::brewer.pal(num_colors, "Set3")) +
  labs(title = "Proporción de Propiedades por Número de Baños", x = "", y = "") +
  theme_void()

5. Conclusiones / Observaciones finales

  • La reducción de dimensionalidad mediante PCA permitió identificar las variables numéricas más influyentes en la variabilidad del mercado inmobiliario. El 70 % de la varianza total fue capturada por los primeros 2 componentes. Estos resultados evidencian la capacidad del ACP para discernir patrones y descomponer relaciones subyacentes que dan forma a las disparidades presentes en los datos.

  • Las variables en las componentes principales brindan un enfoque estratégico para abordar futuros análisis y modelización. En contextos como la selección de modelos de regresión, las dos primeras componentes pueden actuar como predictores clave.

  • En el Análisis de Correlación, se evidenció una fuerte correlación positiva el precio mantiene una correlación y la cantidad de parqueaderos, baños, área construida y el estrato.

*La visualización gráfica brinda una mayor perspectiva de las relaciones entre las variables estrato y zona en el mercado inmobiliario. Podemos identificar patrones significativos permitiendo una segmentación más precisa del mercado y una mejor comprensión de las preferencias de los compradores en diferentes áreas geográficas.

  • En el Análisis de Conglomerados, se ejecuta un análisis de k-means con el propósito de segmentar las viviendas en conglomerados según las variables pertinentes.