Introducción

El mercado inmobiliario urbano se caracteriza por una alta heterogeneidad: propiedades con atributos físicos, ubicaciones y precios muy diversos coexisten en un mismo mercado. Para una empresa inmobiliaria, comprender la estructura subyacente de esa heterogeneidad es condición necesaria para tomar decisiones informadas sobre compra, venta y valoración de inmuebles.

El presente informe analiza una base de 8.322 ofertas de vivienda urbana, descritas por trece variables que combinan atributos físicos, localización y precio. El volumen y la dimensionalidad del conjunto hacen inviable el análisis variable por variable: se requieren técnicas capaces de tratar simultáneamente múltiples atributos y revelar patrones que el análisis univariado no detecta.

Con ese propósito se aplican tres técnicas de análisis multivariado. El análisis de componentes principales reduce la dimensionalidad del conjunto e identifica las dimensiones latentes que explican la mayor parte de la variabilidad de la oferta. El análisis de conglomerados segmenta las propiedades en grupos homogéneos que permiten caracterizar submercados. El análisis de correspondencia examina las asociaciones entre los atributos categóricos de las viviendas y su localización. Los resultados se integran en un conjunto de conclusiones y recomendaciones dirigidas a la dirección de la empresa.

Objetivos

Objetivo general

Caracterizar la estructura del mercado de vivienda urbana mediante técnicas de análisis multivariado, con el fin de identificar patrones, relaciones y segmentaciones que sustenten decisiones estratégicas de compra, venta y valoración de propiedades.

Objetivos específicos

  • Explorar y depurar la base de datos, documentando el tratamiento de valores faltantes y atípicos.
  • Reducir la dimensionalidad del conjunto de variables numéricas e identificar las dimensiones que explican la mayor proporción de la variabilidad de la oferta.
  • Segmentar las propiedades en grupos homogéneos y caracterizar cada segmento en términos de sus atributos y localización.
  • Analizar las asociaciones entre el tipo de vivienda y su ubicación dentro de la ciudad.
  • Formular recomendaciones estratégicas derivadas de los hallazgos.

Plan de trabajo

Para llegar a los resultados, el informe sigue estos pasos:

  1. Cargar la base de datos vivienda del paquete paqueteMODELOS.
  2. Depurar los datos: identificar y tratar registros vacíos, valores faltantes y posibles inconsistencias.
  3. Describir las variables: revisar tipo, distribución y frecuencias de las variables numéricas y categóricas.
  4. Analizar relaciones entre variables: correlaciones entre numéricas, y pruebas de asociación (Chi-cuadrado, Phi o V de Cramér) entre categóricas.
  5. Aplicar Análisis de Componentes Principales (ACP) sobre las variables numéricas.
  6. Aplicar Análisis de Clúster para identificar grupos de viviendas similares.
  7. Aplicar Análisis de Correspondencias sobre las variables categóricas.
  8. Concluir, integrando los hallazgos de las tres técnicas.

Cargar la base de datos

Descripción de la base de datos

Los datos provienen del portal OLX y fueron recolectados mediante un procedimiento de web scraping. El conjunto se distribuye en el paquete paqueteMODELOS bajo el nombre vivienda, y contiene 8.322 registros descritos por trece variables.

data("vivienda")

tabla_str <- data.frame(
  Variable = names(vivienda),
  Tipo     = sapply(vivienda, function(x) class(x)[1]),
  Ejemplo  = sapply(vivienda, function(x) as.character(x[1])),
  NAs      = sapply(vivienda, function(x) sum(is.na(x)))
)

knitr::kable(
  tabla_str,
  caption = "Tabla 1. Estructura de la base de datos vivienda",
  row.names = FALSE
)
Tabla 1. Estructura de la base de datos vivienda
Variable Tipo Ejemplo NAs
id numeric 1147 3
zona character Zona Oriente 3
piso character NA 2638
estrato numeric 3 3
preciom numeric 250 2
areaconst numeric 70 3
parqueaderos numeric 1 1605
banios numeric 3 3
habitaciones numeric 6 3
tipo character Casa 3
barrio character 20 de julio 3
longitud numeric -76.51168 3
latitud numeric 3.43382 3

La Tabla 1 confirma la estructura de las 13 variables. Se observa además que id no tiene significado estadístico, y que longitud y latitud describen posición geográfica más que atributos del inmueble, por lo que las tres se reservan para la representación cartográfica de los resultados y no se incluirán en el ACP. En cuanto a los datos faltantes, la tabla evidencia mayor concentración en piso y parqueaderos; su tratamiento se aborda en detalle en la sección de depuración de datos.

Exploración y depuración de los datos

Caracterización de las variables numéricas

Antes de aplicar cualquier técnica multivariada se examina la distribución de las variables numéricas. Esta revisión persigue tres fines: dimensionar las escalas de medición, cuantificar los valores faltantes, y detectar observaciones extremas. Los tres aspectos condicionan directamente los resultados posteriores, dado que el análisis de componentes principales y el de conglomerados operan sobre varianzas y distancias, y son por tanto sensibles tanto a las diferencias de escala como a la presencia de valores atípicos. Iniciamos este proceso con la variables númericas del dataset

num <- vivienda[sapply(vivienda, is.numeric)]

resumen <- data.frame(
  Variable = names(num),
  Minimo   = sapply(num, min, na.rm = TRUE),
  Q1       = sapply(num, quantile, probs = 0.25, na.rm = TRUE),
  Mediana  = sapply(num, median, na.rm = TRUE),
  Media    = sapply(num, mean, na.rm = TRUE),
  Q3       = sapply(num, quantile, probs = 0.75, na.rm = TRUE),
  Maximo   = sapply(num, max, na.rm = TRUE),
  NAs      = sapply(num, function(x) sum(is.na(x))),
  row.names = NULL
)

resumen[, -1] <- round(resumen[, -1], 2)

knitr::kable(resumen, caption = " Tabla 2. Resumen estadístico de las variables numéricas")
Tabla 2. Resumen estadístico de las variables numéricas
Variable Minimo Q1 Mediana Media Q3 Maximo NAs
id 1.00 2080.50 4160.00 4160.00 6239.50 8319.00 3
estrato 3.00 4.00 5.00 4.63 5.00 6.00 3
preciom 58.00 220.00 330.00 433.89 540.00 1999.00 2
areaconst 30.00 80.00 123.00 174.93 229.00 1745.00 3
parqueaderos 1.00 1.00 2.00 1.84 2.00 10.00 1605
banios 0.00 2.00 3.00 3.11 4.00 10.00 3
habitaciones 0.00 3.00 3.00 3.61 4.00 10.00 3
longitud -76.59 -76.54 -76.53 -76.53 -76.52 -76.46 3
latitud 3.33 3.38 3.42 3.42 3.45 3.50 3

La Tabla 2, revela cuatro hallazgos relevantes.

Escalas heterogéneas. Las variables operan en órdenes de magnitud muy distintos: preciom alcanza valores cercanos a 2.000 y areaconst supera los 1.700, mientras que banios, habitaciones y parqueaderos no exceden 10. Sin una estandarización previa, las dos primeras dominarían por completo el cálculo de componentes principales y de distancias, no por su relevancia sino por su escala. Se confirma así la necesidad de escalar las variables antes del análisis.

Distribuciones asimétricas. En preciom la media (433,9) supera ampliamente a la mediana (330), y lo mismo ocurre en areaconst (174,9 frente a 123). Este patrón indica asimetría positiva: la mayoría de las propiedades se concentra en valores bajos y una minoría de inmuebles de alto valor desplaza la media hacia arriba. El máximo de areaconst equivale a 14 veces la mediana, y el de preciom a 6 veces.

