# -------------------------------------------------------------------------
# Carga de la base de datos
# -------------------------------------------------------------------------
data("vivienda")

# Copia de trabajo para conservar intacta la base original
inmuebles <- vivienda

#str(inmuebles)

1 1. Resumen ejecutivo

En este informe se desarrolla una evaluación holística de la oferta inmobiliaria urbana mediante técnicas de análisis multivariado. La base contiene información de localización, características físicas y precio comercial de las viviendas. Más allá de limitarse a describir cada variable de manera aislada, se realiza un estudio simultaneo de sus relaciones:

  1. Se identifican las dimensiones cuantitativas que concentran la mayor variabilidad de la oferta;
  2. Se construye segmentos de inmuebles con características internas homogéneas;
  3. Se examinan asociaciones entre las categorías de tipo de vivienda, zona y estrato;
  4. Se analizan los resultados estadísticos para determinar hallazgos y recomendaciones útiles que permitan tomar decisiones.

El desarrollo se hace en base a lo aprendido en el curso: comprensión y depuración de los datos, análisis exploratorio, matriz de correlaciones, análisis de componentes principales, análisis de conglomerados, análisis de correspondencia y comunicación de resultados.

2 2. Planteamiento del problema

Una empresa inmobiliaria necesita comprender la estructura del mercado de viviendas urbanas para orientar decisiones de compra, venta, valoración, focalización comercial y diseño de portafolio. La oferta no depende únicamente del precio, intervienen otras características del predio, tales como: el área construida, el número de habitaciones, los baños, número de parqueaderos, el estrato socioeconómico, el tipo de vivienda y la ubicación.

Por lo cual, el problema se analiza con un enfoque multivariado, porque varias variables actúan de manera simultánea y pueden presentar relaciones, redundancias y patrones de agrupación que no serían visibles mediante análisis univariados separados.

2.1 2.1 Pregunta de análisis

¿Qué dimensiones, segmentos y asociaciones caracterizan la oferta inmobiliaria urbana registrada en la base de datos vivienda, y cómo pueden estos resultados permitir decisiones estratégicas de la empresa?

2.2 2.2 Objetivos

2.2.1 Objetivo general

Analizar la estructura multivariada de la oferta inmobiliaria urbana mediante componentes principales, conglomerados y correspondencias, con el fin de identificar patrones relevantes para la toma de decisiones.

2.2.2 Objetivos específicos

  • Caracterizar la calidad, distribución y estructura de las variables.
  • Determinar las relaciones lineales entre las variables cuantitativas.
  • Reducir la dimensionalidad mediante análisis de componentes principales (ACP).
  • Segmentar los predios mediante análisis de conglomerados.
  • Evaluar asociaciones entre variables categóricas mediante pruebas de independencia y análisis de correspondencia.
  • Formular conclusiones y recomendaciones basadas en evidencia.

3 3. Comprensión y preparación de los datos

3.1 3.1 Dimensiones y diccionario de variables

cat(
  "La base contiene", format(nrow(inmuebles), big.mark = "."),
  "registros y", ncol(inmuebles), "variables."
)
## La base contiene 8.322 registros y 13 variables.
diccionario <- tibble::tribble(
  ~Variable, ~Tipo, ~Descripción, ~Rol_analítico,
  "id", "Identificador", "Código del inmueble", "No se utiliza para calcular distancias",
  "zona", "Categórica nominal", "Zona urbana de ubicación", "Segmentación y correspondencia",
  "piso", "Categórica/ordinal", "Piso del inmueble; puede contener NA", "Descripción complementaria",
  "estrato", "Ordinal", "Estrato socioeconómico", "Correspondencia y caracterización de segmentos",
  "preciom", "Cuantitativa", "Precio comercial del inmueble", "PCA, conglomerados y análisis de valor",
  "areaconst", "Cuantitativa", "Área construida", "PCA y conglomerados",
  "parqueaderos", "Cuantitativa discreta", "Número de parqueaderos", "PCA y conglomerados",
  "banios", "Cuantitativa discreta", "Número de baños", "PCA y conglomerados",
  "habitaciones", "Cuantitativa discreta", "Número de habitaciones", "PCA y conglomerados",
  "tipo", "Categórica nominal", "Tipo de vivienda", "Correspondencia y perfil de segmentos",
  "barrio", "Categórica nominal", "Barrio de ubicación", "Correspondencia y localización",
  "longitud", "Cuantitativa geográfica", "Coordenada longitudinal", "Visualización espacial",
  "latitud", "Cuantitativa geográfica", "Coordenada latitudinal", "Visualización espacial"
)

tabla_html(diccionario, "Diccionario analítico de variables", digitos = 2)
Diccionario analítico de variables
Variable Tipo Descripción Rol_analítico
id Identificador Código del inmueble No se utiliza para calcular distancias
zona Categórica nominal Zona urbana de ubicación Segmentación y correspondencia
piso Categórica/ordinal Piso del inmueble; puede contener NA Descripción complementaria
estrato Ordinal Estrato socioeconómico Correspondencia y caracterización de segmentos
preciom Cuantitativa Precio comercial del inmueble PCA, conglomerados y análisis de valor
areaconst Cuantitativa Área construida PCA y conglomerados
parqueaderos Cuantitativa discreta Número de parqueaderos PCA y conglomerados
banios Cuantitativa discreta Número de baños PCA y conglomerados
habitaciones Cuantitativa discreta Número de habitaciones PCA y conglomerados
tipo Categórica nominal Tipo de vivienda Correspondencia y perfil de segmentos
barrio Categórica nominal Barrio de ubicación Correspondencia y localización
longitud Cuantitativa geográfica Coordenada longitudinal Visualización espacial
latitud Cuantitativa geográfica Coordenada latitudinal Visualización espacial

3.2 3.2 Control de calidad

Se verifican valores faltantes, duplicados, rangos imposibles (outliers) y consistencia básica. Esta fase es indispensable porque las técnicas multivariadas son sensibles a errores de registro, escalas distintas y observaciones incompletas.

calidad <- tibble(
  variable = names(inmuebles),
  clase = sapply(inmuebles, function(x) paste(class(x), collapse = "/")),
  faltantes = sapply(inmuebles, function(x) sum(is.na(x))),
  porcentaje_faltantes = sapply(inmuebles, function(x) mean(is.na(x)) * 100),
  valores_unicos = sapply(inmuebles, dplyr::n_distinct)
)

tabla_html(calidad, "Diagnóstico de calidad por variable", digitos = 2)
Diagnóstico de calidad por variable
variable clase faltantes porcentaje_faltantes valores_unicos
id numeric 3 0.04 8320
zona character 3 0.04 6
piso character 2638 31.70 13
estrato numeric 3 0.04 5
preciom numeric 2 0.02 540
areaconst numeric 3 0.04 653
parqueaderos numeric 1605 19.29 11
banios numeric 3 0.04 12
habitaciones numeric 3 0.04 12
tipo character 3 0.04 3
barrio character 3 0.04 437
longitud numeric 3 0.04 2929
latitud numeric 3 0.04 3680
duplicados_id <- sum(duplicated(inmuebles$id))
duplicados_fila <- sum(duplicated(inmuebles))

tibble(
  verificacion = c("Identificadores duplicados", "Filas completamente duplicadas"),
  cantidad = c(duplicados_id, duplicados_fila)
) |>
  tabla_html("Control de duplicados", digitos = 0)
Control de duplicados
verificacion cantidad
Identificadores duplicados 2
Filas completamente duplicadas 1

Inmuebles con id faltante:

inmuebles_id_na <- inmuebles %>%
  filter(is.na(id))

tabla_html(
  inmuebles_id_na,
  "Registros con ID faltante",
  digitos = 2
)
Registros con ID faltante
id zona piso estrato preciom areaconst parqueaderos banios habitaciones tipo barrio longitud latitud
330
tabla_piso_tipo_pct <- inmuebles %>%
  mutate(
    Piso_faltante = ifelse(
      is.na(piso),
      "Faltante",
      "Disponible"
    )
  ) %>%
  count(tipo, Piso_faltante) %>%
  group_by(tipo) %>%
  mutate(
    Porcentaje = round(
      n / sum(n) * 100,
      2
    )
  ) %>%
  ungroup() %>%
  rename(
    Tipo_inmueble = tipo,
    Cantidad = n
  )


tabla_html(
  tabla_piso_tipo_pct,
  "Análisis de valores faltantes en piso según tipo de inmueble",
  digitos = 2
)
Análisis de valores faltantes en piso según tipo de inmueble
Tipo_inmueble Piso_faltante Cantidad Porcentaje
Apartamento Disponible 3719 72.92
Apartamento Faltante 1381 27.08
Casa Disponible 1965 61.04
Casa Faltante 1254 38.96
Faltante 3 100.00

Los valores faltantes de la variable piso se presentan tanto en apartamentos (27.08%) como en casas (38.95%). Aunque la proporción es mayor en casas, la ausencia también ocurre en apartamentos, por lo cual no se puede asumir que los valores faltantes representan únicamente inmuebles donde la variable no aplica. En consecuencia, los valores faltantes deben tratarse como información no disponible, y en nuestro puede descartarse la variable de análisis.

Por otro lado parqueaderos puede imputarse con 0 debido a que se observa que en los datos aportados el mínimo es 1, por lo cual el dato faltante puede asumirse en 0 ya que si es un dato no reportado debido a que no existe parqueadero.

Antes de continuar lo primero que se hará es eliminar los datos duplicados y datos con id faltante, estos coinciden con los faltantes de otras variables.

# Resumen antes de eliminar duplicados y registros sin ID
resumen_antes <- data.frame(
  Indicador = c(
    "Número de registros",
    "IDs faltantes (NA)",
    "IDs duplicados",
    "Filas completamente duplicadas"
  ),
  Cantidad = c(
    nrow(inmuebles),
    sum(is.na(inmuebles$id)),
    sum(duplicated(inmuebles$id)),
    sum(duplicated(inmuebles))
  )
)


# 1. Eliminar registros sin ID
inmuebles_limpio <- inmuebles %>%
  filter(!is.na(id))


# 2. Eliminar filas completamente duplicadas
inmuebles_limpio <- inmuebles_limpio %>%
  distinct()


# 3. Eliminar registros con ID repetido
# Conserva la primera aparición del ID
inmuebles_limpio <- inmuebles_limpio %>%
  distinct(id, .keep_all = TRUE)



# Resumen después de limpieza
resumen_despues <- data.frame(
  Indicador = c(
    "Número de registros",
    "IDs faltantes (NA)",
    "IDs duplicados",
    "Filas completamente duplicadas"
  ),
  Cantidad = c(
    nrow(inmuebles_limpio),
    sum(is.na(inmuebles_limpio$id)),
    sum(duplicated(inmuebles_limpio$id)),
    sum(duplicated(inmuebles_limpio))
  )
)


# Consolidar tabla
tabla_duplicados <- rbind(
  data.frame(
    Momento = "Antes",
    resumen_antes
  ),
  data.frame(
    Momento = "Después",
    resumen_despues
  )
)


tabla_html(
  tabla_duplicados,
  "Resumen del tratamiento de identificadores y registros duplicados",
  digitos = 0
)
Resumen del tratamiento de identificadores y registros duplicados
Momento Indicador Cantidad
Antes Número de registros 8322
Antes IDs faltantes (NA) 3
Antes IDs duplicados 2
Antes Filas completamente duplicadas 1
Después Número de registros 8319
Después IDs faltantes (NA) 0
Después IDs duplicados 0
Después Filas completamente duplicadas 0
# Copia de la base
inmuebles_imp <- inmuebles_limpio

# Cantidad de faltantes antes
faltantes_antes <- sum(is.na(inmuebles_imp$parqueaderos))


# Imputación: NA = 0 parqueaderos
inmuebles_imp$parqueaderos[
  is.na(inmuebles_imp$parqueaderos)
] <- 0


# Cantidad de faltantes después
faltantes_despues <- sum(is.na(inmuebles_imp$parqueaderos))


# Tabla resumen de imputación
tabla_imputacion <- data.frame(
  Variable = "parqueaderos",
  Faltantes_antes = faltantes_antes,
  Valores_imputados = faltantes_antes - faltantes_despues,
  Faltantes_despues = faltantes_despues,
  Metodo = "Recodificación NA a 0",
  Justificacion = "Se asume que ausencia de registro indica inexistencia de parqueaderos"
)


tabla_html(
  tabla_imputacion,
  "Resumen de imputación de parqueaderos",
  digitos = 2
)
Resumen de imputación de parqueaderos
Variable Faltantes_antes Valores_imputados Faltantes_despues Metodo Justificacion
parqueaderos 1602 1602 0 Recodificación NA a 0 Se asume que ausencia de registro indica inexistencia de parqueaderos

Podemos ver el resultado de la limpieza:

calidad <- tibble(
  variable = names(inmuebles_imp),
  clase = sapply(inmuebles_imp, function(x) paste(class(x), collapse = "/")),
  faltantes = sapply(inmuebles_imp, function(x) sum(is.na(x))),
  porcentaje_faltantes = sapply(inmuebles_imp, function(x) mean(is.na(x)) * 100),
  valores_unicos = sapply(inmuebles_imp, dplyr::n_distinct)
)

tabla_html(calidad, "Diagnóstico de calidad por variable", digitos = 2)
Diagnóstico de calidad por variable
variable clase faltantes porcentaje_faltantes valores_unicos
id numeric 0 0.00 8319
zona character 0 0.00 5
piso character 2635 31.67 13
estrato numeric 0 0.00 4
preciom numeric 0 0.00 539
areaconst numeric 0 0.00 652
parqueaderos numeric 0 0.00 11
banios numeric 0 0.00 11
habitaciones numeric 0 0.00 11
tipo character 0 0.00 2
barrio character 0 0.00 436
longitud numeric 0 0.00 2928
latitud numeric 0 0.00 3679

3.3 3.3 Decisiones de preparación

Para el análisis cuantitativo se seleccionan las variables que describen directamente el tamaño, dotación, nivel socioeconómico y valor del inmueble:

  • preciom
  • areaconst
  • parqueaderos
  • banios
  • habitaciones
  • estrato

El identificador no se usa como variable explicativa. Las coordenadas se reservan para visualización espacial, porque incluirlas en el PCA o en el conglomerado mezclaría proximidad geográfica con características físicas y comerciales. La variable piso presentó un 31,7 % de valores faltantes y debido a que este porcentaje es elevado y no existe evidencia suficiente para realizar una imputación confiable, la variable se excluyó de las técnicas multivariadas. Sin embargo, se conservó para el análisis descriptivo con el fin de evitar introducir sesgos en los resultados.

vars_cuant <- c(
  "preciom", "areaconst", "parqueaderos",
  "banios", "habitaciones", "estrato"
)