Valores faltantes de dos naturalezas. Ocho variables presentan exactamente tres faltantes, coincidencia que sugiere un patrón común. La variable parqueaderos, en contraste, presenta 1.605 (19,3%), lo que constituye un problema de otra índole.

Cobertura del estrato. La variable estrato toma valores entre 3 y 6, sin presencia de los estratos 1 y 2. La base describe por tanto el segmento medio y alto del mercado, lo que delimita el alcance de las conclusiones: los hallazgos no son extrapolables a la vivienda de interés social.

Tratamiento de valores faltantes

Identificación en variables númericas.

El análisis de valores faltantes se realiza en dos niveles: por variable, para dimensionar la magnitud del problema, y por registro, para determinar si los faltantes se concentran en observaciones específicas o se distribuyen aleatoriamente en la base. Se inicia por las variables con pocos datos faltantes (id, zona, estrato, preciom, areaconst, banios, habitaciones, tipo, barrio, longitud y latitud), ya que en estos casos es más viable identificar un patrón o parámetro que explique su ausencia y así justificar la decisión de tratamiento antes de abordar los casos más críticos: piso y parqueaderos..

vars_pocos_na <- c("id", "zona", "estrato", "preciom", "areaconst",
                    "banios", "habitaciones", "tipo", "barrio",
                    "longitud", "latitud")

# Filas donde al menos una de estas variables tiene NA
filas_na <- which(rowSums(is.na(vivienda[, vars_pocos_na])) > 0)

# Tabla con esas filas, agregando el número de registro como primera columna
tabla_na <- vivienda[filas_na, vars_pocos_na]
tabla_na <- cbind(registro = filas_na, tabla_na)

knitr::kable(
  tabla_na,
  caption = "Tabla 3.Registros con datos faltantes en variables con pocos NA",
  row.names = FALSE
)
Tabla 3.Registros con datos faltantes en variables con pocos NA
registro id zona estrato preciom areaconst banios habitaciones tipo barrio longitud latitud
8320 NA NA NA NA NA NA NA NA NA NA NA
8321 NA NA NA NA NA NA NA NA NA NA NA
8322 NA NA NA 330 NA NA NA NA NA NA NA

La inspección confirma el diagnóstico: los tres registros están vacíos en la totalidad de sus campos, incluido el identificador. Un único registro conserva el valor de preciom (330 millones), pero al carecer del resto de atributos no resulta utilizable en ninguna de las técnicas previstas. El análisis de componentes principales y el de conglomerados requieren el vector completo de variables numéricas para calcular varianzas y distancias; el análisis de correspondencia opera sobre las variables categóricas, todas ausentes en este caso.

Decisión 1 sobre datos faltates

Se eliminan los tres registros, con base en tres criterios:

  • Ausencia de información aprovechable. No son registros parcialmente incompletos susceptibles de imputación, sino filas vacías. Imputarlos equivaldría a fabricar observaciones inexistentes.
  • Impacto nulo sobre la muestra. Representan el 0,04% de las observaciones; la base conserva el 99,96% de su tamaño original.
  • Origen identificable. Su posición al final del archivo y la ausencia de identificador señalan un artefacto de exportación, no una característica del mercado.

La eliminación se implementa filtrando los registros sin identificador, criterio robusto frente a eventuales cambios en el orden de las filas.

vivienda_limpia <- vivienda[!is.na(vivienda$id), ]
print(paste("La base de datos después de la limpieza es de", nrow(vivienda_limpia), "registros"))
## [1] "La base de datos después de la limpieza es de 8319 registros"

Queda pendiente el tratamiento de parqueaderos, cuyos 1.605 valores faltantes no admiten la misma solución: su eliminación implicaría descartar casi una quinta parte de la muestra.En este caso primero de concerá mejor el pomportmiento comportamiento de la la variable, para esto seelaborará una tabla de fecuncia sin tener e ncunta los NA.

tabla_parqueaderos <- table(vivienda_limpia$parqueaderos, useNA = "ifany")

knitr::kable(
  as.data.frame(tabla_parqueaderos),
  col.names = c("Parqueaderos", "Frecuencia"),
  caption = "Tabla 4.Distribución de valores de la variable parqueaderos"
)
Tabla 4.Distribución de valores de la variable parqueaderos
Parqueaderos Frecuencia
1 3155
2 2475
3 520
4 384
5 68
6 68
7 18
8 17
9 4
10 8
NA 1602

La Tabla 4 muestra la distribución de valores que toma la variable parqueaderos: los registros van de 1 a 10, y en ningún caso aparece el valor 0. Esta ausencia es la base de la hipótesis de trabajo: los 1.602 valores faltantes no corresponden a información perdida al azar, sino a viviendas que efectivamente no cuentan con parqueadero, y que por esa razón no fueron registradas con ningún valor numérico.

Para confirmar esta hipótesis se analiza el patrón de los datos faltantes comparando el área construida, el estrato y el precio entre los registros sin dato y los niveles 1, 2 y 3 de parqueaderos (los de mayor tamaño de muestra). La expectativa es que, si el faltante corresponde efectivamente a 0 parqueaderos, sus valores se ubiquen de forma consistente por debajo del grupo con 1 parqueadero, siguiendo la misma tendencia creciente que se observa entre 1, 2 y 3. Se excluyen las categorías de 4 parqueaderos en adelante por su tamaño de muestra reducido, que haría poco confiables sus estadísticos.

Es importante aclarar que este análisis es descriptivo y no constituye una prueba estadística formal de tendencia; su propósito es reforzar, con evidencia adicional, la hipótesis planteada.

library(dplyr)

vivienda_tendencia <- vivienda_limpia %>%
  filter(parqueaderos %in% c(1, 2, 3) | is.na(parqueaderos)) %>%
  mutate(grupo_parq = ifelse(is.na(parqueaderos), "Sin dato", as.character(parqueaderos))) %>%
  mutate(grupo_parq = factor(grupo_parq, levels = c("Sin dato", "1", "2", "3")))

resumen_tendencia <- vivienda_tendencia %>%
  group_by(grupo_parq) %>%
  summarise(
    n              = n(),
    area_mediana   = median(areaconst, na.rm = TRUE),
    estrato_medio  = mean(estrato, na.rm = TRUE),
    precio_mediano = median(preciom, na.rm = TRUE)
  )

knitr::kable(
  resumen_tendencia,
  caption = "Tabla 5. Área, estrato y precio según número de parqueaderos (incluye grupo sin dato)"
)
Tabla 5. Área, estrato y precio según número de parqueaderos (incluye grupo sin dato)
grupo_parq n area_mediana estrato_medio precio_mediano
Sin dato 1602 90 3.808365 179
1 3155 86 4.303328 250
2 2475 150 5.214950 450
3 520 259 5.436539 800

Decisión 2 sobre datos faltates

La tabla 5 muestran que, en estrato y precio, el grupo sin dato se ubica de forma consistente por debajo del grupo con 1 parqueadero, siguiendo la misma tendencia creciente que se observa entre 1, 2 y 3 parqueaderos (estrato: 3.8 < 4.3 < 5.2 < 5.4; precio mediano: 179 < 250 450 < 800). El área construida, sin embargo, no sigue el mismo patrón: el grupo sin dato (90 m² de mediana) es ligeramente mayor que el grupo con 1 parqueadero (86 m²). Lejos de contradecir la hipótesis, este resultado la refuerza: si la diferencia en precio se debiera únicamente al tamaño de la vivienda, se esperaría que el grupo sin dato también tuviera menor área. Al tratarse de viviendas de tamaño similar pero con precio y estrato consistentemente más bajos, la caída de valor parece explicarse por la ausencia del parqueadero en sí, y no por otras características de la propiedad.