datos_completos <- inmuebles_imp |>
  mutate(.fila_original = row_number()) |>
  mutate(across(all_of(vars_cuant), as.numeric)) |>
  filter(if_all(all_of(vars_cuant), is.finite)) |>
  drop_na(all_of(vars_cuant))

datos_modelo <- datos_completos |>
  select(all_of(vars_cuant))

filas_modelo <- datos_completos$.fila_original

n_excluidos <- nrow(inmuebles_imp) - nrow(datos_modelo)

cat(
  "Registros completos empleados en los métodos cuantitativos:",
  format(nrow(datos_modelo), big.mark = ".")
)
## Registros completos empleados en los métodos cuantitativos: 8.319

4 4. Análisis exploratorio de datos

4.1 4.1 Resumen estadístico de variables cuantitativas

resumen_cuant <- purrr::map_dfr(
  vars_cuant,
  function(v) {
    x <- datos_modelo[[v]]
    tibble(
      variable = v,
      n = length(x),
      media = mean(x),
      desviacion = sd(x),
      minimo = min(x),
      q1 = quantile(x, 0.25),
      mediana = median(x),
      q3 = quantile(x, 0.75),
      maximo = max(x),
      coef_variacion = sd(x) / mean(x),
      asimetria = mean((x - mean(x))^3) / sd(x)^3
    )
  }
)

tabla_html(
  resumen_cuant,
  "Estadísticos descriptivos de las variables cuantitativas",
  digitos = 2
)
Estadísticos descriptivos de las variables cuantitativas
variable n media desviacion minimo q1 mediana q3 maximo coef_variacion asimetria
preciom 8319 433.90 328.67 58 220 330 540 1999 0.76 1.85
areaconst 8319 174.93 142.96 30 80 123 229 1745 0.82 2.69
parqueaderos 8319 1.48 1.24 0 1 1 2 10 0.84 1.65
banios 8319 3.11 1.43 0 2 3 4 10 0.46 0.93
habitaciones 8319 3.61 1.46 0 3 3 4 10 0.40 1.63
estrato 8319 4.63 1.03 3 4 5 5 6 0.22 -0.18

La media y la mediana se verifican para detectar asimetrías. El coeficiente de variación permite comparar dispersión relativa entre variables medidas en escalas distintas. Una diferencia amplia entre media y mediana, acompañada de asimetría positiva, sugiere que unas pocas propiedades de gran tamaño o alto precio extienden la cola derecha de la distribución.

4.2 4.2 Distribuciones

datos_largos <- datos_modelo |>
  pivot_longer(
    cols = everything(),
    names_to = "variable",
    values_to = "valor"
  )

ggplot(datos_largos, aes(x = valor)) +
  geom_histogram(bins = 35, fill = "#F28E2B", color = "white", alpha = 0.85) +
  facet_wrap(~ variable, scales = "free", ncol = 2) +
  labs(
    title = "Distribución de las variables cuantitativas",
    subtitle = "Las escalas se presentan de forma independiente para facilitar la lectura",
    x = NULL,
    y = "Frecuencia"
  ) +
  theme_minimal(base_size = 12)

4.3 4.3 Precio por tipo y zona

precio_tipo <- inmuebles_imp |>
  filter(!is.na(tipo), !is.na(preciom)) |>
  group_by(tipo) |>
  summarise(
    oferta = n(),
    precio_mediano = median(preciom, na.rm = TRUE),
    precio_promedio = mean(preciom, na.rm = TRUE),
    area_mediana = median(areaconst, na.rm = TRUE),
    .groups = "drop"
  ) |>
  arrange(desc(precio_mediano))


tabla_html(
  precio_tipo,
  "Resumen de precio por tipo de vivienda",
  digitos = 2
)
Resumen de precio por tipo de vivienda
tipo oferta precio_mediano precio_promedio area_mediana
Casa 3219 430 539.99 240
Apartamento 5100 279 366.94 90
ggplot(
  inmuebles_imp |> filter(!is.na(tipo), !is.na(preciom)),
  aes(x = forcats::fct_reorder(tipo, preciom, median, na.rm = TRUE), y = preciom)
) +
  geom_boxplot(fill = "#F28E2B", alpha = 0.75, outlier.alpha = 0.25) +
  coord_flip() +
  scale_y_continuous(labels = scales::label_number(big.mark = ".", decimal.mark = ",")) +
  labs(
    title = "Distribución del precio por tipo de vivienda",
    x = "Tipo de vivienda",
    y = "Precio"
  ) +
  theme_minimal(base_size = 12)

precio_zona <- inmuebles_imp |>
  filter(!is.na(zona), !is.na(preciom)) |>
  group_by(zona) |>
  summarise(
    oferta = n(),
    precio_mediano = median(preciom),
    precio_promedio = mean(preciom),
    area_mediana = median(areaconst, na.rm = TRUE),
    .groups = "drop"
  ) |>
  arrange(desc(precio_mediano))

tabla_html(precio_zona, "Oferta y precio por zona", digitos = 2)
Oferta y precio por zona
zona oferta precio_mediano precio_promedio area_mediana
Zona Oeste 1198 580 677.58 165.5
Zona Sur 4726 320 426.52 113.0
Zona Norte 1920 300 345.61 107.0
Zona Centro 124 297 309.69 160.0
Zona Oriente 351 210 228.53 160.0
ggplot(
  inmuebles |> filter(!is.na(zona), !is.na(preciom)),
  aes(
    x = forcats::fct_reorder(zona, preciom, median, na.rm = TRUE),
    y = preciom
  )
) +
  geom_boxplot(
    fill = "#4E79A7",
    alpha = 0.75,
    outlier.alpha = 0.25
  ) +
  coord_flip() +
  scale_y_continuous(
    labels = scales::label_number(
      big.mark = ".",
      decimal.mark = ","
    )
  ) +
  labs(
    title = "Distribución del precio por zona",
    x = "Zona",
    y = "Precio"
  ) +
  theme_minimal(base_size = 12)

4.3.1 4.3 Verificación de las condiciones para el análisis de correlación

Antes de construir la matriz de correlaciones se revisó la naturaleza de las variables, su variabilidad y el comportamiento gráfico de sus relaciones. Esta verificación es necesaria porque el hecho de que una variable esté almacenada en R como numeric no implica necesariamente que sea cuantitativa en términos estadísticos.

diagnostico_cor <- tibble(
  variable = names(datos_modelo),
  clase = sapply(
    datos_modelo,
    function(x) paste(class(x), collapse = "/")
  ),
  valores_unicos = sapply(
    datos_modelo,
    dplyr::n_distinct
  ),
  faltantes = sapply(
    datos_modelo,
    function(x) sum(is.na(x))
  ),
  desviacion_estandar = sapply(
    datos_modelo,
    function(x) {
      if (is.numeric(x)) sd(x, na.rm = TRUE) else NA_real_
    }
  )
)

tabla_html(
  diagnostico_cor,
  "Verificación de las variables para el análisis de correlación",
  digitos = 3
)
Verificación de las variables para el análisis de correlación
variable clase valores_unicos faltantes desviacion_estandar
preciom numeric 539 0 328.665
areaconst numeric 652 0 142.964
parqueaderos numeric 11 0 1.243
banios numeric 11 0 1.428
habitaciones numeric 11 0 1.460
estrato numeric 4 0 1.029

Las variables preciom y areaconst son cuantitativas continuas, mientras que parqueaderos, banios y habitaciones corresponden a variables cuantitativas discretas de conteo. Por su parte, estrato, aunque se encuentra almacenada numéricamente, es conceptualmente una variable ordinal, pues sus valores representan categorías socioeconómicas ordenadas.

Todas las variables presentan variabilidad y no se observan valores faltantes en el conjunto utilizado para este análisis.

4.3.1.1 Exploración gráfica de las relaciones

GGally::ggpairs(
  datos_modelo,
  lower = list(
    continuous = GGally::wrap(
      "smooth",
      alpha = 0.4,
      size = 0.3
    )
  ),
  diag = list(
    continuous = "densityDiag"
  ),
  upper = list(
    continuous = "cor"
  )
)

La exploración gráfica evidencia que preciom y areaconst presentan distribuciones asimétricas hacia la derecha. Las variables parqueaderos, banios y habitaciones, al ser conteos discretos, generan concentraciones de observaciones en valores enteros. No obstante, los diagramas permiten identificar tendencias aproximadamente monotónicas y, en varios casos, lineales entre las variables.

Debido a estas características, se utilizará la correlación de Pearson para describir las asociaciones lineales entre las variables cuantitativas. La variable estrato se analizará adicionalmente mediante el coeficiente de Spearman, debido a su naturaleza ordinal.

5 4.4 Matriz de varianzas-covarianzas y correlaciones

La matriz de covarianzas permite analizar la variabilidad conjunta de las variables cuantitativas, aunque sus magnitudes dependen de las unidades de medida. La matriz de correlaciones de Pearson estandariza estas relaciones en una escala comprendida entre -1 y 1, permitiendo comparar la intensidad y dirección de las asociaciones lineales.

Para este análisis se consideran como variables cuantitativas preciom, areaconst, parqueaderos, banios y habitaciones.

datos_pearson <- datos_modelo |>
  dplyr::select(
    preciom,
    areaconst,
    parqueaderos,
    banios,
    habitaciones
  )

5.0.1 Matriz de varianzas-covarianzas

matriz_cov <- cov(
  datos_pearson,
  use = "complete.obs"
)

tabla_html(
  as.data.frame(matriz_cov) |>
    tibble::rownames_to_column("Variable"),
  "Matriz de varianzas-covarianzas",
  digitos = 2
)
Matriz de varianzas-covarianzas
Variable preciom areaconst parqueaderos banios habitaciones
preciom 108020.70 32296.82 261.40 314.10 126.68
areaconst 32296.82 20438.74 85.73 132.40 107.86
parqueaderos 261.40 85.73 1.55 0.93 0.36
banios 314.10 132.40 0.93 2.04 1.23
habitaciones 126.68 107.86 0.36 1.23 2.13

5.0.2 Matriz de correlaciones de Pearson

matriz_cor <- cor(
  datos_pearson,
  use = "complete.obs",
  method = "pearson"
)

tabla_html(
  as.data.frame(matriz_cor) |>
    tibble::rownames_to_column("Variable"),
  "Matriz de correlaciones de Pearson",
  digitos = 3
)
Matriz de correlaciones de Pearson
Variable preciom areaconst parqueaderos banios habitaciones
preciom 1.000 0.687 0.640 0.669 0.264
areaconst 0.687 1.000 0.482 0.648 0.517
parqueaderos 0.640 0.482 1.000 0.523 0.199
banios 0.669 0.648 0.523 1.000 0.590
habitaciones 0.264 0.517 0.199 0.590 1.000
corrplot::corrplot(
  matriz_cor,
  method = "ellipse",
  type = "upper",
  addCoef.col = "black",
  tl.col = "black",
  tl.srt = 35,
  number.cex = 0.75,
  diag = FALSE
)

5.0.3 Relaciones lineales de mayor magnitud

pares_cor <- as.data.frame(as.table(matriz_cor)) |>
  dplyr::rename(
    variable_1 = Var1,
    variable_2 = Var2,
    correlacion = Freq
  ) |>
  dplyr::filter(
    as.character(variable_1) <
      as.character(variable_2)
  ) |>
  dplyr::mutate(
    magnitud = abs(correlacion)
  ) |>
  dplyr::arrange(desc(magnitud))

tabla_html(
  head(pares_cor, 10),
  "Diez relaciones lineales de mayor magnitud",
  digitos = 3
)
Diez relaciones lineales de mayor magnitud
variable_1 variable_2 correlacion magnitud
areaconst preciom 0.687 0.687
banios preciom 0.669 0.669
areaconst banios 0.648 0.648
parqueaderos preciom 0.640 0.640
banios habitaciones 0.590 0.590
banios parqueaderos 0.523 0.523
areaconst habitaciones 0.517 0.517
areaconst parqueaderos 0.482 0.482
habitaciones preciom 0.264 0.264
habitaciones parqueaderos 0.199 0.199

Tomando como referencia práctica una magnitud absoluta de 0,70 para identificar asociaciones lineales altas, se verifica si existen pares de variables que superen dicho valor.

cor_altas <- pares_cor |>
  dplyr::filter(magnitud >= 0.70)

if (nrow(cor_altas) == 0) {
  cat(
    "No se identificaron correlaciones de Pearson con ",
    "magnitud absoluta igual o superior a 0,70. ",
    "Bajo el criterio adoptado, no se evidencia una alta ",
    "redundancia lineal entre las variables cuantitativas."
  )
} else {
  tabla_html(
    cor_altas,
    "Correlaciones con magnitud absoluta igual o superior a 0,70",
    digitos = 3
  )
}
## No se identificaron correlaciones de Pearson con  magnitud absoluta igual o superior a 0,70.  Bajo el criterio adoptado, no se evidencia una alta  redundancia lineal entre las variables cuantitativas.

5.0.4 Asociación del estrato con las demás variables

Debido a que estrato es una variable ordinal, sus asociaciones se complementan mediante el coeficiente de correlación de Spearman. Este coeficiente se basa en los rangos de las observaciones y resulta apropiado para evaluar asociaciones monotónicas cuando intervienen variables ordinales.

cor_spearman <- sapply(
  datos_modelo |>
    dplyr::select(-estrato),
  function(x) {
    cor(
      x,
      datos_modelo$estrato,
      use = "complete.obs",
      method = "spearman"
    )
  }
)

tabla_spearman <- tibble(
  variable = names(cor_spearman),
  correlacion_spearman = as.numeric(cor_spearman),
  magnitud = abs(as.numeric(cor_spearman))
) |>
  dplyr::arrange(desc(magnitud))

tabla_html(
  tabla_spearman,
  "Correlación de Spearman entre estrato y las demás variables",
  digitos = 3
)
Correlación de Spearman entre estrato y las demás variables
variable correlacion_spearman magnitud
preciom 0.710 0.710
parqueaderos 0.586 0.586
banios 0.471 0.471
areaconst 0.395 0.395
habitaciones 0.040 0.040

La correlación no implica causalidad. En este análisis, los coeficientes se utilizan para describir el grado de asociación entre las variables y detectar posibles relaciones fuertes que puedan indicar información redundante. El valor de 0,70 se adopta como un criterio práctico de referencia y no como un límite estadístico universal, en este caso preciom y estrato.

6 5. Análisis de Componentes Principales (ACP)

6.1 5.1 Justificación

El Análisis de Componentes Principales permite resumir la variabilidad conjunta de varias variables cuantitativas mediante nuevas dimensiones no correlacionadas entre sí. Cada componente corresponde a una combinación lineal de las variables originales y se ordena de acuerdo con la cantidad de variabilidad que explica.

Para este análisis se utilizan preciom, areaconst, parqueaderos, banios y habitaciones. La variable estrato no se incorpora como variable activa del ACP porque, aunque se encuentra almacenada numéricamente, representa categorías ordinales. En consecuencia, se conserva como variable externa para apoyar posteriormente la interpretación de los componentes y para el análisis de correspondencias.