Con esta evidencia (ausencia del valor 0, relación con el tipo de propiedad y tendencia por estrato y precio), se considera suficientemente sustentada la hipótesis de que los valores faltantes en parqueaderos corresponden a viviendas sin parqueadero. En consecuencia, se procede a recodificar estos valores como 0. Esta operación no constituye una imputación estadística —no se está estimando un valor desconocido—, sino la corrección de una categoría real que no fue registrada explícitamente en la base de datos.

vivienda_recod <- vivienda_limpia
vivienda_recod$parqueaderos[is.na(vivienda_recod$parqueaderos)] <- 0

Identificación en variables cateóricas

Para cerrar el tratamiento de valores faltantes, se analiza la variable piso, con 2.638 valores ausentes (31.7% del total), la unica vatiable categóriaca con datos faltates idetificadas e la Table 1, y la de mayor proporción de faltantes en la base de datos. A diferencia de parqueaderos, aún no se cuenta con una hipótesis sobre el origen de estos datos, por lo que el análisis inicia con la exploración de la distribución de valores que toma la variable, con el fin de identificar posibles patrones que orienten la decisión de tratamiento.

#Se elabora la tabla de frecuncia

tabla_piso <- table(vivienda_recod$piso, useNA = "ifany")

knitr::kable(
  as.data.frame(tabla_piso),
  col.names = c("Piso", "Frecuencia"),
  caption = "Tabla 6. Distribución de valores de la variable piso"
)
Tabla 6. Distribución de valores de la variable piso
Piso Frecuencia
01 860
02 1450
03 1097
04 607
05 567
06 245
07 204
08 211
09 146
10 130
11 84
12 83
NA 2635

La Tabla 6 de frecuencias de piso muestra 12 categorías (de “01” a “12”), consistentes con el número de piso del apartamento dentro de un edificio. Bajo esta interpretación, se plantea como primera hipótesis que piso es una característica propia de los apartamentos y no aplica a las casas, al tratarse de construcciones independientes sin niveles dentro de un edificio. De confirmarse, los 2.635 valores faltantes (31.7% del total) corresponderían principalmente a registros de tipo Casa, y no a información perdida. Esta hipótesis se evalúa a continuación cruzando la variable con el tipo de vivienda.

vivienda_recod %>%
  group_by(tipo) %>%
  summarise(
    n            = n(),
    sin_piso     = sum(is.na(piso)),
    pct_sin_piso = round(100 * mean(is.na(piso)), 1)
  ) %>%
  kable(caption = "Tabla 7. Ausencia de dato en piso según tipo de vivienda") %>%
  kable_styling(full_width = FALSE)
Tabla 7. Ausencia de dato en piso según tipo de vivienda
tipo n sin_piso pct_sin_piso
Apartamento 5100 1381 27.1
Casa 3219 1254 39.0

La tabla 7 La Tabla 7, no brinda información clara sobre un posible patrón de datos que explican los datos faltantes y permita establece una estrategia de imputación, por lo tanto, se amplía las variables a comparar, incluyendo tipo de tipo, área construida, estrato y precio en función de tener dato o no.

library(dplyr)

resumen_piso_completo <- vivienda_recod %>%
  mutate(grupo = case_when(
    tipo == "Casa" & !is.na(piso)        ~ "Casa con piso",
    tipo == "Casa" & is.na(piso)         ~ "Casa sin piso",
    tipo == "Apartamento" & !is.na(piso) ~ "Apartamento con piso",
    tipo == "Apartamento" & is.na(piso)  ~ "Apartamento sin piso"
  )) %>%
  group_by(grupo) %>%
  summarise(
    n              = n(),
    area_mediana   = median(areaconst, na.rm = TRUE),
    estrato_medio  = mean(estrato, na.rm = TRUE),
    precio_mediano = median(preciom, na.rm = TRUE)
  )

knitr::kable(
  resumen_piso_completo,
  caption = "Tabla 8. Resumen de área, estrato y precio según tipo de vivienda y disponibilidad de dato en piso"
)
Tabla 8. Resumen de área, estrato y precio según tipo de vivienda y disponibilidad de dato en piso
grupo n area_mediana estrato_medio precio_mediano
Apartamento con piso 3719 90.0 4.719279 270
Apartamento sin piso 1381 91.0 4.748733 290
Casa con piso 1965 245.0 4.544020 440
Casa sin piso 1254 230.5 4.393142 400

La Tabla 8 tampoco revela un patrón concluyente: los cuatro grupos presentan perfiles de área, estrato y precio sin separación sistemática. No se logra identificar el mecanismo de ausencia: no es completamente aleatorio, pero ninguna variable observada lo explica.

Decisión 3 sobre datos faltantes

• No se imputa. Toda imputación exige sustentar que la ausencia depende de variables observadas, condición que aquí no se cumple. A ello se suma que ninguna variable de la base determina el piso de un apartamento —ni el área, ni el precio, ni el estrato—, por lo que una imputación por vecinos más cercanos asignaría el valor más frecuente al 31,7% de los registros, introduciendo precisión ficticia.

• No se eliminan registros. Resultaría innecesario: excluir la variable conserva íntegros los 8.319 registros para las tres técnicas.

• Se recodifica la ausencia como categoría explícita, “Sin registro”, aplicando la técnica del indicador de faltante. La base queda sin valores nulos y la no-respuesta documentada, sin fabricar información.

• Se excluye la variable del análisis. Del ACP y de los conglomerados, por ser categórica. Del análisis de correspondencia, porque las variables definidas para ese análisis son tipo, zona y barrio, y porque “Sin registro” no es una modalidad del mercado sino del proceso de recolección: con un peso del 31,7% dominaría los ejes factoriales, reflejando prácticas de publicación antes que patrones de oferta.

vivienda_recod$piso_cat <- ifelse(is.na(vivienda_recod$piso),
                                  "Sin registro",
                                  vivienda_recod$piso)

A manera de cierre de la identifiación y manejo de los datos faltantes, se corre nuevametes el código para idenificar datos faltante el dataset.

data.frame(
  Variable = names(vivienda_recod),
  NAs      = sapply(vivienda_recod, function(x) sum(is.na(x)))
) %>%
  kable(caption = "Tabla 9. Verificación de valores faltantes tras la depuración",
        row.names = FALSE) %>%
  kable_styling(full_width = FALSE)
Tabla 9. Verificación de valores faltantes tras la depuración
Variable NAs
id 0
zona 0
piso 2635
estrato 0
preciom 0
areaconst 0
parqueaderos 0
banios 0
habitaciones 0
tipo 0
barrio 0
longitud 0
latitud 0
piso_cat 0

Se conservan ambas versiones de la variable: piso, en su forma original, para preservar la trazabilidad del dato de origen, y piso_cat, con la ausencia recodificada como categoría explícita. Las 2.635 ausencias que registra la primera corresponden por tanto a una decisión deliberada de preservación, no a un tratamiento pendiente. Ninguna de las dos interviene en las técnicas multivariadas aplicadas.

Con esta verificación se corrobora que las tres situaciones de datos faltantes identificadas en la base han sido tratadas y documentadas: los registros vacíos fueron eliminados, las ausencias de parqueaderos recodificadas como valor cero, y las de piso recodificadas como categoría explícita y excluidas del análisis. La base de trabajo no presenta valores faltantes en ninguna de las variables que intervienen en las técnicas previstas, por lo que se da por cerrada esta etapa.

Identificación y tratamiento de valores atípicos

Resta un último aspecto de la preparación de los datos: la identificación y el tratamiento de los valores atípicos, anticipados en el análisis de la Tabla 2 al constatar la asimetría de las distribuciones de preciom y areaconst. Dado que el análisis de componentes principales opera sobre varianzas y el de conglomerados sobre distancias, ambas técnicas resultan sensibles a las observaciones extremas, lo que hace necesario examinar su naturaleza antes de decidir si corresponde conservarlas o descartarlas.

b1 <- ggplot(vivienda_recod, aes(y = preciom)) +
  geom_boxplot(fill = "steelblue") +
  labs(title = "Fig1.Precio (millones)", y = "preciom") +
  theme_minimal()

b2 <- ggplot(vivienda_recod, aes(y = areaconst)) +
  geom_boxplot(fill = "darkgreen") +
  labs(title = "Fig 2.Área construida (m²)", y = "areaconst") +
  theme_minimal()

b3 <- ggplot(vivienda_recod, aes(x = factor(estrato), y = preciom)) +
  geom_boxplot(fill = "steelblue") +
  labs(title = "Fig3. Precio por estrato", x = "Estrato", y = "preciom") +
  theme_minimal()

b4 <- ggplot(vivienda_recod, aes(x = factor(estrato), y = areaconst)) +
  geom_boxplot(fill = "darkgreen") +
  labs(title = "Fig 4.Área por estrato", x = "Estrato", y = "areaconst") +
  theme_minimal()

grid.arrange(b1, b2, b3, b4, ncol = 2)

La inspección visual revela dos patrones. Al desagregar por estrato, las cajas del precio se desplazan hacia arriba de forma sostenida, mientras que las del área construida mantienen posiciones similares: el estrato parece incidir más sobre el precio que sobre el tamaño. Los valores extremos también difieren: en precio se concentran en el estrato 5, y en área alcanzan sus máximos en el estrato 3, pese a que este presenta una de las cajas más bajas.

Estas son impresiones visuales, no mediciones. El diagrama de caja representa cuartiles mediante posiciones, por lo que permite reconocer patrones pero no cuantificarlos. Para sustentar las afirmaciones anteriores se calculan a continuación los estadísticos correspondientes.

# Estadísticos de posición de precio y área, calculados dentro de cada estrato.
# group_by parte la base en 4 grupos; summarise resume cada grupo en una fila.
vivienda_recod %>%
  group_by(estrato) %>%
  summarise(
    n              = n(),
    precio_mediana = median(preciom),
    precio_Q1      = quantile(preciom, 0.25),
    precio_Q3      = quantile(preciom, 0.75),
    precio_max     = max(preciom),
    area_mediana   = median(areaconst),
    area_Q1        = quantile(areaconst, 0.25),
    area_Q3        = quantile(areaconst, 0.75),
    area_max       = max(areaconst)
  ) %>%
  kable(caption = "Tabla 10. Precio y área construida por estrato") %>%
  kable_styling(full_width = FALSE)
Tabla 10. Precio y área construida por estrato
estrato n precio_mediana precio_Q1 precio_Q3 precio_max area_mediana area_Q1 area_Q3 area_max
3 1453 160 120 270 1600 91 60 190.00 1745
4 2129 235 165 335 1800 83 64 159.42 1000
5 2750 350 270 470 1999 115 90 217.03 1200
6 1987 700 515 990 1950 198 136 304.50 1600

Para evaluar la coherencia interna de los registros extremos se calcula el precio por metro cuadrado, indicador que permite distinguir entre errores de digitación y propiedades efectivamente valorizadas. La variable se emplea con fines diagnósticos y no se incorpora al análisis de componentes principales, por tratarse de un cociente entre dos variables ya incluidas en él.

# mutate agrega una columna nueva a la base.
# Al asignar con <- sobre el mismo objeto, la columna queda guardada.
vivienda_recod <- vivienda_recod %>%
  mutate(precio_m2 = preciom / areaconst)
# Cuenta cuántos registros superan el bigote superior del boxplot en cada
# variable. El umbral (Q3 + 1.5*IQR) se recalcula dentro de cada estrato,
# de modo que cada vivienda se compara contra su propio grupo.
vivienda_recod %>%
  group_by(estrato) %>%
  summarise(
    n           = n(),
    atip_precio = sum(preciom > quantile(preciom, 0.75) + 1.5 * IQR(preciom)),
    pct_precio  = round(100 * atip_precio / n, 1),
    atip_area   = sum(areaconst > quantile(areaconst, 0.75) + 1.5 * IQR(areaconst)),
    pct_area    = round(100 * atip_area / n, 1),
    atip_m2     = sum(precio_m2 > quantile(precio_m2, 0.75) + 1.5 * IQR(precio_m2)),
    pct_m2      = round(100 * atip_m2 / n, 1)
  ) %>%
  kable(caption = "Tabla 11. Valores atípicos por estrato") %>%
  kable_styling(full_width = FALSE)
Tabla 11. Valores atípicos por estrato
estrato n atip_precio pct_precio atip_area pct_area atip_m2 pct_m2
3 1453 50 3.4 55 3.8 32 2.2
4 2129 86 4.0 166 7.8 20 0.9
5 2750 155 5.6 178 6.5 9 0.3
6 1987 49 2.5 97 4.9 13 0.7
# Rango del precio por metro cuadrado en cada estrato.
vivienda_recod %>%
  group_by(estrato) %>%
  summarise(
    minimo  = round(min(precio_m2), 2),
    Q1      = round(quantile(precio_m2, 0.25), 2),
    mediana = round(median(precio_m2), 2),
    Q3      = round(quantile(precio_m2, 0.75), 2),
    maximo  = round(max(precio_m2), 2)
  ) %>%
  kable(caption = "Tabla 12. Precio por metro cuadrado según estrato (millones)") %>%
  kable_styling(full_width = FALSE)
Tabla 12. Precio por metro cuadrado según estrato (millones)
estrato minimo Q1 mediana Q3 maximo
3 0.15 1.32 1.71 2.10 7.00
4 0.38 1.96 2.50 2.90 7.74
5 0.28 2.10 2.82 3.42 9.47
6 0.85 2.83 3.66 4.37 8.30

El precio mediano crece de manera sostenida entre estratos —160, 235, 350 y 700 millones—, mientras que el área mediana lo hace de forma irregular: 91, 83, 115 y 198 m², con un descenso en el estrato 4. Entre los extremos, el precio se multiplica por más de cuatro y el área apenas por dos: la valorización responde más al estrato y la localización que a los metros construidos.

Los valores atípicos representan una proporción reducida en todos los estratos: entre 2,5% y 5,6% en precio, y entre 3,8% y 7,8% en área. El estrato 5, que en el gráfico parecía concentrar los extremos de precio, presenta 155 casos equivalentes al 5,6% de sus 2.750 registros: la densidad observada respondía al mayor tamaño del grupo y no a una proporción superior. El estrato 6 registra la menor proporción en precio (2,5%), consistente con que allí los valores elevados constituyen el comportamiento habitual.