Las variables cuantitativas presentan unidades y escalas diferentes. Por esta razón, el ACP se realiza con centrado y estandarización, evitando que el precio o el área construida dominen los componentes únicamente por tener magnitudes numéricas mayores.

6.2 5.2 Selección, estandarización y ajuste

datosPca <- datos_modelo |>
  dplyr::transmute(
    Precio = preciom,
    AreaConstruida = areaconst,
    Parqueaderos = parqueaderos,
    Banios = banios,
    Habitaciones = habitaciones
  )

datosPcaZ <- scale(datosPca)

verificacionEscala <- tibble(
  Variable = colnames(datosPcaZ),
  MediaEstandarizada = apply(datosPcaZ, 2, mean),
  DesviacionEstandarizada = apply(datosPcaZ, 2, sd)
)

tabla_html(
  verificacionEscala,
  "Verificación de la estandarización para el ACP",
  digitos = 4
)
Verificación de la estandarización para el ACP
Variable MediaEstandarizada DesviacionEstandarizada
Precio 0 1
AreaConstruida 0 1
Parqueaderos 0 1
Banios 0 1
Habitaciones 0 1

La media de cada variable estandarizada debe ser aproximadamente cero y su desviación estándar aproximadamente uno. Esta transformación permite comparar las variables en una escala común.

modeloPca <- prcomp(
  datosPca,
  center = TRUE,
  scale. = TRUE
)

6.3 5.3 Varianza explicada y selección de componentes

Los autovalores representan la variabilidad explicada por cada componente. A partir de ellos se calcula tanto el porcentaje de varianza explicada individual como el porcentaje acumulado.

autovaloresPca <- modeloPca$sdev^2
varianzaPca <- autovaloresPca / sum(autovaloresPca) * 100
varianzaAcumuladaPca <- cumsum(varianzaPca)

tablaVarianzaPca <- tibble(
  Componente = paste0("CP", seq_along(autovaloresPca)),
  Autovalor = autovaloresPca,
  VarianzaExplicada = varianzaPca,
  VarianzaAcumulada = varianzaAcumuladaPca
)

tabla_html(
  tablaVarianzaPca,
  "Autovalores y porcentaje de varianza explicada",
  digitos = 2
)
Autovalores y porcentaje de varianza explicada
Componente Autovalor VarianzaExplicada VarianzaAcumulada
CP1 3.13 62.57 62.57
CP2 0.92 18.36 80.92
CP3 0.44 8.70 89.63
CP4 0.32 6.49 96.12
CP5 0.19 3.88 100.00
factoextra::fviz_eig(
  modeloPca,
  addlabels = TRUE
) +
  labs(
    title = "Gráfico de sedimentación del ACP",
    subtitle = "Porcentaje de variabilidad explicado por cada componente",
    x = "Componente",
    y = "Varianza Explicada (%)"
  ) +
  theme_minimal(base_size = 12)

k70Pca <- which(varianzaAcumuladaPca >= 70)[1]
k80Pca <- which(varianzaAcumuladaPca >= 80)[1]
kKaiserPca <- sum(autovaloresPca > 1)

cat(
  paste0(
    "Los dos primeros componentes explican **",
    round(sum(varianzaPca[1:2]), 1),
    "%** de la variabilidad total. Para alcanzar al menos el 70% se requieren **",
    k70Pca,
    " componentes**, y para alcanzar al menos el 80% se requieren **",
    k80Pca,
    " componentes**. Según el criterio de Kaiser, se retienen **",
    kKaiserPca,
    " componentes** con autovalor mayor que uno."
  )
)

Los dos primeros componentes explican 80.9% de la variabilidad total. Para alcanzar al menos el 70% se requieren 2 componentes, y para alcanzar al menos el 80% se requieren 2 componentes. Según el criterio de Kaiser, se retienen 1 componentes con autovalor mayor que uno.

La selección final no se basa únicamente en la posibilidad de representar dos dimensiones. Se consideran conjuntamente la varianza acumulada, el comportamiento del gráfico de sedimentación y el criterio de Kaiser. Los dos primeros componentes se utilizan para la representación gráfica, mientras que la cantidad de componentes necesaria para resumir la información se interpreta a partir de los porcentajes anteriores.

6.4 5.4 Cargas y contribuciones de las variables

Las cargas permiten identificar qué variables tienen mayor peso en cada componente y, por tanto, ayudan a asignar un significado estadístico a las nuevas dimensiones.

cargasPca <- as.data.frame(modeloPca$rotation) |>
  tibble::rownames_to_column("Variable")

tabla_html(
  cargasPca,
  "Cargas de las variables en los componentes principales",
  digitos = 3
)
Cargas de las variables en los componentes principales
Variable PC1 PC2 PC3 PC4 PC5
Precio 0.479 0.360 -0.359 0.264 0.666
AreaConstruida 0.484 -0.085 -0.562 -0.538 -0.392
Parqueaderos 0.408 0.520 0.661 -0.346 -0.081
Banios 0.496 -0.157 0.121 0.695 -0.482
Habitaciones 0.352 -0.754 0.323 -0.196 0.406
factoextra::fviz_pca_var(
  modeloPca,
  col.var = "contrib",
  repel = TRUE
) +
  labs(
    title = "Círculo de correlaciones del ACP",
    subtitle = "Dirección y contribución de las variables",
    x = paste0("CP1 (", round(varianzaPca[1], 1), "%)"),
    y = paste0("CP2 (", round(varianzaPca[2], 1), "%)")
  ) +
  theme_minimal(base_size = 12)

variablesDominantesPca <- cargasPca |>
  dplyr::select(Variable, PC1, PC2) |>
  dplyr::mutate(
    MagnitudPC1 = abs(PC1),
    MagnitudPC2 = abs(PC2)
  )

topPc1 <- variablesDominantesPca |>
  dplyr::arrange(desc(MagnitudPC1)) |>
  dplyr::slice_head(n = 3) |>
  dplyr::select(Variable, PC1, MagnitudPC1) |>
  dplyr::rename(
    Componente1 = PC1,
    MagnitudComponente1 = MagnitudPC1
  )

topPc2 <- variablesDominantesPca |>
  dplyr::arrange(desc(MagnitudPC2)) |>
  dplyr::slice_head(n = 3) |>
  dplyr::select(Variable, PC2, MagnitudPC2) |>
  dplyr::rename(
    Componente2 = PC2,
    MagnitudComponente2 = MagnitudPC2
  )

tabla_html(
  topPc1,
  "Variables con mayor peso absoluto en el Componente 1",
  digitos = 3
)
Variables con mayor peso absoluto en el Componente 1
Variable Componente1 MagnitudComponente1
Banios 0.496 0.496
AreaConstruida 0.484 0.484
Precio 0.479 0.479
tabla_html(
  topPc2,
  "Variables con mayor peso absoluto en el Componente 2",
  digitos = 3
)
Variables con mayor peso absoluto en el Componente 2
Variable Componente2 MagnitudComponente2
Habitaciones -0.754 0.754
Parqueaderos 0.520 0.520
Precio 0.360 0.360

6.5 5.5 Representación de los inmuebles

La representación de los individuos en el plano CP1-CP2 permite reconocer patrones, gradientes y posibles agrupaciones. El estrato se incorpora únicamente como variable descriptiva externa: no interviene en el cálculo de los componentes.

scoresPca <- as.data.frame(modeloPca$x) |>
  dplyr::mutate(
    Estrato = factor(datos_modelo$estrato, ordered = TRUE)
  )

ggplot(
  scoresPca,
  aes(x = PC1, y = PC2, color = Estrato)
) +
  geom_point(alpha = 0.55, size = 1.4) +
  geom_hline(yintercept = 0, linetype = 2) +
  geom_vline(xintercept = 0, linetype = 2) +
  labs(
    title = "Inmuebles en el plano de los dos primeros componentes",
    subtitle = "El estrato se utiliza únicamente para caracterizar la distribución de los individuos",
    x = paste0("CP1 (", round(varianzaPca[1], 1), "%)"),
    y = paste0("CP2 (", round(varianzaPca[2], 1), "%)"),
    color = "Estrato"
  ) +
  theme_minimal(base_size = 12) +
  theme(legend.position = "bottom")

6.6 5.6 Casos extremos e interpretación contextual

Siguiendo la lógica del ejemplo desarrollado en el curso, se revisan los inmuebles ubicados en los extremos de los dos primeros componentes. Esta comparación ayuda a interpretar qué características originales explican las posiciones más alejadas del centro.

indicesExtremosPca <- unique(c(
  order(modeloPca$x[, 1])[1:3],
  order(modeloPca$x[, 1], decreasing = TRUE)[1:3],
  order(modeloPca$x[, 2])[1:3],
  order(modeloPca$x[, 2], decreasing = TRUE)[1:3]
))

extremosPca <- datos_completos[indicesExtremosPca, ] |>
  dplyr::mutate(
    CP1 = modeloPca$x[indicesExtremosPca, 1],
    CP2 = modeloPca$x[indicesExtremosPca, 2]
  ) |>
  dplyr::transmute(
    Id = id,
    Zona = zona,
    Barrio = barrio,
    Tipo = tipo,
    Estrato = estrato,
    Precio = preciom,
    AreaConstruida = areaconst,
    Habitaciones = habitaciones,
    Banios = banios,
    Parqueaderos = parqueaderos,
    CP1 = CP1,
    CP2 = CP2
  ) |>
  dplyr::arrange(CP1)

tabla_html(
  extremosPca,
  "Inmuebles extremos en los dos primeros componentes",
  digitos = 2
)
Inmuebles extremos en los dos primeros componentes
Id Zona Barrio Tipo Estrato Precio AreaConstruida Habitaciones Banios Parqueaderos CP1 CP2
7011 Zona Sur el refugio Apartamento 4 160 76 0 0 0 -3.17 1.34
7522 Zona Sur meléndez Apartamento 3 148 87 0 0 0 -3.15 1.32
69 Zona Oriente jose manuel marroquín Casa 3 165 80 0 0 0 -3.15 1.35
5908 Zona Sur el limonar Apartamento 5 950 280 0 0 10 1.95 6.27
6811 Zona Sur urbanización tequendama Casa 5 1650 600 0 0 6 2.74 5.17
5741 Zona Sur el cedro Casa 4 500 500 10 7 0 3.60 -4.47
4597 Zona Sur bretaña Casa 3 470 366 10 10 0 4.15 -4.75
4508 Zona Centro alameda Casa 4 610 750 10 8 0 4.96 -4.61
7505 Zona Sur pance Casa 6 1600 330 3 5 10 5.53 4.85
3652 Zona Sur pance Casa 6 1900 850 6 7 8 8.49 2.27
5396 Zona Sur parcelaciones pance Casa 6 1600 1600 6 6 3 8.60 -0.49
5684 Zona Sur ciudad jardín Casa 6 1800 1586 5 4 10 10.21 3.40

6.7 5.7 Síntesis del ACP

variablePrincipalPc1 <- topPc1$Variable[1]
variablePrincipalPc2 <- topPc2$Variable[1]

cat(
  paste0(
    "El primer componente está principalmente caracterizado por **",
    variablePrincipalPc1,
    "**, mientras que el segundo presenta su mayor peso absoluto en **",
    variablePrincipalPc2,
    "**. Los dos primeros componentes explican conjuntamente **",
    round(sum(varianzaPca[1:2]), 1),
    "%** de la variabilidad total. "
  )
)

El primer componente está principalmente caracterizado por Banios, mientras que el segundo presenta su mayor peso absoluto en Habitaciones. Los dos primeros componentes explican conjuntamente 80.9% de la variabilidad total.

if (sum(varianzaPca[1:2]) >= 70) {
  cat(
    "La representación bidimensional conserva una proporción alta ",
    "de la información y resulta adecuada para sintetizar visualmente ",
    "la estructura principal de los datos."
  )
} else if (sum(varianzaPca[1:2]) >= 50) {
  cat(
    "La representación bidimensional es útil para explorar la ",
    "estructura principal, aunque una parte relevante de la ",
    "variabilidad queda contenida en componentes posteriores."
  )
} else {
  cat(
    "La representación bidimensional debe interpretarse con cautela, ",
    "porque conserva menos de la mitad de la variabilidad total."
  )
}

La representación bidimensional conserva una proporción alta de la información y resulta adecuada para sintetizar visualmente la estructura principal de los datos. El ACP no debería interpretarse únicamente como un mecanismo para reducir cinco variables a un número menor de componentes. Su principal aporte en este análisis consiste en revelar que las características físicas y económicas de los inmuebles no varían de manera completamente independiente, sino que existe una estructura común. Precio, área construida, número de baños, habitaciones y parqueaderos describen diferentes atributos de una vivienda, pero parte de su variabilidad responde a una misma lógica: los inmuebles de mayor dimensión o dotación tienden a presentar simultáneamente valores mayores en varias de estas características. En consecuencia, los primeros componentes condensan un patrón general de escala, capacidad y valor del inmueble, mientras que los componentes posteriores recogen diferencias más específicas entre viviendas que no quedan explicadas por ese patrón dominante.

Desde una perspectiva inmobiliaria, el primer componente puede interpretarse como una medida general de las características de una vivienda, teniendo en cuenta aspectos como su tamaño, precio y dotación. Esto permite identificar viviendas que, en conjunto, presentan características superiores o inferiores al promedio de la muestra. El segundo componente aporta una diferencia adicional, ya que permite distinguir viviendas que pueden ser similares en términos generales, pero que tienen diferentes combinaciones de espacio, dotación y precio. De esta manera, el ACP permite encontrar patrones entre varias características que serían más difíciles de observar al analizarlas por separado.

También es importante no interpretar la reducción dimensional como una eliminación de variables “poco importantes”. Los componentes son combinaciones lineales de todas las variables consideradas; por tanto, reducir dimensiones significa representar una proporción importante de variabilidad mediante nuevos ejes sintéticos, no demostrar que las variables originales restantes carezcan de valor. La información que queda en componentes posteriores representa variabilidad más específica y, dependiendo del objetivo analítico, podría seguir siendo relevante.

Finalmente, la utilidad del ACP en este problema no se limita a producir una visualización bidimensional. Su resultado permite concluir que la oferta inmobiliaria posee una estructura continua: existen inmuebles que se desplazan gradualmente desde configuraciones más pequeñas y menos dotadas hacia configuraciones de mayor tamaño, equipamiento y valor. Esto será importante al interpretar posteriormente K-Means, porque los conglomerados no necesariamente representan poblaciones naturalmente separadas, sino segmentaciones discretas de una estructura que en parte es continua.

7 6. Análisis de Conglomerados mediante K-Means

7.1 6.1 Propósito y elección del método

El análisis de conglomerados busca identificar grupos de inmuebles homogéneos internamente y diferentes entre sí. En este informe se utiliza K-Means, un procedimiento no jerárquico que asigna cada observación al centroide más cercano y actualiza iterativamente los centroides con el objetivo de reducir la suma de cuadrados dentro de los conglomerados.

K-Means trabaja con distancia euclidiana y es sensible a las diferencias de escala. Por ello se utilizan las mismas cinco variables cuantitativas del ACP y se estandarizan antes del agrupamiento. Estrato queda fuera del algoritmo porque es ordinal; posteriormente se emplea para caracterizar los segmentos obtenidos.

7.2 6.2 Preparación de los datos

datosKmeans <- datosPca

datosKmeansZ <- scale(datosKmeans)

verificacionKmeans <- tibble(
  Variable = colnames(datosKmeansZ),
  MediaEstandarizada = apply(datosKmeansZ, 2, mean),
  DesviacionEstandarizada = apply(datosKmeansZ, 2, sd)
)

tabla_html(
  verificacionKmeans,
  "Verificación de la estandarización para K-Means",
  digitos = 4
)
Verificación de la estandarización para K-Means
Variable MediaEstandarizada DesviacionEstandarizada
Precio 0 1
AreaConstruida 0 1
Parqueaderos 0 1
Banios 0 1
Habitaciones 0 1

7.3 6.3 Parámetros del algoritmo

Se utiliza algorithm = "Hartigan-Wong", que corresponde al algoritmo predeterminado de stats::kmeans() en R. La documentación oficial de R señala además que múltiples arranques aleatorios mediante nstart > 1 suelen ser recomendables. Como K-Means puede converger a soluciones diferentes según la inicialización de los centroides, en este análisis se utiliza nstart = 50. Cada ajuste conserva la solución con menor suma de cuadrados dentro de los conglomerados entre los arranques evaluados. También se fija una semilla para que el resultado pueda reproducirse.

El parámetro iter.max = 100 establece un límite suficientemente amplio para las iteraciones del algoritmo. La estrategia adoptada en R consiste en combinar el algoritmo Hartigan-Wong con múltiples inicializaciones aleatorias.

semillaKmeans <- 20260730
nstartKmeans <- 50
iterMaxKmeans <- 100
kMaximo <- min(10, nrow(datosKmeansZ) - 1)

set.seed(semillaKmeans)

7.4 6.4 Elección del número de conglomerados: método del codo

Para cada valor de k se calcula la suma total de cuadrados dentro de los conglomerados (TotWithinSS). A medida que aumenta el número de grupos, esta cantidad disminuye. El método del codo busca el punto a partir del cual añadir nuevos conglomerados produce una reducción marginal cada vez menor.

ajustesKmeans <- vector("list", kMaximo)
wssKmeans <- numeric(kMaximo)

for (k in seq_len(kMaximo)) {
  set.seed(semillaKmeans + k)
  ajustesKmeans[[k]] <- kmeans(
    datosKmeansZ,
    centers = k,
    nstart = nstartKmeans,
    iter.max = iterMaxKmeans,
    algorithm = "Hartigan-Wong"
  )
  wssKmeans[k] <- ajustesKmeans[[k]]$tot.withinss
}

tablaCodo <- tibble(
  K = seq_len(kMaximo),
  TotWithinSS = wssKmeans
)

tabla_html(
  tablaCodo,
  "Suma de cuadrados dentro de los conglomerados para cada K",
  digitos = 2
)
Suma de cuadrados dentro de los conglomerados para cada K
K TotWithinSS
1 41590.00
2 24256.33
3 19299.38
4 16114.85
5 14506.98
6 13467.35
7 12581.08
8 11891.54
9 11295.50
10 10714.08
ggplot(tablaCodo, aes(x = K, y = TotWithinSS)) +
  geom_line(linewidth = 0.8) +
  geom_point(size = 2.4) +
  scale_x_continuous(breaks = tablaCodo$K) +
  labs(
    title = "Método del codo para seleccionar el número de conglomerados",
    subtitle = "La reducción de la variabilidad interna se evalúa para K entre 1 y 10",
    x = "Número de Conglomerados K",
    y = "Suma de Cuadrados Dentro de los Conglomerados"
  ) +
  theme_minimal(base_size = 12)

7.5 6.5 Segunda diferencia y punto de máxima curvatura

Para complementar la inspección visual del codo se calcula una aproximación discreta de la segunda derivada. Para cada valor interior de k, la segunda diferencia compara el cambio de pendiente de la curva de WSS. Un valor alto indica una curvatura más marcada y, por tanto, un posible punto de codo.

segundaDiferencia <- rep(NA_real_, kMaximo)

for (k in 2:(kMaximo - 1)) {
  segundaDiferencia[k] <-
    wssKmeans[k + 1] - 2 * wssKmeans[k] + wssKmeans[k - 1]
}

kOptimo <- which.max(replace(segundaDiferencia, is.na(segundaDiferencia), -Inf))

tablaCurvatura <- tibble(
  K = seq_len(kMaximo),
  TotWithinSS = wssKmeans,
  SegundaDiferencia = segundaDiferencia,
  Seleccionado = ifelse(seq_len(kMaximo) == kOptimo, "Sí", "No")
)

tabla_html(
  tablaCurvatura,
  "Evaluación del codo mediante segunda diferencia",
  digitos = 3
)
Evaluación del codo mediante segunda diferencia
K TotWithinSS SegundaDiferencia Seleccionado
1 41590.00
No
2 24256.33 12376.722
3 19299.38 1772.415 No
4 16114.85 1576.668 No
5 14506.98 568.228 No
6 13467.35 153.372 No
7 12581.08 196.723 No
8 11891.54 93.507 No
9 11295.50 14.613 No
10 10714.08
No
ggplot(
  tablaCurvatura |> dplyr::filter(!is.na(SegundaDiferencia)),
  aes(x = K, y = SegundaDiferencia)
) +
  geom_line(linewidth = 0.8) +
  geom_point(size = 2.4) +
  geom_vline(xintercept = kOptimo, linetype = 2) +
  annotate(
    "text",
    x = kOptimo,
    y = max(segundaDiferencia, na.rm = TRUE),
    label = paste0("K = ", kOptimo),
    vjust = -0.8
  ) +
  scale_x_continuous(breaks = 2:(kMaximo - 1)) +
  labs(
    title = "Segunda diferencia de la curva del codo",
    subtitle = "El máximo cambio de curvatura se utiliza como criterio reproducible de selección",
    x = "Número de Conglomerados K",
    y = "Segunda Diferencia"
  ) +
  theme_minimal(base_size = 12)

ggplot(tablaCodo, aes(x = K, y = TotWithinSS)) +
  geom_line(linewidth = 0.8) +
  geom_point(size = 2.4) +
  geom_vline(xintercept = kOptimo, linetype = 2) +
  geom_point(
    data = tablaCodo |> dplyr::filter(K == kOptimo),
    size = 4
  ) +
  annotate(
    "text",
    x = kOptimo,
    y = tablaCodo$TotWithinSS[kOptimo],
    label = paste0("K seleccionado = ", kOptimo),
    hjust = -0.08,
    vjust = -1
  ) +
  scale_x_continuous(breaks = tablaCodo$K) +
  labs(
    title = "Selección del número de conglomerados",
    subtitle = "El codo se identifica mediante la máxima segunda diferencia",
    x = "Número de Conglomerados K",
    y = "Suma de Cuadrados Dentro de los Conglomerados"
  ) +
  theme_minimal(base_size = 12)

7.6 6.6 Comprobación mediante coeficiente de silueta

El coeficiente de silueta se utiliza como una comprobación complementaria. Valores más altos indican que, en promedio, las observaciones están mejor cohesionadas con su propio conglomerado y más separadas de los demás. Este criterio no reemplaza la selección mediante codo y segunda diferencia; permite valorar si la partición propuesta presenta una estructura interna razonable.

distanciaKmeans <- dist(datosKmeansZ, method = "euclidean")
kEvaluables <- 2:kMaximo
silhouettePromedio <- numeric(length(kEvaluables))

for (i in seq_along(kEvaluables)) {
  k <- kEvaluables[i]
  silhouettePromedio[i] <- mean(
    cluster::silhouette(
      ajustesKmeans[[k]]$cluster,
      distanciaKmeans
    )[, "sil_width"]
  )
}

tablaSilhouette <- tibble(
  K = kEvaluables,
  SilhouettePromedio = silhouettePromedio
)

kMejorSilhouette <- tablaSilhouette$K[
  which.max(tablaSilhouette$SilhouettePromedio)
]

tabla_html(
  tablaSilhouette,
  "Coeficiente de silueta promedio para cada K",
  digitos = 3
)
Coeficiente de silueta promedio para cada K
K SilhouettePromedio
2 0.457
3 0.379
4 0.350
5 0.326
6 0.256
7 0.259
8 0.261
9 0.257
10 0.278
ggplot(tablaSilhouette, aes(x = K, y = SilhouettePromedio)) +
  geom_line(linewidth = 0.8) +
  geom_point(size = 2.4) +
  geom_vline(xintercept = kOptimo, linetype = 2) +
  scale_x_continuous(breaks = tablaSilhouette$K) +
  labs(
    title = "Coeficiente de silueta por número de conglomerados",
    subtitle = paste0(
      "La línea vertical corresponde al K seleccionado por codo y segunda diferencia: ",
      kOptimo
    ),
    x = "Número de Conglomerados K",
    y = "Silueta Promedio"
  ) +
  theme_minimal(base_size = 12)

cat(
  paste0(
    "El método del codo complementado con la segunda diferencia selecciona **K = ",
    kOptimo,
    "**. El mayor coeficiente de silueta se observa en **K = ",
    kMejorSilhouette,
    "**. "
  )
)

El método del codo complementado con la segunda diferencia selecciona K = 2. El mayor coeficiente de silueta se observa en K = 2.

if (kOptimo == kMejorSilhouette) {
  cat("Ambos criterios coinciden, lo que fortalece la evidencia a favor de esta partición.")
} else {
  cat("Los criterios no coinciden exactamente. Se conserva K definido por el criterio principal del estudio —codo y segunda diferencia— y la silueta se utiliza como elemento adicional para valorar la calidad de la solución.")
}

Ambos criterios coinciden, lo que fortalece la evidencia a favor de esta partición.

7.7 6.7 Ajuste final de K-Means

modeloKmeans <- ajustesKmeans[[kOptimo]]

resultadoKmeans <- datos_completos |>
  dplyr::mutate(
    Cluster = factor(modeloKmeans$cluster),
    EstratoFactor = factor(estrato, ordered = TRUE)
  )
calidadKmeans <- tibble(
  Indicador = c(
    "Número de conglomerados",
    "Suma de cuadrados dentro de conglomerados",
    "Suma de cuadrados entre conglomerados",
    "Proporción de variabilidad entre conglomerados",
    "Silueta promedio del K seleccionado"
  ),
  Valor = c(
    kOptimo,
    modeloKmeans$tot.withinss,
    modeloKmeans$betweenss,
    modeloKmeans$betweenss / modeloKmeans$totss,
    tablaSilhouette$SilhouettePromedio[tablaSilhouette$K == kOptimo]
  )
)

tabla_html(
  calidadKmeans,
  "Indicadores del modelo K-Means seleccionado",
  digitos = 3
)
Indicadores del modelo K-Means seleccionado
Indicador Valor
Número de conglomerados 2.000
Suma de cuadrados dentro de conglomerados 24256.330
Suma de cuadrados entre conglomerados 17333.670
Proporción de variabilidad entre conglomerados 0.417
Silueta promedio del K seleccionado 0.457

7.8 6.8 Tamaño y centroides de los conglomerados

Los centroides estandarizados muestran la posición relativa de cada grupo respecto al promedio general. Valores positivos indican niveles superiores al promedio y valores negativos indican niveles inferiores.

centroidesKmeans <- as.data.frame(modeloKmeans$centers) |>
  tibble::rownames_to_column("Cluster")

tabla_html(
  centroidesKmeans,
  "Centroides estandarizados de los conglomerados",
  digitos = 2
)
Centroides estandarizados de los conglomerados
Cluster Precio AreaConstruida Parqueaderos Banios Habitaciones
1 -0.45 -0.45 -0.34 -0.48 -0.36
2 1.06 1.05 0.81 1.14 0.84
tamanosKmeans <- resultadoKmeans |>
  dplyr::count(Cluster, name = "Cantidad") |>
  dplyr::mutate(
    Porcentaje = Cantidad / sum(Cantidad) * 100
  )

tabla_html(
  tamanosKmeans,
  "Tamaño de los conglomerados",
  digitos = 2
)
Tamaño de los conglomerados
Cluster Cantidad Porcentaje
1 5835 70.14
2 2484 29.86

7.9 6.9 Perfil de los conglomerados en unidades originales

Para facilitar la interpretación empresarial, los conglomerados se describen en las unidades originales de las variables y no solamente mediante puntuaciones estandarizadas.

perfilKmeans <- resultadoKmeans |>
  dplyr::group_by(Cluster) |>
  dplyr::summarise(
    Cantidad = dplyr::n(),
    PrecioMediano = median(preciom),
    PrecioPromedio = mean(preciom),
    AreaConstruidaMediana = median(areaconst),
    ParqueaderosPromedio = mean(parqueaderos),
    BaniosPromedio = mean(banios),
    HabitacionesPromedio = mean(habitaciones),
    .groups = "drop"
  )

tabla_html(
  perfilKmeans,
  "Perfil de los conglomerados en unidades originales",
  digitos = 2
)
Perfil de los conglomerados en unidades originales
Cluster Cantidad PrecioMediano PrecioPromedio AreaConstruidaMediana ParqueaderosPromedio BaniosPromedio HabitacionesPromedio
1 5835 260 285.10 93 1.05 2.42 3.08
2 2484 695 783.44 296 2.49 4.74 4.84

7.10 6.10 Estrato como variable descriptiva externa

El estrato no interviene en la construcción de los conglomerados. Después de obtener los grupos, se analiza su distribución para determinar si los segmentos presentan perfiles socioeconómicos diferenciados.

tablaEstratoCluster <- resultadoKmeans |>
  dplyr::count(Cluster, EstratoFactor, name = "Cantidad") |>
  dplyr::group_by(Cluster) |>
  dplyr::mutate(
    Porcentaje = Cantidad / sum(Cantidad) * 100
  ) |>
  dplyr::ungroup() |>
  dplyr::rename(Estrato = EstratoFactor)

tabla_html(
  tablaEstratoCluster,
  "Distribución del estrato dentro de cada conglomerado",
  digitos = 2
)
Distribución del estrato dentro de cada conglomerado
Cluster Estrato Cantidad Porcentaje
1 3 1206 20.67
1 4 1835 31.45
1 5 2037 34.91
1 6 757 12.97
2 3 247 9.94
2 4 294 11.84
2 5 713 28.70
2 6 1230 49.52
ggplot(
  tablaEstratoCluster,
  aes(x = Cluster, y = Cantidad, fill = Estrato)
) +
  geom_col(position = "fill") +
  scale_y_continuous(labels = scales::percent_format()) +
  labs(
    title = "Composición de estrato por conglomerado",
    x = "Conglomerado",
    y = "Participación dentro del conglomerado",
    fill = "Estrato"
  ) +
  theme_minimal(base_size = 12)