El precio por metro cuadrado asciende de forma consistente con el estrato —1,71, 2,50, 2,82 y 3,66 millones—, lo que confirma que la clasificación socioeconómica ordena de manera coherente el valor unitario del suelo. Los registros extremos en este indicador son escasos y decrecen al ascender de estrato, del 2,2% en el estrato 3 al 0,3% en el estrato 5. Este patrón sugiere que en los estratos bajos una vivienda con atributos superiores se distingue con facilidad de su grupo, mientras que en los altos la calidad está ya incorporada al precio típico. Las causas de esa valorización diferencial —calidad de acabados, estado de conservación, ubicación específica dentro del barrio— corresponden a factores no observados en la base.

Decisión: se conservan los valores atípicos. El análisis por estrato muestra que no se trata de errores de registro sino de propiedades de alto valor y gran tamaño efectivamente presentes en el mercado. El precio por metro cuadrado de estos registros resulta coherente con el de su estrato, lo que descarta inconsistencias de digitación. Eliminarlas supondría excluir el segmento premium, de mayor margen y evidente interés estratégico para la empresa. Su influencia se atenúa por vía metodológica: el análisis de componentes principales se realizará sobre variables estandarizadas, lo que reduce el peso de las escalas extremas sin descartar información. Es previsible que estas propiedades conformen un segmento diferenciado en el análisis de conglomerados.

Cabe señalar, por último, la posibilidad de que la base agrupe inmuebles de usos distintos —vivienda unifamiliar, edificaciones divididas o propiedades de uso mixto— bajo las dos categorías de la variable tipo. La base no incluye información que permita verificarlo directamente.

Con esto concluye la etapa de preparación de los datos. La base cuenta con 8.319 registros sin valores faltantes en las variables de análisis, con los valores extremos identificados, dimensionados y conservados de forma justificada.

Análisis de Componentes Principales

Fundamento de la técnica

El análisis de componentes principales es una técnica de reducción de dimensionalidad que transforma un conjunto de variables correlacionadas en un número menor de variables nuevas, denominadas componentes principales, que son combinaciones lineales de las originales y no están correlacionadas entre sí. Cada componente se construye de modo que recoja la mayor cantidad posible de la varianza no explicada por las anteriores: la primera captura la máxima variabilidad del conjunto, la segunda la máxima variabilidad restante, y así sucesivamente.

Su utilidad en este estudio es doble. Por una parte, permite identificar qué combinaciones de atributos concentran la variación observada en la oferta inmobiliaria, respondiendo a la pregunta de qué características diferencian realmente a unas propiedades de otras. Por otra, produce un conjunto reducido de variables no correlacionadas que sirve de insumo al análisis de conglomerados, evitando que la redundancia entre atributos distorsione la medición de distancias entre propiedades.

La técnica requiere que las variables sean numéricas y que exista correlación apreciable entre ellas: si fueran mutuamente independientes, no habría redundancia que resumir y el procedimiento carecería de objeto. Por ello el análisis se inicia con el examen de la matriz de correlaciones.

Variables incluidas y verificación de supuestos

Se incorporan al análisis las seis variables numéricas de la base: estrato, preciom, areaconst, parqueaderos, banios y habitaciones. Se excluyen id, por tratarse de un identificador sin contenido informativo, y longitud y latitud, que corresponden a coordenadas geográficas y no a atributos del inmueble. Se excluye asimismo precio_m2, calculada con fines diagnósticos, por ser un cociente entre dos variables ya incluidas.

La variable estrato, de naturaleza ordinal, se trata como numérica dado que sus niveles guardan un orden y una distancia interpretable en términos socioeconómicos. Las variables parqueaderos, banios y habitaciones son discretas; su inclusión es admisible porque las diferencias entre sus valores son interpretables de manera uniforme.

datos_acp <- vivienda_recod %>%
  select(estrato, preciom, areaconst, parqueaderos, banios, habitaciones)

data.frame(
  Variable = names(datos_acp),
  NAs      = colSums(is.na(datos_acp))
) %>%
  kable(caption = "Tabla 13. Verificación de valores faltantes en las variables del ACP",
        row.names = FALSE) %>%
  kable_styling(full_width = FALSE)
Tabla 13. Verificación de valores faltantes en las variables del ACP
Variable NAs
estrato 0
preciom 0
areaconst 0
parqueaderos 0
banios 0
habitaciones 0
# Correlación de Pearson: mide asociación lineal. Es la que emplea el ACP.
round(cor(datos_acp, method = "pearson"), 3) %>%
  kable(caption = "Tabla 13. Matriz de correlaciones de Pearson") %>%
  kable_styling(full_width = FALSE)
Tabla 13. Matriz de correlaciones de Pearson
estrato preciom areaconst parqueaderos banios habitaciones
estrato 1.000 0.610 0.274 0.513 0.420 -0.071
preciom 0.610 1.000 0.687 0.640 0.669 0.264
areaconst 0.274 0.687 1.000 0.482 0.648 0.517
parqueaderos 0.513 0.640 0.482 1.000 0.523 0.199
banios 0.420 0.669 0.648 0.523 1.000 0.590
habitaciones -0.071 0.264 0.517 0.199 0.590 1.000
# Correlación de Spearman: se calcula sobre rangos, por lo que es robusta
# frente a los valores extremos que se decidió conservar.
round(cor(datos_acp, method = "spearman"), 3) %>%
  kable(caption = "Tabla 14. Matriz de correlaciones de Spearman") %>%
  kable_styling(full_width = FALSE)
Tabla 14. Matriz de correlaciones de Spearman
estrato preciom areaconst parqueaderos banios habitaciones
estrato 1.000 0.710 0.395 0.586 0.471 0.040
preciom 0.710 1.000 0.822 0.660 0.768 0.427
areaconst 0.395 0.822 1.000 0.507 0.771 0.651
parqueaderos 0.586 0.660 0.507 1.000 0.547 0.250
banios 0.471 0.768 0.771 0.547 1.000 0.621
habitaciones 0.040 0.427 0.651 0.250 0.621 1.000

Se examinan las correlaciones mediante dos coeficientes complementarios. El de Pearson mide asociación lineal y constituye la base del análisis de componentes principales. El de Spearman, calculado sobre rangos, es robusto frente a los valores extremos conservados en la etapa anterior, por lo que permite verificar que la estructura de asociación no dependa de ellos.

Aplicación del análisis

El análisis se realiza sobre variables estandarizadas, es decir, sobre la matriz de correlaciones y no sobre la de covarianzas. Esta decisión responde a la heterogeneidad de las escalas documentada en el análisis descriptivo: el precio se expresa en millones de pesos y alcanza valores del orden de dos mil, mientras que el número de baños o habitaciones no supera las dos cifras. Sin estandarizar, las variables de mayor magnitud dominarían por completo las componentes, no por su relevancia sino por su unidad de medida. La estandarización iguala el peso inicial de todas las variables y tiene el efecto adicional de atenuar la influencia de los valores extremos.

# prcomp calcula las componentes principales.
# scale. = TRUE estandariza las variables antes del cálculo.
acp <- prcomp(datos_acp, scale. = TRUE)

# Tabla de varianza explicada.
# acp$sdev son las desviaciones estándar de cada componente;
# al elevarlas al cuadrado se obtiene la varianza (el autovalor).
varianza <- data.frame(
  Componente = paste0("CP", 1:length(acp$sdev)),
  Autovalor  = round(acp$sdev^2, 3),
  Prop       = round(100 * acp$sdev^2 / sum(acp$sdev^2), 2),
  Acumulada  = round(100 * cumsum(acp$sdev^2) / sum(acp$sdev^2), 2)
)