7.11 6.11 Representación de los conglomerados en el plano del ACP

K-Means se calcula utilizando las cinco variables estandarizadas. El plano CP1-CP2 se emplea únicamente como recurso de visualización para observar la separación de los grupos en dos dimensiones.

scoresCluster <- as.data.frame(modeloPca$x[, 1:2]) |>
  dplyr::mutate(Cluster = factor(modeloKmeans$cluster))

centrosVisuales <- scoresCluster |>
  dplyr::group_by(Cluster) |>
  dplyr::summarise(
    PC1 = mean(PC1),
    PC2 = mean(PC2),
    .groups = "drop"
  )

ggplot(scoresCluster, aes(x = PC1, y = PC2, color = Cluster)) +
  geom_point(alpha = 0.5, size = 1.4) +
  geom_point(
    data = centrosVisuales,
    aes(x = PC1, y = PC2),
    size = 5,
    shape = 4,
    stroke = 1.4
  ) +
  labs(
    title = "Conglomerados K-Means representados sobre el plano del ACP",
    subtitle = "Los grupos se calcularon en cinco dimensiones estandarizadas; CP1 y CP2 se usan solo para visualización",
    x = paste0("CP1 (", round(varianzaPca[1], 1), "%)"),
    y = paste0("CP2 (", round(varianzaPca[2], 1), "%)"),
    color = "Conglomerado"
  ) +
  theme_minimal(base_size = 12) +
  theme(legend.position = "bottom")

7.12 6.12 Interpretación de los conglomerados

clusterMayorPrecio <- perfilKmeans$Cluster[which.max(perfilKmeans$PrecioMediano)]
clusterMayorArea <- perfilKmeans$Cluster[which.max(perfilKmeans$AreaConstruidaMediana)]
clusterMenorPrecio <- perfilKmeans$Cluster[which.min(perfilKmeans$PrecioMediano)]

cat(
  paste0(
    "El conglomerado **", clusterMayorPrecio,
    "** presenta el mayor precio mediano, mientras que el conglomerado **",
    clusterMenorPrecio,
    "** presenta el menor. El conglomerado **",
    clusterMayorArea,
    "** concentra la mayor área construida mediana. "
  )
)

El conglomerado 2 presenta el mayor precio mediano, mientras que el conglomerado 1 presenta el menor. El conglomerado 2 concentra la mayor área construida mediana.

cat(
  "Estas diferencias permiten interpretar los conglomerados como perfiles de oferta inmobiliaria y no como categorías previamente conocidas. La distribución posterior del estrato aporta contexto socioeconómico sin haber condicionado la formación de los grupos."
)

Estas diferencias permiten interpretar los conglomerados como perfiles de oferta inmobiliaria y no como categorías previamente conocidas. La distribución posterior del estrato aporta contexto socioeconómico sin haber condicionado la formación de los grupos. El análisis de conglomerados permite pasar de la estructura continua identificada mediante ACP a una segmentación operativa de la oferta inmobiliaria. Los grupos obtenidos por K-Means no deben entenderse como categorías naturales o previamente existentes en el mercado, sino como perfiles construidos a partir de similitudes multivariadas en precio, área, habitaciones, baños y parqueaderos. Esta distinción es fundamental: el algoritmo no “descubre tipos oficiales de vivienda”, sino regiones del espacio de características donde determinados inmuebles resultan más semejantes entre sí que respecto a los demás grupos.

La solución obtenida muestra que la segmentación está muy relacionada con la escala económica y física de los inmuebles. En particular, el conglomerado 2 reúne simultáneamente el mayor precio mediano y la mayor área construida mediana, mientras que el conglomerado 1 se encuentra en el extremo inferior del precio. Esta correspondencia entre precio y tamaño sugiere que una parte importante de la estructura de los conglomerados reproduce el gradiente previamente identificado mediante ACP. La coincidencia entre dos métodos con objetivos diferentes fortalece la interpretación: el ACP identifica la dirección dominante de variabilidad y K-Means permite ver perfiles diferenciados.

Sin embargo, el valor del agrupamiento no consiste únicamente en ordenar viviendas de “menor a mayor”. Debe observarse la configuración conjunta de las demás variables dentro de cada conglomerado. Dos segmentos pueden diferir no solamente por precio, sino por la manera en que ese precio se acompaña de área, baños, habitaciones y parqueaderos. Esto permite distinguir entre tamaño, dotación y posicionamiento económico, evitando reducir el mercado a una clasificación exclusivamente monetaria. Desde esta perspectiva, los centroides y las medianas deben interpretarse como perfiles resultantes de la combinación de varias variales y no como simples rankings de precio.

También es importante reconocer una limitación inherente al método. K-Means obliga a cada inmueble a pertenecer a un conglomerado incluso cuando se encuentra próximo a la frontera entre dos grupos. Por ello, la existencia de conglomerados no implica necesariamente separaciones absolutas en el mercado. Si el ACP muestra continuidad o superposición entre observaciones, los clusters deben interpretarse como una simplificación útil, no como segmentos completamente independientes.

La incorporación posterior del estrato adquiere especial valor analítico precisamente porque esta variable no participó en la formación de los conglomerados. Si posteriormente aparecen diferencias en su distribución entre clusters, estas no fueron impuestas por el algoritmo, sino que emergen como una característica asociada con los perfiles encontrados. Esto permite evaluar hasta qué punto una segmentación construida exclusivamente con características físicas y económicas reproduce también patrones socioeconómicos del mercado inmobiliario.

8 7. Análisis de Correspondencias

8.1 7.1 Preparación de las variables categóricas

El análisis de correspondencias se utiliza para representar asociaciones entre categorías de variables cualitativas. En esta etapa estrato se trata explícitamente como una variable categórica ordinal. Se estudian dos relaciones con sentido inmobiliario y consistentes con la estructura de la base: Zona-Estrato y Tipo de Vivienda-Estrato.

No se utiliza barrio como variable principal del análisis de correspondencias porque presenta un número elevado de categorías, lo que produciría una tabla muy dispersa y una representación factorial difícil de interpretar.

datosCorrespondenciaZona <- inmuebles_imp |>
  dplyr::filter(!is.na(estrato), !is.na(zona)) |>
  dplyr::mutate(
    Estrato = factor(
      estrato,
      levels = sort(unique(estrato)),
      ordered = TRUE
    ),
    Zona = factor(zona)
  )

datosCorrespondenciaTipo <- inmuebles_imp |>
  dplyr::filter(!is.na(estrato), !is.na(tipo)) |>
  dplyr::mutate(
    Estrato = factor(
      estrato,
      levels = sort(unique(estrato)),
      ordered = TRUE
    ),
    TipoVivienda = factor(tipo)
  )

8.2 7.2 Correspondencia entre Zona y Estrato

8.2.1 Tabla de contingencia y prueba de independencia

tablaZonaEstrato <- table(
  datosCorrespondenciaZona$Zona,
  datosCorrespondenciaZona$Estrato
)

colnames(tablaZonaEstrato) <- paste0("Estrato", colnames(tablaZonaEstrato))

as.data.frame.matrix(tablaZonaEstrato) |>
  tibble::rownames_to_column("Zona") |>
  tabla_html(
    "Tabla de contingencia entre Zona y Estrato",
    digitos = 0
  )
Tabla de contingencia entre Zona y Estrato
Zona Estrato3 Estrato4 Estrato5 Estrato6
Zona Centro 105 14 4 1
Zona Norte 572 407 769 172
Zona Oeste 54 84 290 770
Zona Oriente 340 8 2 1
Zona Sur 382 1616 1685 1043
chiZonaEstrato <- chisq.test(tablaZonaEstrato)

resumenChiZonaEstrato <- tibble(
  ChiCuadrado = as.numeric(chiZonaEstrato$statistic),
  GradosLibertad = as.numeric(chiZonaEstrato$parameter),
  ValorP = chiZonaEstrato$p.value,
  PorcentajeEsperadasMenor5 = mean(chiZonaEstrato$expected < 5) * 100
)

tabla_html(
  resumenChiZonaEstrato,
  "Prueba Chi-cuadrado entre Zona y Estrato",
  digitos = 5
)
Prueba Chi-cuadrado entre Zona y Estrato
ChiCuadrado GradosLibertad ValorP PorcentajeEsperadasMenor5
3830.435 12 0 0
if (chiZonaEstrato$p.value < 0.05) {
  cat("El p-valor es inferior a 0,05, por lo que se rechaza la hipótesis de independencia. Existe evidencia estadística de asociación entre la zona y el estrato de los inmuebles.")
} else {
  cat("El p-valor no es inferior a 0,05, por lo que no existe evidencia suficiente para rechazar la hipótesis de independencia entre zona y estrato.")
}

El p-valor es inferior a 0,05, por lo que se rechaza la hipótesis de independencia. Existe evidencia estadística de asociación entre la zona y el estrato de los inmuebles.

8.2.2 Residuos estandarizados

Los residuos estandarizados permiten identificar las combinaciones de categorías que aparecen con una frecuencia mayor o menor a la esperada bajo independencia. Valores absolutos altos muestran las celdas que más contribuyen a la asociación.

residuosZonaEstrato <- as.data.frame(as.table(chiZonaEstrato$stdres)) |>
  dplyr::rename(
    Zona = Var1,
    Estrato = Var2,
    ResiduoEstandarizado = Freq
  ) |>
  dplyr::mutate(Magnitud = abs(ResiduoEstandarizado)) |>
  dplyr::arrange(desc(Magnitud))

tabla_html(
  head(residuosZonaEstrato, 12),
  "Mayores residuos estandarizados entre Zona y Estrato",
  digitos = 3
)
Mayores residuos estandarizados entre Zona y Estrato
Zona Estrato ResiduoEstandarizado Magnitud
Zona Oriente Estrato3 40.033 40.033
Zona Oeste Estrato6 35.437 35.437
Zona Sur Estrato3 -25.852 25.852
Zona Sur Estrato4 20.620 20.620
Zona Centro Estrato3 19.861 19.861
Zona Norte Estrato6 -17.490 17.490
Zona Norte Estrato3 16.219 16.219
Zona Oeste Estrato4 -15.929 15.929
Zona Oriente Estrato5 -13.220 13.220
Zona Oeste Estrato3 -12.768 12.768
Zona Oriente Estrato6 -10.596 10.596
Zona Oriente Estrato4 -10.227 10.227
ggplot(
  residuosZonaEstrato,
  aes(x = Estrato, y = Zona, fill = ResiduoEstandarizado)
) +
  geom_tile() +
  geom_text(aes(label = round(ResiduoEstandarizado, 1)), size = 3.2) +
  scale_fill_gradient2(midpoint = 0) +
  labs(
    title = "Residuos estandarizados entre Zona y Estrato",
    x = "Estrato",
    y = "Zona",
    fill = "Residuo"
  ) +
  theme_minimal(base_size = 12)

8.2.3 Mapa de correspondencias e inercia

modeloCaZonaEstrato <- FactoMineR::CA(
  tablaZonaEstrato,
  graph = FALSE
)
factoextra::fviz_ca_biplot(
  modeloCaZonaEstrato,
  repel = TRUE
) +
  labs(
    title = "Mapa de correspondencias entre Zona y Estrato",
    subtitle = "La proximidad relativa permite reconocer asociaciones entre categorías"
  ) +
  theme_minimal(base_size = 12)

inerciaZonaEstrato <- as.data.frame(modeloCaZonaEstrato$eig) |>
  tibble::rownames_to_column("Dimension") |>
  dplyr::rename(
    Autovalor = eigenvalue,
    PorcentajeInercia = `percentage of variance`,
    PorcentajeAcumulado = `cumulative percentage of variance`
  )

tabla_html(
  inerciaZonaEstrato,
  "Inercia explicada en la correspondencia Zona-Estrato",
  digitos = 2
)
Inercia explicada en la correspondencia Zona-Estrato
Dimension Autovalor PorcentajeInercia PorcentajeAcumulado
dim 1 0.32 69.97 69.97
dim 2 0.13 27.68 97.65
dim 3 0.01 2.35 100.00
factoextra::fviz_screeplot(
  modeloCaZonaEstrato,
  addlabels = TRUE
) +
  labs(
    title = "Inercia explicada: Zona y Estrato",
    x = "Dimensión",
    y = "Porcentaje de Inercia Explicada"
  ) +
  theme_minimal(base_size = 12)

8.3 7.3 Correspondencia entre Tipo de Vivienda y Estrato

8.3.1 Tabla de contingencia y prueba de independencia

tablaTipoEstrato <- table(
  datosCorrespondenciaTipo$TipoVivienda,
  datosCorrespondenciaTipo$Estrato
)

colnames(tablaTipoEstrato) <- paste0("Estrato", colnames(tablaTipoEstrato))

as.data.frame.matrix(tablaTipoEstrato) |>
  tibble::rownames_to_column("TipoVivienda") |>
  tabla_html(
    "Tabla de contingencia entre Tipo de Vivienda y Estrato",
    digitos = 0
  )
Tabla de contingencia entre Tipo de Vivienda y Estrato
TipoVivienda Estrato3 Estrato4 Estrato5 Estrato6
Apartamento 639 1404 1766 1291
Casa 814 725 984 696
chiTipoEstrato <- chisq.test(tablaTipoEstrato)

resumenChiTipoEstrato <- tibble(
  ChiCuadrado = as.numeric(chiTipoEstrato$statistic),
  GradosLibertad = as.numeric(chiTipoEstrato$parameter),
  ValorP = chiTipoEstrato$p.value,
  PorcentajeEsperadasMenor5 = mean(chiTipoEstrato$expected < 5) * 100
)

tabla_html(
  resumenChiTipoEstrato,
  "Prueba Chi-cuadrado entre Tipo de Vivienda y Estrato",
  digitos = 5
)
Prueba Chi-cuadrado entre Tipo de Vivienda y Estrato
ChiCuadrado GradosLibertad ValorP PorcentajeEsperadasMenor5
224.331 3 0 0
if (chiTipoEstrato$p.value < 0.05) {
  cat("El p-valor es inferior a 0,05, por lo que se rechaza la hipótesis de independencia. Existe evidencia estadística de asociación entre el tipo de vivienda y el estrato.")
} else {
  cat("El p-valor no es inferior a 0,05, por lo que no existe evidencia suficiente para rechazar la hipótesis de independencia entre tipo de vivienda y estrato.")
}

El p-valor es inferior a 0,05, por lo que se rechaza la hipótesis de independencia. Existe evidencia estadística de asociación entre el tipo de vivienda y el estrato.

8.3.2 Residuos estandarizados

residuosTipoEstrato <- as.data.frame(as.table(chiTipoEstrato$stdres)) |>
  dplyr::rename(
    TipoVivienda = Var1,
    Estrato = Var2,
    ResiduoEstandarizado = Freq
  ) |>
  dplyr::mutate(Magnitud = abs(ResiduoEstandarizado)) |>
  dplyr::arrange(desc(Magnitud))

tabla_html(
  head(residuosTipoEstrato, 12),
  "Mayores residuos estandarizados entre Tipo de Vivienda y Estrato",
  digitos = 3
)
Mayores residuos estandarizados entre Tipo de Vivienda y Estrato
TipoVivienda Estrato ResiduoEstandarizado Magnitud
Apartamento Estrato3 -14.927 14.927
Casa Estrato3 14.927 14.927
Casa Estrato4 -5.097 5.097
Apartamento Estrato4 5.097 5.097
Apartamento Estrato6 3.847 3.847
Casa Estrato6 -3.847 3.847
Apartamento Estrato5 3.833 3.833
Casa Estrato5 -3.833 3.833
ggplot(
  residuosTipoEstrato,
  aes(x = Estrato, y = TipoVivienda, fill = ResiduoEstandarizado)
) +
  geom_tile() +
  geom_text(aes(label = round(ResiduoEstandarizado, 1)), size = 3.2) +
  scale_fill_gradient2(midpoint = 0) +
  labs(
    title = "Residuos estandarizados entre Tipo de Vivienda y Estrato",
    x = "Estrato",
    y = "Tipo de Vivienda",
    fill = "Residuo"
  ) +
  theme_minimal(base_size = 12)

8.3.3 Mapa de correspondencias e inercia

modeloCaTipoEstrato <- FactoMineR::CA(
  tablaTipoEstrato,
  graph = FALSE
)
# Verificar que el objeto contenga coordenadas de filas y columnas
if (
  !is.null(modeloCaTipoEstrato$row$coord) &&
  !is.null(modeloCaTipoEstrato$col$coord)
) {

  numeroDimensionesTipoEstrato <- ncol(
    as.matrix(modeloCaTipoEstrato$row$coord)
  )

  if (numeroDimensionesTipoEstrato >= 2) {

    factoextra::fviz_ca_biplot(
      modeloCaTipoEstrato,
      repel = TRUE
    ) +
      labs(
        title = "Mapa de Correspondencias entre Tipo de Vivienda y Estrato",
        subtitle = paste0(
          "Las dos primeras dimensiones representan las principales ",
          "asociaciones entre las categorías"
        )
      ) +
      theme_minimal(base_size = 12)

  } else {

    coordenadasTipo <- as.data.frame(
      modeloCaTipoEstrato$row$coord
    ) |>
      tibble::rownames_to_column("Categoria")

    coordenadasEstrato <- as.data.frame(
      modeloCaTipoEstrato$col$coord
    ) |>
      tibble::rownames_to_column("Categoria")

    names(coordenadasTipo)[2] <- "Dimension1"
    names(coordenadasEstrato)[2] <- "Dimension1"

    coordenadasTipo <- coordenadasTipo |>
      dplyr::mutate(
        Origen = "Tipo de Vivienda"
      ) |>
      dplyr::select(
        Categoria,
        Dimension1,
        Origen
      )

    coordenadasEstrato <- coordenadasEstrato |>
      dplyr::mutate(
        Origen = "Estrato"
      ) |>
      dplyr::select(
        Categoria,
        Dimension1,
        Origen
      )

    coordenadasCaTipo <- dplyr::bind_rows(
      coordenadasTipo,
      coordenadasEstrato
    )

    ggplot(
      coordenadasCaTipo,
      aes(
        x = Dimension1,
        y = Origen,
        label = Categoria,
        shape = Origen
      )
    ) +
      geom_vline(
        xintercept = 0,
        linetype = 2
      ) +
      geom_point(
        size = 3
      ) +
      geom_text(
        vjust = -0.8,
        check_overlap = TRUE
      ) +
      labs(
        title = "Correspondencia entre Tipo de Vivienda y Estrato",
        subtitle = paste0(
          "La tabla genera una sola dimensión factorial; ",
          "las categorías se representan sobre el eje principal"
        ),
        x = "Dimensión 1",
        y = NULL,
        shape = NULL
      ) +
      theme_minimal(base_size = 12) +
      theme(
        legend.position = "none"
      )
  }

} else {

  cat(
    "No fue posible construir el mapa porque el objeto ",
    "`modeloCaTipoEstrato` no contiene las coordenadas ",
    "esperadas de un análisis de correspondencias de FactoMineR."
  )
}

inerciaTipoEstrato <- as.data.frame(modeloCaTipoEstrato$eig) |>
  tibble::rownames_to_column("Dimension") |>
  dplyr::rename(
    Autovalor = eigenvalue,
    PorcentajeInercia = `percentage of variance`,
    PorcentajeAcumulado = `cumulative percentage of variance`
  )

tabla_html(
  inerciaTipoEstrato,
  "Inercia explicada en la correspondencia Tipo de Vivienda-Estrato",
  digitos = 2
)
Inercia explicada en la correspondencia Tipo de Vivienda-Estrato
Dimension Autovalor PorcentajeInercia PorcentajeAcumulado
dim 1 0.03 100 100
factoextra::fviz_screeplot(
  modeloCaTipoEstrato,
  addlabels = TRUE
) +
  labs(
    title = "Inercia explicada: Tipo de Vivienda y Estrato",
    x = "Dimensión",
    y = "Porcentaje de Inercia Explicada"
  ) +
  theme_minimal(base_size = 12)

8.4 7.4 Síntesis del análisis de correspondencias

cat("**Zona y Estrato:** ")

Zona y Estrato:

if (chiZonaEstrato$p.value < 0.05) {
  cat("se identifica una asociación estadísticamente significativa. ")
} else {
  cat("no se identifica evidencia estadística suficiente de asociación. ")
}

se identifica una asociación estadísticamente significativa.

cat(
  paste0(
    "Las dos primeras dimensiones del análisis de correspondencias explican **",
    round(sum(modeloCaZonaEstrato$eig[1:min(2, nrow(modeloCaZonaEstrato$eig)), 2]), 1),
    "%** de la inercia.\n\n"
  )
)

Las dos primeras dimensiones del análisis de correspondencias explican 97.6% de la inercia.

cat("**Tipo de Vivienda y Estrato:** ")

Tipo de Vivienda y Estrato:

if (chiTipoEstrato$p.value < 0.05) {
  cat("se identifica una asociación estadísticamente significativa. ")
} else {
  cat("no se identifica evidencia estadística suficiente de asociación. ")
}

se identifica una asociación estadísticamente significativa.

cat(
  paste0(
    "Las dos primeras dimensiones explican **",
    round(sum(modeloCaTipoEstrato$eig[1:min(2, nrow(modeloCaTipoEstrato$eig)), 2]), 1),
    "%** de la inercia de esta tabla."
  )
)

Las dos primeras dimensiones explican 100% de la inercia de esta tabla.

La interpretación de los mapas se realiza conjuntamente con la prueba Chi-cuadrado y los residuos estandarizados. La proximidad entre categorías en el plano factorial sugiere patrones de asociación, pero no debe interpretarse como causalidad.

El análisis de correspondencias complementa los métodos anteriores porque desplaza la atención desde las características cuantitativas del inmueble hacia la estructura de asociación entre categorías del mercado. Las pruebas de independencia muestran evidencia estadística muy fuerte de asociación tanto entre Zona y Estrato como entre Tipo de Vivienda y Estrato, con valores p<2×10 −16. Por tanto, la distribución observada de los estratos no puede considerarse independiente de la localización ni de la tipología de vivienda dentro de esta muestra.

No obstante, un p-valor extremadamente pequeño no indica por sí mismo que la asociación sea fuerte ni permite identificar qué categorías explican dicha relación. Con un tamaño de muestra suficiente, incluso desviaciones relativamente moderadas respecto de la independencia pueden producir significancia estadística. Por ello, la interpretación debe apoyarse en la inercia, las posiciones factoriales y especialmente los residuos estandarizados, que permiten determinar qué combinaciones aparecen con una frecuencia superior o inferior a la esperada bajo independencia. Esta distinción evita confundir evidencia de asociación con magnitud de asociación.

Zona × Estrato

En la relación Zona–Estrato, el resultado sugiere que la dimensión socioeconómica de la oferta inmobiliaria posee también una expresión territorial. Los estratos no se distribuyen aleatoriamente entre las zonas consideradas, lo que indica que la localización contiene información relacionada con la composición socioeconómica de la oferta inmobiliaria. Desde una perspectiva de mercado, esto implica que la ubicación no funciona únicamente como una coordenada geográfica, sino como un componente de la estructura de segmentación.

Sin embargo, la proximidad entre una zona y un estrato en el mapa factorial debe interpretarse como una asociación relativa dentro de la tabla de contingencia y no como equivalencia, causalidad o exclusividad. Una zona próxima a determinado estrato no significa que todos sus inmuebles pertenezcan a él; significa que esa combinación contribuye de manera particular a diferenciarse del patrón esperado bajo independencia.

Tipo de vivienda × Estrato

La asociación entre Tipo de Vivienda y Estrato añade una segunda dimensión a esta estructura. El resultado indica que las tipologías residenciales tampoco se distribuyen de manera homogénea entre los niveles socioeconómicos. En consecuencia, el estrato no parece estar relacionado únicamente con el valor o la localización, sino también con la forma que adopta la oferta habitacional.

La lectura conjunta de ambas correspondencias resulta bien interesante: si el estrato presenta asociaciones tanto con la zona como con el tipo de vivienda, entonces la diferenciación socioeconómica observada en la base parece estar articulada simultáneamente con territorio y tipología residencial. Esto revela una estructura más compleja que una simple relación “mayor estrato = mayor precio”: el mercado observado presenta una organización multidimensional donde ubicación, características físicas, tipología y contexto socioeconómico se encuentran relacionados.

Estas asociaciones son descriptivas y no causales. Los resultados permiten afirmar que las categorías aparecen relacionadas en la muestra analizada, pero no que la zona cause determinado estrato ni que el estrato determine el tipo de vivienda. Para formular explicaciones causales serían necesarios otros diseños y variables de control.

9 8. Integración de hallazgos

Las técnicas empleadas responden a preguntas diferentes y complementarias. El análisis de correlaciones describe asociaciones lineales o monotónicas entre pares de variables; el ACP resume la variabilidad conjunta de las características cuantitativas; K-Means construye segmentos de inmuebles a partir de similitudes multivariadas; y el análisis de correspondencias estudia la asociación entre categorías, utilizando el estrato en su naturaleza ordinal/categórica.

tablaIntegracion <- tibble::tribble(
  ~Tecnica, ~Pregunta, ~VariablesPrincipales, ~ResultadoEsperado,
  "Correlaciones", "¿Qué variables se asocian?", "Variables cuantitativas y Estrato", "Fuerza y dirección de asociación",
  "ACP", "¿Qué dimensiones resumen la variabilidad?", "Precio, AreaConstruida, Parqueaderos, Banios y Habitaciones", "Componentes principales",
  "K-Means", "¿Qué perfiles homogéneos existen?", "Cinco variables cuantitativas estandarizadas", "Segmentos de inmuebles",
  "Correspondencias", "¿Qué categorías aparecen asociadas?", "Zona-Estrato y Tipo de Vivienda-Estrato", "Mapas de asociación categórica"
)

tabla_html(
  tablaIntegracion,
  "Integración de las técnicas multivariadas utilizadas",
  digitos = 2
)
Integración de las técnicas multivariadas utilizadas
Tecnica Pregunta VariablesPrincipales ResultadoEsperado
Correlaciones ¿Qué variables se asocian? Variables cuantitativas y Estrato Fuerza y dirección de asociación
ACP ¿Qué dimensiones resumen la variabilidad? Precio, AreaConstruida, Parqueaderos, Banios y Habitaciones Componentes principales
K-Means ¿Qué perfiles homogéneos existen? Cinco variables cuantitativas estandarizadas Segmentos de inmuebles
Correspondencias ¿Qué categorías aparecen asociadas? Zona-Estrato y Tipo de Vivienda-Estrato Mapas de asociación categórica

10 9. Conclusiones

cat(
  paste0(
    "1. El ACP se construyó con cinco variables cuantitativas y excluyó `Estrato` como variable activa debido a su naturaleza ordinal. Los dos primeros componentes explican **",
    round(sum(varianzaPca[1:2]), 1),
    "%** de la variabilidad total, mientras que para alcanzar al menos el 80% se requieren **",
    k80Pca,
    " componentes**.\n\n"
  )
)
  1. El ACP se construyó con cinco variables cuantitativas y excluyó Estrato como variable activa debido a su naturaleza ordinal. Los dos primeros componentes explican 80.9% de la variabilidad total, mientras que para alcanzar al menos el 80% se requieren 2 componentes.
cat(
  paste0(
    "2. El análisis K-Means seleccionó **",
    kOptimo,
    " conglomerados** mediante el método del codo complementado con la segunda diferencia. Para esta solución, la silueta promedio fue **",
    round(tablaSilhouette$SilhouettePromedio[tablaSilhouette$K == kOptimo], 3),
    "**.\n\n"
  )
)
  1. El análisis K-Means seleccionó 2 conglomerados mediante el método del codo complementado con la segunda diferencia. Para esta solución, la silueta promedio fue 0.457.
cat(
  "3. El estrato no fue utilizado para construir los conglomerados, sino para caracterizarlos después de la segmentación, evitando imponerle una interpretación métrica dentro de la distancia euclidiana.\n\n"
)
  1. El estrato no fue utilizado para construir los conglomerados, sino para caracterizarlos después de la segmentación, evitando imponerle una interpretación métrica dentro de la distancia euclidiana.
cat(
  paste0(
    "4. La prueba de independencia entre Zona y Estrato obtuvo un p-valor de **",
    format.pval(chiZonaEstrato$p.value, digits = 3),
    "**, mientras que la relación entre Tipo de Vivienda y Estrato obtuvo un p-valor de **",
    format.pval(chiTipoEstrato$p.value, digits = 3),
    "**. Estas pruebas orientan la interpretación posterior de los mapas de correspondencias.\n\n"
  )
)
  1. La prueba de independencia entre Zona y Estrato obtuvo un p-valor de <0.0000000000000002, mientras que la relación entre Tipo de Vivienda y Estrato obtuvo un p-valor de <0.0000000000000002. Estas pruebas orientan la interpretación posterior de los mapas de correspondencias.