kable(varianza, caption = "Tabla 15. Varianza explicada por componente") %>%
  kable_styling(full_width = FALSE)
Tabla 15. Varianza explicada por componente
Componente Autovalor Prop Acumulada
CP1 3.415 56.92 56.92
CP2 1.259 20.99 77.91
CP3 0.460 7.66 85.57
CP4 0.435 7.24 92.82
CP5 0.241 4.02 96.83
CP6 0.190 3.17 100.00
# Gráfico de sedimentación: barras = varianza de cada componente,
# línea roja = varianza acumulada.
ggplot(varianza, aes(x = Componente, y = Prop, group = 1)) +
  geom_col(fill = "steelblue") +
  geom_line(aes(y = Acumulada), color = "darkred", linewidth = 1) +
  geom_point(aes(y = Acumulada), color = "darkred") +
  geom_hline(yintercept = 100 / ncol(datos_acp), linetype = "dashed") +
  labs(title = "Fig 5. Gráfico de sedimentación",
       subtitle = "La línea punteada marca el criterio de Kaiser (autovalor = 1)",
       x = "", y = "% de varianza") +
  theme_minimal()

Criterio de retención de componentes

La decisión sobre cuántas componentes conservar se apoya en tres criterios de uso extendido, que conviene contrastar entre sí:

  • Criterio de Kaiser: se retienen las componentes con autovalor superior a 1, es decir, aquellas que explican más varianza que una variable original por sí sola. Con seis variables estandarizadas, cada una aporta un autovalor de 1, de modo que una componente por debajo de ese umbral no justifica su inclusión.
  • Gráfico de sedimentación: se observa el punto en que la curva de autovalores deja de descender pronunciadamente y se aplana. Las componentes anteriores a ese codo concentran la estructura; las posteriores aportan variación residual.
  • Varianza acumulada: se retienen las componentes necesarias para alcanzar un porcentaje de varianza explicada considerado suficiente, habitualmente entre el 70% y el 80%.
# Cargas: peso de cada variable original en cada componente.
# Valores altos en magnitud (positivos o negativos) indican que la variable
# contribuye fuertemente a esa componente.
round(acp$rotation, 3) %>%
  kable(caption = "Tabla 16. Cargas de las variables en cada componente") %>%
  kable_styling(full_width = FALSE)
Tabla 16. Cargas de las variables en cada componente
PC1 PC2 PC3 PC4 PC5 PC6
estrato 0.331 -0.582 0.555 0.077 0.461 -0.159
preciom 0.478 -0.188 -0.036 -0.361 -0.220 0.746
areaconst 0.442 0.239 -0.288 -0.618 0.279 -0.453
parqueaderos 0.410 -0.261 -0.657 0.571 0.048 -0.070
banios 0.467 0.192 0.363 0.185 -0.679 -0.344
habitaciones 0.285 0.681 0.211 0.349 0.445 0.300

Interpretación de las componentes

Las cargas indican el peso de cada variable original en la construcción de cada componente. Su lectura permite asignar un significado sustantivo a dimensiones que en principio son constructos matemáticos: una componente en la que todas las variables cargan con el mismo signo y magnitud similar representa un efecto de tamaño general, mientras que una en la que unas variables cargan positivamente y otras negativamente representa un contraste entre atributos.

# Representación de las variables en el plano de las dos primeras componentes.
# Cada flecha es una variable: su dirección indica con qué componente se
# asocia y su longitud, cuán bien representada queda en este plano.
cargas <- as.data.frame(acp$rotation[, 1:2])
cargas$variable <- rownames(cargas)

ggplot(cargas, aes(x = PC1, y = PC2)) +
  geom_segment(aes(x = 0, y = 0, xend = PC1, yend = PC2),
               arrow = arrow(length = unit(0.25, "cm")), color = "steelblue") +
  geom_text(aes(label = variable), vjust = -0.6, size = 3.5) +
  geom_hline(yintercept = 0, linetype = "dashed", color = "grey60") +
  geom_vline(xintercept = 0, linetype = "dashed", color = "grey60") +
  labs(title = "Fig 6. Variables en el plano de las dos primeras componentes",
       x = "Componente 1", y = "Componente 2") +
  theme_minimal()

En este gráfico, las variables representadas por flechas próximas entre sí se encuentran correlacionadas positivamente; las que apuntan en direcciones opuestas, negativamente; y las perpendiculares resultan independientes. La longitud de cada flecha indica el grado en que la variable queda representada en el plano de las dos primeras componentes.

Análisis de Conglomerados

Fundamento de la técnica

El análisis de conglomerados agrupa observaciones en segmentos de modo que las propiedades pertenecientes a un mismo grupo resulten similares entre sí y diferentes de las de los demás grupos. A diferencia de las técnicas de clasificación supervisada, no parte de categorías conocidas: los segmentos emergen de la estructura de los propios datos.

Se emplea el algoritmo de k-medias, que procede de forma iterativa. Fijado un número k de grupos, el algoritmo sitúa k centros iniciales, asigna cada observación al centro más próximo, recalcula la posición de cada centro como el promedio de las observaciones asignadas, y repite el proceso hasta que las asignaciones se estabilizan. Se selecciona por su eficiencia computacional, adecuada para una base de 8.319 registros, frente a los métodos jerárquicos, que requieren calcular y almacenar la matriz de distancias entre todos los pares de observaciones.

El análisis se aplica sobre las componentes principales retenidas y no sobre las variables originales. Esta decisión obedece a que las distancias euclidianas resultan distorsionadas cuando las variables están correlacionadas: los atributos redundantes se cuentan varias veces y pesan en exceso en la conformación de los grupos. Las componentes principales, al ser ortogonales entre sí, eliminan ese problema.

# Se toman las puntuaciones de las observaciones en las componentes retenidas.
# acp$x contiene las coordenadas de cada vivienda en cada componente.
# AJUSTAR el número 2 según las componentes que se hayan decidido retener.
puntuaciones <- acp$x[, 1:2]

Determinación del número de conglomerados

El algoritmo de k-medias requiere fijar de antemano el número de grupos, que no se deriva de los datos sino que constituye una decisión del analista. Para sustentarla se emplea el método del codo, que evalúa cómo disminuye la suma de cuadrados dentro de los grupos a medida que aumenta k. Esta medida siempre decrece al añadir grupos, de modo que el criterio no es minimizarla sino identificar el punto a partir del cual las reducciones adicionales se vuelven marginales.

# set.seed fija la semilla aleatoria para que el resultado sea reproducible:
# k-medias parte de centros aleatorios, y sin semilla el resultado varía.
set.seed(123)

# Se calcula la suma de cuadrados intragrupo para k de 1 a 10.
inercia <- numeric(10)
for (k in 1:10) {
  modelo <- kmeans(puntuaciones, centers = k, nstart = 10)
  inercia[k] <- modelo$tot.withinss
}

data.frame(k = 1:10, inercia = inercia) %>%
  ggplot(aes(x = k, y = inercia)) +
  geom_line(color = "steelblue", linewidth = 1) +
  geom_point(color = "steelblue", size = 2) +
  scale_x_continuous(breaks = 1:10) +
  labs(title = "Fig 7. Método del codo",
       x = "Número de conglomerados (k)",
       y = "Suma de cuadrados intragrupo") +
  theme_minimal()

# AJUSTAR centers según el número de conglomerados decidido.
set.seed(123)
modelo_km <- kmeans(puntuaciones, centers = 4, nstart = 25)