cat(
  "5. La combinación de ACP, K-Means y análisis de correspondencias permite estudiar la oferta inmobiliaria desde tres perspectivas distintas: reducción de dimensionalidad, segmentación de inmuebles y asociación entre categorías."
)
  1. La combinación de ACP, K-Means y análisis de correspondencias permite estudiar la oferta inmobiliaria desde tres perspectivas distintas: reducción de dimensionalidad, segmentación de inmuebles y asociación entre categorías. El resultado global sugiere que la heterogeneidad del mercado inmobiliario analizado no responde a una sola variable. Existe un eje cuantitativo relacionado con tamaño, dotación y valor; sobre ese espacio pueden identificarse perfiles de oferta mediante conglomerados; y paralelamente aparecen asociaciones entre estrato, territorio y tipo de vivienda. Por tanto, interpretar exclusivamente el precio sería insuficiente: inmuebles con valores semejantes pueden diferenciarse en configuración física, ubicación o tipología, mientras que determinadas configuraciones socioeconómicas aparecen asociadas de manera no aleatoria con ciertas categorías territoriales y residenciales.

Esto también permite entender por qué fue metodológicamente apropiado no introducir indiscriminadamente todas las variables en un único algoritmo. Las variables cuantitativas fueron estudiadas mediante ACP y K-Means, mientras que las cualitativas y ordinales se analizaron mediante correspondencias o se utilizaron posteriormente para caracterizar los segmentos. Esta separación respeta la naturaleza de las variables y evita imponer distancias métricas artificiales sobre categorías que no necesariamente las admiten.

  • La oferta inmobiliaria analizada presenta una estructura multivariada y no puede explicarse adecuadamente mediante el precio de forma aislada. El ACP evidencia que precio, área construida, habitaciones, baños y parqueaderos comparten patrones de variación, lo que permite interpretar una parte importante de la heterogeneidad de los inmuebles a partir de dimensiones latentes relacionadas con su escala física, dotación y posicionamiento económico. Esto demuestra el valor del análisis multivariado frente a una lectura basada exclusivamente en indicadores individuales.

  • La segmentación obtenida mediante K-Means transforma esa heterogeneidad continua en perfiles operativos de oferta. El hecho de que el conglomerado 2 concentre simultáneamente el mayor precio mediano y la mayor área construida mediana, mientras el conglomerado 1 presenta el menor precio mediano, evidencia una diferenciación económica y física entre los segmentos. Sin embargo, estos grupos deben entenderse como perfiles exploratorios dependientes de las variables, escalamiento y número de clusters seleccionados, y no como categorías universales del mercado.

  • Existe coherencia entre la estructura revelada por ACP y la segmentación obtenida mediante K-Means. Mientras el primero muestra los principales ejes sobre los cuales se diferencian los inmuebles, el segundo identifica agrupaciones dentro de ese mismo espacio multivariado. Esta convergencia aporta consistencia interna al análisis y sugiere que los conglomerados responden a patrones estructurales presentes en los datos, aunque su delimitación concreta continúe siendo una construcción algorítmica.

  • La dimensión socioeconómica y territorial amplía la interpretación de los perfiles cuantitativos. Las asociaciones estadísticamente significativas entre Zona–Estrato y Tipo de Vivienda–Estrato muestran que la estructura de la oferta no está determinada únicamente por las características internas del inmueble. La ubicación, la tipología residencial y el contexto socioeconómico aparecen relacionados, configurando un mercado donde las dimensiones físicas, económicas, territoriales y categóricas interactúan. No obstante, estas asociaciones deben entenderse en términos descriptivos y no causales.

  • El principal aprendizaje del ejercicio no radica en identificar un componente, un número de clusters o una asociación particular, sino en reconocer que diferentes técnicas multivariadas responden preguntas distintas y complementarias sobre un mismo fenómeno. El ACP sintetiza estructura, K-Means segmenta observaciones y el análisis de correspondencias identifica asociaciones categóricas. Integrados, permiten pasar de describir inmuebles individuales a comprender patrones globales de organización de la oferta. Desde una perspectiva de Ciencia de Datos, esta integración también muestra que la elección de una técnica debe estar determinada por la naturaleza de las variables y por la pregunta analítica, y no simplemente por la disponibilidad del algoritmo.

En síntesis, los resultados muestran que la oferta inmobiliaria estudiada puede entenderse como un sistema heterogéneo en el que valor, dimensión física, dotación, localización, tipología y contexto socioeconómico forman estructuras relacionadas pero no equivalentes. El ACP permitió reconocer los ejes principales de esa heterogeneidad; K-Means convirtió parte de ella en segmentos interpretables; y el análisis de correspondencias mostró que la dimensión categórica del mercado tampoco se distribuye de manera independiente. La principal fortaleza del análisis consiste, por tanto, en la convergencia de perspectivas: ninguna técnica explica por sí sola el fenómeno, pero juntas proporcionan una representación más completa de cómo se organiza la oferta inmobiliaria. Al mismo tiempo, los resultados deben mantenerse dentro de su alcance exploratorio: los conglomerados dependen de las decisiones adoptadas y las asociaciones encontradas no implican causalidad. Esta combinación entre capacidad interpretativa y reconocimiento explícito de las limitaciones constituye una lectura más sólida de los resultados desde la Ciencia de Datos.

Teniendo lo anterior para este caso de estudio, el mercado inmobiliario analizado se organiza como un continuo de perfiles que van desde inmuebles de menor escala física y económica hasta propiedades de mayor tamaño, dotación y precio. El PCA revela ese gradiente general; K-Means lo convierte en segmentos o perfiles concretos de inmuebles; y el análisis de correspondencias muestra que estos patrones económicos y físicos se insertan además en una estructura territorial y socioeconómica, donde zona, tipo de vivienda y estrato no se distribuyen de manera independiente.

11 10. Recomendaciones

  1. Utilizar los conglomerados como perfiles exploratorios de oferta para orientar estrategias comerciales, evitando tratarlos como categorías universales o permanentes del mercado.
  2. Interpretar los centroides y perfiles de cada conglomerado en unidades originales, especialmente precio y área construida, para facilitar su uso en decisiones empresariales.
  3. Mantener el estrato como variable de caracterización y correspondencia, en lugar de incorporarlo directamente como variable métrica en ACP o K-Means.
  4. Revisar periódicamente la solución de K-Means cuando se incorporen nuevos inmuebles, porque el número y composición de los grupos pueden cambiar con la oferta disponible.
  5. Complementar la lectura de los mapas de correspondencias con los residuos estandarizados para evitar interpretar únicamente la cercanía visual entre categorías.

12 11. Limitaciones

  • El análisis es exploratorio y describe la estructura contenida en la base disponible; no establece relaciones causales.
  • K-Means favorece conglomerados relativamente compactos alrededor de centroides y su solución puede verse afectada por observaciones extremas y por la forma real de los grupos.
  • La elección de K se apoya en un criterio geométrico de codo y segunda diferencia; la silueta se utiliza como validación complementaria y puede sugerir una solución diferente.
  • El estrato es ordinal. Su tratamiento como factor en correspondencias conserva mejor su naturaleza categórica que su inclusión directa como variable cuantitativa continua.
  • Los mapas de correspondencias representan asociaciones relativas entre categorías y deben interpretarse junto con la prueba de independencia, los residuos y la inercia explicada.

13 12. Anexos

13.1 12.1 Frecuencias de variables categóricas

frecuenciasCategoricas <- purrr::map_dfr(
  c("zona", "piso", "estrato", "tipo", "barrio"),
  function(v) {
    inmuebles_imp |>
      dplyr::count(.data[[v]], name = "Frecuencia", sort = TRUE) |>
      dplyr::mutate(
        Variable = v,
        Categoria = as.character(.data[[v]]),
        Porcentaje = Frecuencia / sum(Frecuencia) * 100
      ) |>
      dplyr::select(Variable, Categoria, Frecuencia, Porcentaje)
  }
)