# Se incorpora la asignación de cada vivienda a la base.
vivienda_recod$cluster <- factor(modelo_km$cluster)

# Tamaño de cada conglomerado.
vivienda_recod %>%
  count(cluster, name = "Viviendas") %>%
  mutate(Porcentaje = round(100 * Viviendas / sum(Viviendas), 1)) %>%
  kable(caption = "Tabla 17. Tamaño de los conglomerados") %>%
  kable_styling(full_width = FALSE)
Tabla 17. Tamaño de los conglomerados
cluster Viviendas Porcentaje
1 2634 31.7
2 3787 45.5
3 869 10.4
4 1029 12.4

Caracterización de los conglomerados

La utilidad de una segmentación depende de que los grupos obtenidos admitan una descripción sustantiva. Para ello se examinan los valores típicos de cada atributo en cada conglomerado, empleando la mediana por su robustez frente a los valores extremos conservados.

vivienda_recod %>%
  group_by(cluster) %>%
  summarise(
    n            = n(),
    estrato      = round(median(estrato), 1),
    precio       = round(median(preciom), 1),
    area         = round(median(areaconst), 1),
    precio_m2    = round(median(precio_m2), 2),
    habitaciones = round(median(habitaciones), 1),
    banios       = round(median(banios), 1),
    parqueaderos = round(median(parqueaderos), 1)
  ) %>%
  kable(caption = "Tabla 18. Perfil de los conglomerados (valores medianos)") %>%
  kable_styling(full_width = FALSE)
Tabla 18. Perfil de los conglomerados (valores medianos)
cluster n estrato precio area precio_m2 habitaciones banios parqueaderos
1 2634 5 450 149 3.19 3 3 2
2 3787 4 210 77 2.56 3 2 1
3 869 4 400 280 1.45 6 4 1
4 1029 6 1050 348 2.92 4 5 3
# Representación de las viviendas en el plano de las dos primeras componentes,
# coloreadas según el conglomerado al que fueron asignadas.
data.frame(
  CP1 = acp$x[, 1],
  CP2 = acp$x[, 2],
  cluster = vivienda_recod$cluster
) %>%
  ggplot(aes(x = CP1, y = CP2, color = cluster)) +
  geom_point(alpha = 0.3, size = 0.8) +
  labs(title = "Fig 8. Conglomerados en el plano de las componentes principales",
       x = "Componente 1", y = "Componente 2", color = "Grupo") +
  theme_minimal()

# Distribución de los conglomerados por zona.
table(vivienda_recod$cluster, vivienda_recod$zona) %>%
  kable(caption = "Tabla 19. Conglomerados según zona de la ciudad") %>%
  kable_styling(full_width = FALSE)
Tabla 19. Conglomerados según zona de la ciudad
Zona Centro Zona Norte Zona Oeste Zona Oriente Zona Sur
2 533 694 3 1402
71 1080 166 194 2276
50 197 45 153 424
1 110 293 1 624
# Distribución de los conglomerados por tipo de vivienda.
table(vivienda_recod$cluster, vivienda_recod$tipo) %>%
  kable(caption = "Tabla 20. Conglomerados según tipo de vivienda") %>%
  kable_styling(full_width = FALSE)
Tabla 20. Conglomerados según tipo de vivienda
Apartamento Casa
1762 872
3008 779
17 852
313 716

El cruce de los conglomerados con las variables de localización y tipo permite establecer si los segmentos identificados presentan una expresión territorial diferenciada, aspecto de interés directo para las decisiones de inversión y comercialización de la empresa.

Análisis de Correspondencia

Fundamento de la técnica

El análisis de correspondencia examina la asociación entre variables categóricas a partir de una tabla de contingencia, representando sus categorías en un espacio de pocas dimensiones. Su lógica es análoga a la del análisis de componentes principales, pero opera sobre frecuencias en lugar de sobre varianzas: descompone la inercia de la tabla, medida que cuantifica cuánto se apartan las frecuencias observadas de las que cabría esperar si las variables fueran independientes.

El resultado es un mapa en el que la proximidad entre categorías de una misma variable indica perfiles similares, y la proximidad entre categorías de variables distintas indica asociación. La técnica es descriptiva: no contrasta hipótesis, sino que revela la estructura de las relaciones. Por ello suele acompañarse de la prueba chi-cuadrado, que sí permite establecer si la asociación observada es estadísticamente significativa.

En este estudio se aplica a las variables tipo, zona y barrio, con el propósito de identificar patrones de localización de la oferta según el tipo de inmueble.

# Ejecutar UNA VEZ en la consola, no en el documento:
# install.packages("ca")
library(ca)

Asociación entre tipo de vivienda y zona

# Tabla de contingencia: frecuencias observadas de cada combinación.
tabla_tz <- table(vivienda_recod$tipo, vivienda_recod$zona)

tabla_tz %>%
  kable(caption = "Tabla 21. Distribución de la oferta por tipo y zona") %>%
  kable_styling(full_width = FALSE)
Tabla 21. Distribución de la oferta por tipo y zona
Zona Centro Zona Norte Zona Oeste Zona Oriente Zona Sur
Apartamento 24 1198 1029 62 2787
Casa 100 722 169 289 1939
# Perfiles fila: composición porcentual de cada tipo entre las zonas.
# round(prop.table(tabla, 1), 3) calcula porcentajes por fila.
round(100 * prop.table(tabla_tz, 1), 1) %>%
  kable(caption = "Tabla 22. Perfiles fila: distribución zonal de cada tipo (%)") %>%
  kable_styling(full_width = FALSE)
Tabla 22. Perfiles fila: distribución zonal de cada tipo (%)
Zona Centro Zona Norte Zona Oeste Zona Oriente Zona Sur
Apartamento 0.5 23.5 20.2 1.2 54.6
Casa 3.1 22.4 5.3 9.0 60.2
# Prueba chi-cuadrado de independencia.
chisq.test(tabla_tz)
## 
##  Pearson's Chi-squared test
## 
## data:  tabla_tz
## X-squared = 690.93, df = 4, p-value < 2.2e-16

La prueba chi-cuadrado contrasta la hipótesis nula de independencia entre las dos variables. Debe advertirse que, con un tamaño muestral de 8.319 registros, la prueba detecta como significativas asociaciones de magnitud reducida, por lo que el resultado se interpreta junto con los perfiles de la Tabla 22 y no de manera aislada.

Cabe señalar una limitación de orden técnico. El número máximo de dimensiones que puede extraer un análisis de correspondencia equivale al menor valor entre el número de filas menos uno y el número de columnas menos uno. Dado que tipo presenta únicamente dos categorías, el análisis de esta tabla admite una sola dimensión, insuficiente para construir un mapa factorial. La asociación entre tipo y zona se interpreta, por tanto, mediante los perfiles porcentuales y la prueba chi-cuadrado.

Asociación entre barrio y estrato

Para obtener una representación factorial que admita interpretación bidimensional se requiere que ambas variables presenten al menos tres categorías. Se analiza en consecuencia la relación entre barrio y estrato, que permite caracterizar la composición socioeconómica de la oferta por localización.

# Número de barrios distintos en la base.
n_distinct(vivienda_recod$barrio)
## [1] 436

Dado el elevado número de barrios, incluirlos todos produciría un mapa ilegible y otorgaría el mismo peso visual a barrios con centenares de registros y a otros con unos pocos. Se restringe el análisis a los quince barrios con mayor número de propiedades ofertadas, que concentran una proporción sustantiva de la oferta y resultan relevantes para las decisiones comerciales de la empresa.

# Se identifican los 15 barrios con más registros.
top_barrios <- vivienda_recod %>%
  count(barrio, sort = TRUE) %>%
  head(15) %>%
  pull(barrio)

# Se filtra la base a esos barrios y se construye la tabla de contingencia.
datos_ca <- vivienda_recod %>%
  filter(barrio %in% top_barrios)

tabla_be <- table(datos_ca$barrio, datos_ca$estrato)

tabla_be %>%
  kable(caption = "Tabla 23. Oferta por barrio y estrato (quince barrios principales)") %>%
  kable_styling(full_width = FALSE)
Tabla 23. Oferta por barrio y estrato (quince barrios principales)
3 4 5 6
acopi 21 51 59 27
aguacatal 20 24 4 61
ciudad jardín 2 4 66 444
el caney 9 165 34 0
el ingenio 0 9 156 37
el limonar 6 36 91 2
el refugio 5 87 28 0
la flora 1 33 326 6
la hacienda 2 29 132 1
los cristales 0 3 44 107
normandía 1 0 22 131
pance 0 2 20 387
prados del norte 3 74 49 0
santa teresita 0 4 27 231
valle del lili 4 576 423 5
# Proporción de la oferta total que representan estos barrios.
round(100 * nrow(datos_ca) / nrow(vivienda_recod), 1)
## [1] 49.2
# ca() calcula el análisis de correspondencia.
ca_be <- ca(tabla_be)

# summary muestra la inercia de cada dimensión y las coordenadas.
summary(ca_be)
## 
## Principal inertias (eigenvalues):
## 
##  dim    value      %   cum%   scree plot               
##  1      0.740922  71.2  71.2  ******************       
##  2      0.223668  21.5  92.6  *****                    
##  3      0.076716   7.4 100.0  **                       
##         -------- -----                                 
##  Total: 1.041306 100.0                                 
## 
## 
## Rows:
##      name   mass  qlt  inr    k=1 cor ctr    k=2 cor ctr  
## 1  | acop |   39  183   31 | -339 138   6 | -194  45   6 |
## 2  | agct |   27  303   50 |  456 107   7 | -616 196  45 |
## 3  | cddj |  126  996  139 | 1071 996 195 |  -25   1   0 |
## 4  | elcn |   51  999   74 | -872 499  52 | -873 500 173 |
## 5  | elng |   49  999   36 | -238  76   4 |  832 924 153 |
## 6  | ellm |   33  896   20 | -637 643  18 |  399 253  23 |
## 7  | elrf |   29 1000   34 | -846 593  28 | -701 407  64 |
## 8  | lflr |   89  998  105 | -574 269  40 |  944 728 356 |
## 9  | lhcn |   40  994   35 | -626 428  21 |  719 566  93 |
## 10 | lscr |   38  993   22 |  749 935  28 |  186  57   6 |
## 11 | nrmn |   38  999   40 | 1056 999  57 |    0   0   0 |
## 12 | panc |  100  992  149 | 1235 982 206 | -127  10   7 |
## 13 | prds |   31  995   22 | -796 862  26 | -314 134  14 |
## 14 | sntt |   64  992   76 | 1107 989 106 |  -63   3   1 |
## 15 | vlld |  246  948  167 | -785 874 205 | -228  74  57 |
## 
## Columns:
##     name   mass  qlt  inr    k=1 cor ctr    k=2 cor ctr  
## 1 |    3 |   18  128   79 | -284  18   2 | -711 111  41 |
## 2 |    4 |  268  986  273 | -830 649 249 | -598 337 429 |
## 3 |    5 |  362  999  195 | -491 430 118 |  565 570 518 |
## 4 |    6 |  352  999  452 | 1153 993 631 |  -89   6  13 |
# Mapa factorial. Los barrios y los estratos se representan en el mismo plano.
plot(ca_be,
     main = "Fig 9. Mapa de correspondencia: barrio y estrato",
     col = c("steelblue", "darkred"))

En el mapa, los barrios situados próximos entre sí presentan una composición por estrato semejante, y los que aparecen cercanos a un estrato determinado concentran su oferta en ese nivel. La distancia respecto del origen indica el grado en que una categoría se aparta del perfil promedio: las categorías próximas al centro presentan una distribución similar a la del conjunto, mientras que las alejadas corresponden a perfiles distintivos.

Conclusiones

Recomendaciones

Anexos

Anexo 1. Código completo

El código empleado en la elaboración de este informe se encuentra disponible en el propio documento. Puede visualizarse mediante el botón “Code” situado en el extremo superior derecho de cada bloque, o desplegarse en su totalidad desde el menú “Code” del encabezado del documento.

Anexo 2. Información de la sesión

# Versiones de R y de los paquetes empleados, para garantizar
# la reproducibilidad del análisis.
sessionInfo()
## R version 4.5.2 (2025-10-31 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26200)
## 
## Matrix products: default
##   LAPACK version 3.12.1
## 
## locale:
## [1] LC_COLLATE=Spanish_Colombia.utf8  LC_CTYPE=Spanish_Colombia.utf8   
## [3] LC_MONETARY=Spanish_Colombia.utf8 LC_NUMERIC=C                     
## [5] LC_TIME=Spanish_Colombia.utf8    
## 
## time zone: America/Bogota
## tzcode source: internal
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
##  [1] ca_0.71.1            kableExtra_1.4.1     dplyr_1.2.0         
##  [4] paqueteMODELOS_0.1.0 summarytools_1.1.5   knitr_1.51          
##  [7] gridExtra_2.3.1      GGally_2.4.0         ggplot2_4.0.2       
## [10] broom_1.0.13         boot_1.3-32         
## 
## loaded via a namespace (and not attached):
##  [1] sass_0.4.10        generics_0.1.4     tidyr_1.3.2        xml2_1.5.2        
##  [5] tcltk_4.5.2        stringi_1.8.7      digest_0.6.39      magrittr_2.0.4    
##  [9] evaluate_1.0.5     grid_4.5.2         timechange_0.4.0   RColorBrewer_1.1-3
## [13] fastmap_1.2.0      plyr_1.8.9         jsonlite_2.0.0     backports_1.5.0   
## [17] rapportools_1.2    purrr_1.2.1        pander_0.6.6       viridisLite_0.4.3 
## [21] scales_1.4.0       textshaping_1.0.5  jquerylib_0.1.4    cli_3.6.6         
## [25] rlang_1.3.0        base64enc_0.1-6    withr_3.0.2        cachem_1.1.0      
## [29] yaml_2.3.12        otel_0.2.0         tools_4.5.2        reshape2_1.4.5    
## [33] checkmate_2.3.4    ggstats_0.13.0     vctrs_0.7.1        R6_2.6.1          
## [37] matrixStats_1.5.0  lifecycle_1.0.5    lubridate_1.9.5    magick_2.9.1      
## [41] stringr_1.6.0      MASS_7.3-65        pkgconfig_2.0.3    pillar_1.11.1     
## [45] bslib_0.10.0       gtable_0.3.6       glue_1.8.0         Rcpp_1.1.1        
## [49] systemfonts_1.3.2  xfun_0.56          tibble_3.3.1       tidyselect_1.2.1  
## [53] rstudioapi_0.18.0  farver_2.1.2       htmltools_0.5.9    labeling_0.4.3    
## [57] svglite_2.2.2      rmarkdown_2.30     compiler_4.5.2     S7_0.2.1