tabla_html(
  frecuenciasCategoricas,
  "Frecuencias de las variables categóricas",
  digitos = 2
)
Frecuencias de las variables categóricas
Variable Categoria Frecuencia Porcentaje
zona Zona Sur 4726 56.81
zona Zona Norte 1920 23.08
zona Zona Oeste 1198 14.40
zona Zona Oriente 351 4.22
zona Zona Centro 124 1.49
piso
2635 31.67
piso 02 1450 17.43
piso 03 1097 13.19
piso 01 860 10.34
piso 04 607 7.30
piso 05 567 6.82
piso 06 245 2.95
piso 08 211 2.54
piso 07 204 2.45
piso 09 146 1.76
piso 10 130 1.56
piso 11 84 1.01
piso 12 83 1.00
estrato 5 2750 33.06
estrato 4 2129 25.59
estrato 6 1987 23.89
estrato 3 1453 17.47
tipo Apartamento 5100 61.31
tipo Casa 3219 38.69
barrio valle del lili 1008 12.12
barrio ciudad jardín 516 6.20
barrio pance 409 4.92
barrio la flora 366 4.40
barrio santa teresita 262 3.15
barrio el caney 208 2.50
barrio el ingenio 202 2.43
barrio la hacienda 164 1.97
barrio acopi 158 1.90
barrio los cristales 154 1.85
barrio normandía 154 1.85
barrio el limonar 135 1.62
barrio prados del norte 126 1.51
barrio el refugio 120 1.44
barrio aguacatal 109 1.31
barrio ciudad 2000 95 1.14
barrio caney 88 1.06
barrio cristales 83 1.00
barrio urbanización la flora 83 1.00
barrio brisas de los 81 0.97
barrio zona sur 74 0.89
barrio nueva tequendama 73 0.88
barrio quintas de don 72 0.87
barrio versalles 71 0.85
barrio santa isabel 63 0.76
barrio parcelaciones pance 61 0.73
barrio el peñon 60 0.72
barrio el lido 59 0.71
barrio torres de comfandi 57 0.69
barrio capri 56 0.67
barrio san fernando 54 0.65
barrio juanamb√∫ 53 0.64
barrio melendez 52 0.63
barrio santa monica 51 0.61
barrio villa del prado 51 0.61
barrio el bosque 49 0.59
barrio ciudad bochalema 48 0.58
barrio san vicente 48 0.58
barrio santa anita 48 0.58
barrio mayapan las vegas 46 0.55
barrio santa rita 45 0.54
barrio colseguros 44 0.53
barrio cuarto de legua 44 0.53
barrio tequendama 44 0.53
barrio bellavista 43 0.52
barrio salomia 40 0.48
barrio santa mónica residencial 39 0.47
barrio arboledas 38 0.46
barrio Cali 37 0.44
barrio primero de mayo 37 0.44
barrio camino real 35 0.42
barrio bochalema 33 0.40
barrio seminario 32 0.38
barrio vipasa 32 0.38
barrio zona norte 32 0.38
barrio chipichape 30 0.36
barrio departamental 29 0.35
barrio nápoles 29 0.35
barrio multicentro 27 0.32
barrio la merced 26 0.31
barrio pampa linda 26 0.31
barrio zona oeste 26 0.31
barrio ciudad los álamos 25 0.30
barrio los cambulos 25 0.30
barrio los guaduales 25 0.30
barrio miraflores 25 0.30
barrio villa del sol 25 0.30
barrio gran limonar 24 0.29
barrio san antonio 24 0.29
barrio las ceibas 23 0.28
barrio meléndez 23 0.28
barrio menga 23 0.28
barrio cerro cristales 22 0.26
barrio ciudad jardin 22 0.26
barrio alfonso lópez 21 0.25
barrio bosques del limonar 21 0.25
barrio cañaverales 21 0.25
barrio ciudadela pasoancho 21 0.25
barrio el ingenio ii 21 0.25
barrio guadalupe 21 0.25
barrio los andes 21 0.25
barrio metropolitano del norte 21 0.25
barrio ciudad cordoba 20 0.24
barrio el ingenio iii 20 0.24
barrio prados del limonar 20 0.24
barrio el guabal 19 0.23
barrio el ingenio i 19 0.23
barrio el troncal 19 0.23
barrio pacara 19 0.23
barrio bella suiza 18 0.22
barrio junin 18 0.22
barrio la arboleda 18 0.22
barrio la floresta 18 0.22
barrio san fernando viejo 18 0.22
barrio zona oriente 18 0.22
barrio chiminangos 17 0.20
barrio ciudadela comfandi 17 0.20
barrio los alcazares 17 0.20
barrio alameda 16 0.19
barrio bretaña 16 0.19
barrio flora industrial 16 0.19
barrio guayaquil 16 0.19
barrio san joaquín 16 0.19
barrio aranjuez 15 0.18
barrio centenario 15 0.18
barrio ciudad córdoba 15 0.18
barrio el jardín 15 0.18
barrio granada 15 0.18
barrio la base 15 0.18
barrio nueva floresta 15 0.18
barrio alamos 14 0.17
barrio champagnat 14 0.17
barrio cristobal colón 14 0.17
barrio tejares de san 14 0.17
barrio ciudad capri 13 0.16
barrio la campiña 13 0.16
barrio cañaveralejo 12 0.14
barrio la independencia 12 0.14
barrio las acacias 12 0.14
barrio pampalinda 12 0.14
barrio calipso 11 0.13
barrio la rivera 11 0.13
barrio la selva 11 0.13
barrio occidente 11 0.13
barrio las granjas 10 0.12
barrio san fernando nuevo 10 0.12
barrio santa elena 10 0.12
barrio villa del lago 10 0.12
barrio atanasio girardot 9 0.11
barrio norte 9 0.11
barrio panamericano 9 0.11
barrio san cayetano 9 0.11
barrio benjamín herrera 8 0.10
barrio calicanto 8 0.10
barrio colinas del sur 8 0.10
barrio el cedro 8 0.10
barrio el gran limonar 8 0.10
barrio la nueva base 8 0.10
barrio san bosco 8 0.10
barrio santa fe 8 0.10
barrio siete de agosto 8 0.10
barrio villas de veracruz 8 0.10
barrio buenos aires 7 0.08
barrio cañasgordas 7 0.08
barrio chapinero 7 0.08
barrio ciudad modelo 7 0.08
barrio la cascada 7 0.08
barrio san juan bosco 7 0.08
barrio santa mónica popular 7 0.08
barrio urbanización tequendama 7 0.08
barrio barranquilla 6 0.07
barrio calima 6 0.07
barrio el castillo 6 0.07
barrio el dorado 6 0.07
barrio floralia 6 0.07
barrio junín 6 0.07
barrio los cámbulos 6 0.07
barrio los parques barranquilla 6 0.07
barrio oasis de comfandi 6 0.07
barrio paso del comercio 6 0.07
barrio pasoancho 6 0.07
barrio popular 6 0.07
barrio prados de oriente 6 0.07
barrio puente del comercio 6 0.07
barrio villa colombia 6 0.07
barrio villa de veracruz 6 0.07
barrio alférez real 5 0.06
barrio arboleda 5 0.06
barrio calimio norte 5 0.06
barrio caney especial 5 0.06
barrio el trébol 5 0.06
barrio la alborada 5 0.06
barrio la alianza 5 0.06
barrio las delicias 5 0.06
barrio los alcázares 5 0.06
barrio manzanares 5 0.06
barrio normandia 5 0.06
barrio santa monica residencial 5 0.06
barrio santo domingo 5 0.06
barrio urbanización río lili 5 0.06
barrio villa del sur 5 0.06
barrio altos de guadalupe 4 0.05
barrio bella suiza alta 4 0.05
barrio centro 4 0.05
barrio colseguros andes 4 0.05
barrio jamundi 4 0.05
barrio la fortaleza 4 0.05
barrio los farallones 4 0.05
barrio los libertadores 4 0.05
barrio pacará 4 0.05
barrio quintas de salomia 4 0.05
barrio saavedra galindo 4 0.05
barrio san carlos 4 0.05
barrio san joaquin 4 0.05
barrio tejares cristales 4 0.05
barrio templete 4 0.05
barrio urbanización barranquilla 4 0.05
barrio urbanización la merced 4 0.05
barrio urbanización la nueva 4 0.05
barrio urbanización san joaquin 4 0.05
barrio 20 de julio 3 0.04
barrio Pance 3 0.04
barrio altos de menga 3 0.04
barrio belalcazar 3 0.04
barrio cambulos 3 0.04
barrio ciudad real 3 0.04
barrio colinas de menga 3 0.04
barrio el paraíso 3 0.04
barrio la buitrera 3 0.04
barrio las américas 3 0.04
barrio libertadores 3 0.04
barrio los guayacanes 3 0.04
barrio morichal de comfandi 3 0.04
barrio municipal 3 0.04
barrio porvenir 3 0.04
barrio primitivo crespo 3 0.04
barrio san pedro 3 0.04
barrio santa bárbara 3 0.04
barrio santa mónica 3 0.04
barrio unión de vivienda 3 0.04
barrio urbanización colseguros 3 0.04
barrio urbanización nueva granada 3 0.04
barrio Ciudad Jardín 2 0.02
barrio La Flora 2 0.02
barrio La Hacienda 2 0.02
barrio Santa Anita 2 0.02
barrio aguablanca 2 0.02
barrio alameda del río 2 0.02
barrio alcazares 2 0.02
barrio alferez real 2 0.02
barrio antonio nariño 2 0.02
barrio base aérea 2 0.02
barrio belisario caicedo 2 0.02
barrio chiminangos 2 etapa 2 0.02
barrio ciudad pacifica 2 0.02
barrio cristóbal colón 2 0.02
barrio el diamante 2 0.02
barrio el prado 2 0.02
barrio eucarístico 2 0.02
barrio evaristo garcía 2 0.02
barrio guaduales 2 0.02
barrio juanambu 2 0.02
barrio la libertad 2 0.02
barrio la rivera i 2 0.02
barrio la rivera ii 2 0.02
barrio lourdes 2 0.02
barrio napoles 2 0.02
barrio paseo de los 2 0.02
barrio poblado campestre 2 0.02
barrio portada de comfandi 2 0.02
barrio prados del sur 2 0.02
barrio primavera 2 0.02
barrio refugio 2 0.02
barrio san judas tadeo 2 0.02
barrio san luis 2 0.02
barrio santa monica norte 2 0.02
barrio santa monica popular 2 0.02
barrio sector cañaveralejo guadalupe 2 0.02
barrio urbanizacion lili 2 0.02
barrio 3 de julio 1 0.01
barrio Belalcazar 1 0.01
barrio Brisas De Los 1 0.01
barrio Bueno Madrid 1 0.01
barrio Camino Real 1 0.01
barrio Centenario 1 0.01
barrio Chiminangos 1 0.01
barrio Ciudad 2000 1 0.01
barrio Ciudad Pacifica 1 0.01
barrio Colseguros Andes 1 0.01
barrio El Bosque 1 0.01
barrio El Caney 1 0.01
barrio El Ingenio 1 0.01
barrio Los Guaduales 1 0.01
barrio Miraflores 1 0.01
barrio Prados Del Limonar 1 0.01
barrio Prados Del Norte 1 0.01
barrio Quintas De Don 1 0.01
barrio San Fernando 1 0.01
barrio Santa Isabel 1 0.01
barrio Santa Monica 1 0.01
barrio Santa Teresita 1 0.01
barrio Santafe 1 0.01
barrio Santo Domingo 1 0.01
barrio Valle Del Lili 1 0.01
barrio Villa Del Prado 1 0.01
barrio Villas De Veracruz 1 0.01
barrio agua blanca 1 0.01
barrio alameda del rio 1 0.01
barrio alborada 1 0.01
barrio alfonso lopez 1 0.01
barrio alfonso lópez i 1 0.01
barrio alto jordán 1 0.01
barrio altos de santa 1 0.01
barrio arboleda campestre candelaria 1 0.01
barrio autopista sur 1 0.01
barrio bajo aguacatal 1 0.01
barrio barrio 7de agosto 1 0.01
barrio barrio el recuerdo 1 0.01
barrio barrio eucarístico 1 0.01
barrio barrio obrero 1 0.01
barrio barrio tranquilo y 1 0.01
barrio berlin 1 0.01
barrio bloques del limonar 1 0.01
barrio bolivariano 1 0.01
barrio bosques de alboleda 1 0.01
barrio boyacá 1 0.01
barrio brisas de guadalupe 1 0.01
barrio brisas del guabito 1 0.01
barrio brisas del limonar 1 0.01
barrio caldas 1 0.01
barrio cali bella 1 0.01
barrio cali canto 1 0.01
barrio calibella 1 0.01
barrio calicanto viii 1 0.01
barrio campestre 1 0.01
barrio cascajal 1 0.01
barrio cataya real 1 0.01
barrio cañaverales los samanes 1 0.01
barrio ceibas 1 0.01
barrio centelsa 1 0.01
barrio cerros de guadalupe 1 0.01
barrio chiminangos 1 etapa 1 0.01
barrio ciudad antejardin 1 0.01
barrio ciudad country 1 0.01
barrio ciudad córdoba reservado 1 0.01
barrio ciudad del campo 1 0.01
barrio ciudad jardin pance 1 0.01
barrio ciudad los alamos 1 0.01
barrio ciudad melendez 1 0.01
barrio ciudad meléndez 1 0.01
barrio ciudad talanga 1 0.01
barrio ciudad universitaria 1 0.01
barrio ciudadela del río 1 0.01
barrio ciudadela melendez 1 0.01
barrio ciudadela paso ancho 1 0.01
barrio colinas del bosque 1 0.01
barrio colon 1 0.01
barrio comfenalco 1 0.01
barrio compartir 1 0.01
barrio conjunto gibraltar 1 0.01
barrio ed benjamin herrera 1 0.01
barrio el guabito 1 0.01
barrio el ingenio 3 1 0.01
barrio el jordán 1 0.01
barrio el nacional 1 0.01
barrio el rodeo 1 0.01
barrio el sena 1 0.01
barrio el vallado 1 0.01
barrio farrallones de pance 1 0.01
barrio fenalco kennedy 1 0.01
barrio fepicol 1 0.01
barrio flora 1 0.01
barrio fonaviemcali 1 0.01
barrio francisco eladio ramirez 1 0.01
barrio fuentes de la 1 0.01
barrio gaitan 1 0.01
barrio guadalupe alto 1 0.01
barrio hacienda alferez real 1 0.01
barrio ingenio 1 0.01
barrio ingenio i 1 0.01
barrio ingenio ii 1 0.01
barrio jamundi alfaguara 1 0.01
barrio jorge eliecer gaitán 1 0.01
barrio jorge isaacs 1 0.01
barrio jose manuel marroquín 1 0.01
barrio la ceibas 1 0.01
barrio la esmeralda 1 0.01
barrio la gran colombia 1 0.01
barrio la luisa 1 0.01
barrio la morada 1 0.01
barrio la playa 1 0.01
barrio la portada al 1 0.01
barrio la primavera 1 0.01
barrio la reforma 1 0.01
barrio la riverita 1 0.01
barrio la riviera 1 0.01
barrio la villa del 1 0.01
barrio laflora 1 0.01
barrio lares de comfenalco 1 0.01
barrio las camelias 1 0.01
barrio las quintas de 1 0.01
barrio las vegas 1 0.01
barrio las vegas de 1 0.01
barrio los alamos 1 0.01
barrio los cristales club 1 0.01
barrio los jockeys 1 0.01
barrio los robles 1 0.01
barrio mamellan 1 0.01
barrio mariano ramos 1 0.01
barrio marroquín iii 1 0.01
barrio miradol del aguacatal 1 0.01
barrio normandía west point 1 0.01
barrio norte la flora 1 0.01
barrio nueva base 1 0.01
barrio oasis de pasoancho 1 0.01
barrio palmas del ingenio 1 0.01
barrio parque residencial el 1 0.01
barrio ponce 1 0.01
barrio portales de comfandi 1 0.01
barrio puente palma 1 0.01
barrio rafael uribe uribe 1 0.01
barrio rep√∫blica de israel 1 0.01
barrio rincon de la 1 0.01
barrio rincón de salomia 1 0.01
barrio riveras del valle 1 0.01
barrio rozo la torre 1 0.01
barrio samanes 1 0.01
barrio samanes de guadalupe 1 0.01
barrio sameco 1 0.01
barrio san judas 1 0.01
barrio san luís 1 0.01
barrio san nicolas 1 0.01
barrio san nicolás 1 0.01
barrio santa 1 0.01
barrio santa anita sur 1 0.01
barrio santa helena de 1 0.01
barrio santa mónica alta 1 0.01
barrio santa rosa 1 0.01
barrio santander 1 0.01
barrio sector aguacatal 1 0.01
barrio sierras de normandía 1 0.01
barrio simón bolivar 1 0.01
barrio tequendema 1 0.01
barrio terrón colorado 1 0.01
barrio unicentro cali 1 0.01
barrio urbanizacion el saman 1 0.01
barrio urbanizacion gratamira 1 0.01
barrio urbanización boyacá 1 0.01
barrio urbanización las cascadas 1 0.01
barrio urbanización pacara 1 0.01
barrio valle de lili 1 0.01
barrio valle grande 1 0.01
barrio villa del parque 1 0.01
barrio zona centro 1 0.01
barrio zona norte los 1 0.01
barrio zona residencial 1 0.01

13.2 12.2 Parámetros de reproducibilidad

parametrosReproducibilidad <- tibble(
  Parametro = c(
    "Semilla general",
    "Algoritmo K-Means",
    "Número de inicializaciones",
    "Máximo de iteraciones",
    "Rango evaluado de K"
  ),
  Valor = c(
    "20260730",
    "Hartigan-Wong",
    as.character(nstartKmeans),
    as.character(iterMaxKmeans),
    paste0("1-", kMaximo)
  )
)

tabla_html(
  parametrosReproducibilidad,
  "Parámetros utilizados para reproducibilidad",
  digitos = 0
)
Parámetros utilizados para reproducibilidad
Parametro Valor
Semilla general 20260730
Algoritmo K-Means Hartigan-Wong
Número de inicializaciones 50
Máximo de iteraciones 100
Rango evaluado de K 1-10

13.3 12.3 Información de sesión

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] paqueteMODELOS_0.1.0 summarytools_1.1.5   gridExtra_2.3.1     
##  [4] broom_1.0.13         boot_1.3-32          GGally_2.4.0        
##  [7] mice_3.19.0          naniar_1.1.0         cluster_2.1.8.3     
## [10] factoextra_2.2.0     FactoMineR_2.16      corrplot_0.95       
## [13] scales_1.4.0         kableExtra_1.4.1     knitr_1.51          
## [16] forcats_1.0.1        purrr_1.2.2          tidyr_1.3.2         
## [19] dplyr_1.2.1          ggplot2_4.0.3       
## 
## loaded via a namespace (and not attached):
##   [1] Rdpack_2.6.6         tcltk_4.5.2          rlang_1.3.0         
##   [4] magrittr_2.0.5       otel_0.2.0           matrixStats_1.5.0   
##   [7] compiler_4.5.2       mgcv_1.9-3           reshape2_1.4.5      
##  [10] systemfonts_1.3.2    vctrs_0.7.3          stringr_1.6.0       
##  [13] sysfonts_0.8.9       pkgconfig_2.0.3      shape_1.4.6.1       
##  [16] fastmap_1.2.0        magick_2.9.1         backports_1.5.1     
##  [19] labeling_0.4.3       pander_0.6.6         rmarkdown_2.31      
##  [22] nloptr_2.2.1         visdat_0.6.0         xfun_0.60           
##  [25] glmnet_5.0           jomo_2.7-6           showtext_0.9-8      
##  [28] cachem_1.1.0         jsonlite_2.0.0       flashClust_1.1-4    
##  [31] pan_2.0              irlba_2.3.7          R6_2.6.1            
##  [34] bslib_0.11.0         stringi_1.8.7        RColorBrewer_1.1-3  
##  [37] car_3.1-5            rpart_4.1.24         lubridate_1.9.5     
##  [40] jquerylib_0.1.4      estimability_2.0.0   Rcpp_1.1.2          
##  [43] iterators_1.0.14     base64enc_0.1-6      pacman_0.5.1        
##  [46] timechange_0.4.0     Matrix_1.7-4         splines_4.5.2       
##  [49] nnet_7.3-20          tidyselect_1.2.1     abind_1.4-8         
##  [52] rstudioapi_0.19.0    yaml_2.3.12          ggtext_0.1.2        
##  [55] codetools_0.2-20     plyr_1.8.9           lattice_0.22-7      
##  [58] tibble_3.3.1         withr_3.0.3          S7_0.2.2            
##  [61] evaluate_1.0.5       survival_3.8-3       ggstats_0.13.0      
##  [64] xml2_1.6.0           ggpubr_1.0.0         pillar_1.11.1       
##  [67] carData_3.0-6        checkmate_2.3.4      DT_0.34.0           
##  [70] foreach_1.5.2        reformulas_0.4.4     generics_0.1.4      
##  [73] minqa_1.2.8          xtable_1.8-8         leaps_3.2           
##  [76] glue_1.8.1           emmeans_2.0.4        scatterplot3d_0.3-45
##  [79] tools_4.5.2          lme4_2.0-6           ggsignif_0.6.4      
##  [82] mvtnorm_1.4-2        rapportools_1.2      grid_4.5.2          
##  [85] rbibutils_2.4.1      nlme_3.1-168         showtextdb_3.0      
##  [88] Formula_1.2-5        cli_3.6.6            textshaping_1.0.5   
##  [91] viridisLite_0.4.3    svglite_2.2.2        gtable_0.3.6        
##  [94] rstatix_1.1.0        sass_0.4.10          digest_0.6.39       
##  [97] ggrepel_0.9.8        htmlwidgets_1.6.4    farver_2.1.2        
## [100] htmltools_0.5.9      lifecycle_1.0.5      multcompView_0.1-12 
## [103] mitml_0.4-5          gridtext_0.1.6       MASS_7.3-65