INTRODUCCIÓN

El mercado inmobiliario urbano presenta una dinámica compleja influenciada por múltiples factores, entre ellos las características físicas de las propiedades, su ubicación geográfica y el contexto socioeconómico de las zonas en las que se encuentran. Comprender estas interacciones es clave para identificar patrones de oferta y orientar la toma de decisiones tanto de inversionistas como de entidades del sector.

En este estudio se emplean herramientas estadísticas multivariantes, como el Análisis de Componentes Principales (PCA), el Análisis de Conglomerados y el Análisis de Correspondencia Múltiple (ACM), con el propósito de reducir la dimensionalidad de la información, segmentar el mercado en grupos homogéneos y explorar las asociaciones entre variables categóricas relevantes, tales como el tipo de vivienda, la zona y el barrio. Este enfoque permite obtener una visión integral de la estructura del mercado, resaltando las características que más inciden en la variación de precios y en la distribución espacial de la oferta.

El análisis se desarrolla a partir de un conjunto de datos consolidados y depurados, que integra atributos cuantitativos y cualitativos de las propiedades residenciales. Los resultados obtenidos buscan aportar una base sólida para el entendimiento de las tendencias del mercado y servir como insumo en la formulación de estrategias comerciales y de planificación urbana.

ANALISIS EXPLORTORIO

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

library(paqueteMODELOS)
library(knitr)
library(kableExtra)

# Cargar datos
datos <- paqueteMODELOS::vivienda

# Crear tabla descriptiva
Tabla_descriptiva <- data.frame(
  Variable = names(datos),
  Descripción = c(
    "Identificador único de la vivienda",
    "Zona de la ciudad",
    "Piso en el que se encuentra la vivienda",
    "Estrato socioeconómico",
    "Precio (millones de pesos)",
    "Área construida (m²)",
    "Número de parqueaderos",
    "Número de baños",
    "Número de habitaciones",
    "Tipo de vivienda",
    "Barrio",
    "Coordenada de longitud",
    "Coordenada de latitud"
  ),
  Tipo = c(
    "Categórica",
    "Categórica",
    "Numérica",
    "Categórica",
    "Numérica",
    "Numérica",
    "Numérica",
    "Numérica",
    "Numérica",
    "Categórica",
    "Categórica",
    "Ubicación",
    "Ubicación"
  )
)

# Mostrar tabla con estilo
Tabla_descriptiva %>%
  kable("html", caption = "Tabla descriptiva de variables") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed", "responsive"),
                full_width = FALSE,
                position = "center") %>%
  row_spec(0, bold = TRUE, color = "white", background = "#0073C2FF") %>% # Encabezado azul
  row_spec(1:nrow(Tabla_descriptiva), background = ifelse(1:nrow(Tabla_descriptiva) %% 2 == 0, "#f7f7f7", "white"))
Tabla descriptiva de variables
Variable Descripción Tipo
id Identificador único de la vivienda Categórica
zona Zona de la ciudad Categórica
piso Piso en el que se encuentra la vivienda Numérica
estrato Estrato socioeconómico Categórica
preciom Precio (millones de pesos) Numérica
areaconst Área construida (m²) Numérica
parqueaderos Número de parqueaderos Numérica
banios Número de baños Numérica
habitaciones Número de habitaciones Numérica
tipo Tipo de vivienda Categórica
barrio Barrio Categórica
longitud Coordenada de longitud Ubicación
latitud Coordenada de latitud Ubicación

Previo al desarrollo de cualquier investigación basada en una base de datos, resulta esencial llevar a cabo un análisis exploratorio que facilite comprender su estructura, evaluar su calidad y reconocer sus particularidades. Este proceso garantiza que la información utilizada sea exacta y confiable, reduciendo el riesgo de obtener conclusiones equivocadas que puedan impactar de forma negativa en la toma de decisiones.

library(summarytools)

datos_filtrados <- datos[, !names(datos) %in% c("id", "latitud", "longitud")]


print(descr(datos_filtrados), method = "render")

Descriptive Statistics

datos_filtrados

N: 8322
areaconst banios estrato habitaciones parqueaderos preciom
Mean 174.93 3.11 4.63 3.61 1.84 433.89
Std.Dev 142.96 1.43 1.03 1.46 1.12 328.65
Min 30.00 0.00 3.00 0.00 1.00 58.00
Q1 80.00 2.00 4.00 3.00 1.00 220.00
Median 123.00 3.00 5.00 3.00 2.00 330.00
Q3 229.00 4.00 5.00 4.00 2.00 540.00
Max 1745.00 10.00 6.00 10.00 10.00 1999.00
MAD 84.51 1.48 1.48 1.48 1.48 207.56
IQR 149.00 2.00 1.00 1.00 1.00 320.00
CV 0.82 0.46 0.22 0.40 0.61 0.76
Skewness 2.69 0.93 -0.18 1.63 2.33 1.85
SE.Skewness 0.03 0.03 0.03 0.03 0.03 0.03
Kurtosis 12.91 1.13 -1.11 3.98 8.31 3.67
N.Valid 8319 8319 8319 8319 6717 8320
N 8322 8322 8322 8322 8322 8322
Pct.Valid 99.96 99.96 99.96 99.96 80.71 99.98

Generated by summarytools 1.1.1 (R version 4.4.2)
2025-08-11

#Normalizar variables categoricas
normalizar_columnas_base <- function(df, columnas) {
  for (col in columnas) {
    df[[col]] <- tolower(trimws(df[[col]]))

  }
  return(df)
  
}

datos2 <- normalizar_columnas_base(datos,c("id","zona","estrato","tipo","barrio"))

1. Identificar datos duplicados

duplicados <- datos2[duplicated(datos2), ]
sum(duplicated(datos2))
## [1] 1
datos2 <- unique(datos2)

Se identifica que solo una columna está repetida y se procede a eliminar

2. Identificar datos faltante

blancos <- is.na(datos2)

# Contar el número de celdas vacías (NA) por columna
columna_blancos <- colSums(blancos)

# Ver la cantidad de celdas vacías por columna
#print(columna_blancos)


tabla_blancos <- data.frame(
  Variable   = names(columna_blancos),
  NA_n       = as.integer(columna_blancos),
  Porcentaje = round(columna_blancos / nrow(datos2) * 100, 2),
  row.names  = NULL
)


tabla_blancos %>%
  kable("html", caption = "Celdas NA por columna") %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed", "responsive"),
    full_width = FALSE, position = "center"
  ) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#0073C2") %>% # encabezado azul
  column_spec(1, bold = TRUE)
Celdas NA por columna
Variable NA_n Porcentaje
id 2 0.02
zona 2 0.02
piso 2637 31.69
estrato 2 0.02
preciom 1 0.01
areaconst 2 0.02
parqueaderos 1604 19.28
banios 2 0.02
habitaciones 2 0.02
tipo 2 0.02
barrio 2 0.02
longitud 2 0.02
latitud 2 0.02

Ya que se evidencia que la concentracion de datos faltantes se concentra en dos variable se realiza imputación mediante MODA

# Copia de trabajo
datos_imp <- datos2

# (opcional) columnas a excluir de la imputación
excluir <- c("id")  # agrega "latitud","longitud", etc. si no quieres imputarlas
cols <- setdiff(names(datos_imp), excluir)

# Función de moda (sirve para numéricas, factor y character)
moda <- function(x) {
  y <- x
  if (is.character(y)) y[y == ""] <- NA  # trata vacíos como NA (opcional)
  y <- y[!is.na(y)]
  if (!length(y)) return(NA)
  ux <- unique(y)
  ux[which.max(tabulate(match(y, ux)))]
}

# Imputación con moda por columna (simple y directa)
for (v in cols) {
  m <- moda(datos_imp[[v]])
  if (is.na(m)) next
  if (is.factor(datos_imp[[v]])) {
    # asegura que la moda exista como nivel
    lv <- levels(datos_imp[[v]])
    if (!(as.character(m) %in% lv)) levels(datos_imp[[v]]) <- c(lv, as.character(m))
    datos_imp[[v]][is.na(datos_imp[[v]])] <- as.character(m)
  } else {
    datos_imp[[v]][is.na(datos_imp[[v]])] <- m
  }
}


tabla_na <- data.frame(
  Variable   = names(datos_imp),
  NA_n       = colSums(is.na(datos_imp)),
  Porcentaje = round(colMeans(is.na(datos_imp)) * 100, 2),
  row.names  = NULL
)


tabla_na %>%
  kable("html", caption = "") %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed", "responsive"),
    full_width = FALSE, position = "center"
  ) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#0073C2") %>% # encabezado azul
  column_spec(1, bold = TRUE)
Variable NA_n Porcentaje
id 2 0.02
zona 0 0.00
piso 0 0.00
estrato 0 0.00
preciom 0 0.00
areaconst 0 0.00
parqueaderos 0 0.00
banios 0 0.00
habitaciones 0 0.00
tipo 0 0.00
barrio 0 0.00
longitud 0 0.00
latitud 0 0.00

3. Identificar Valores atipicos

# 1) Excluir columnas no numéricas
vars_excluir <- c("id","zona","tipo","barrio","latitud","longitud")
datos_num <- datos_imp[, setdiff(names(datos_imp), vars_excluir), drop = FALSE]
datos_num <- datos_num[, sapply(datos_num, is.numeric), drop = FALSE]

# 2) Transformación log estable (log1p = log(1+x))
datos_log <- as.data.frame(lapply(datos_num, log1p))

# 3) Outliers por IQR, devolviendo índices (mejor para localizar filas)
detectar_idx_iqr <- function(x){
  x <- x[!is.na(x)]
  if (length(x) < 2) return(integer(0))
  Q1 <- quantile(x, .25); Q3 <- quantile(x, .75); I <- Q3 - Q1
  which(x < (Q1 - 1.5*I) | x > (Q3 + 1.5*I))
}

idx_por_var <- lapply(datos_log, detectar_idx_iqr)
resumen <- data.frame(
  Variable = names(idx_por_var),
  Valores_Atipicos = sapply(idx_por_var, length),
  row.names = NULL
)




#Conteo de válidos por variable (en datos_log)
n_valid <- sapply(datos_log, function(x) sum(!is.na(x)))

# Conteo de outliers (ya lo tienes en idx_por_var)
n_out <- sapply(idx_por_var, length)

# Porcentaje de outliers
pct_out <- ifelse(n_valid > 0, round(100 * n_out / n_valid, 2), NA_real_)

# Tabla final
tabla_out <- data.frame(
  Variable   = names(n_out),
  N_validos  = as.integer(n_valid[names(n_out)]),
  N_outliers = as.integer(n_out),
  Porcentaje = pct_out,
  row.names  = NULL
)

# Ordenar por mayor porcentaje (opcional)
tabla_out <- tabla_out[order(-tabla_out$Porcentaje), ]

tabla_out %>%
  kable("html", caption = "") %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed", "responsive"),
    full_width = FALSE, position = "center"
  ) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#0073C2") %>% # encabezado azul
  column_spec(1, bold = TRUE)
Variable N_validos N_outliers Porcentaje
5 habitaciones 8321 888 10.67
3 parqueaderos 8321 183 2.20
4 banios 8321 54 0.65
2 areaconst 8321 12 0.14
1 preciom 8321 0 0.00
vars <- c("preciom","areaconst","parqueaderos","banios","habitaciones")
vars <- intersect(vars, names(datos_imp))

# Conversión segura a numérico
to_num <- function(x) if (is.numeric(x)) x else suppressWarnings(as.numeric(as.character(x)))
X <- setNames(lapply(datos_imp[vars], to_num), vars)
# X: data.frame/lista con variables numéricas
nmv  <- names(X)
n    <- length(nmv)

# Layout automático
nc <- ceiling(sqrt(n)); nr <- ceiling(n / nc)

op <- par(mfrow = c(nr, nc), mar = c(4, 4, 2, 1), bg = "white")

# Paleta disponible
cols <- grDevices::hcl.colors(n, palette = "Set2")

i <- 0
for (nm in nmv) {
  i <- i + 1
  x <- X[[nm]]
  x <- x[is.finite(x) & x > -1]  # log1p válido

  if (length(x)) {
    # Fondo con rejilla
    plot(NA, xlab = "", ylab = "log(1 + x)", xlim = c(0.5, 1.5),
         ylim = range(log1p(x), na.rm = TRUE), axes = FALSE, main = nm)
    grid(nx = NA, ny = NULL, col = gray(0.9), lty = 3)
    axis(2); box(bty = "n")

    # Boxplot con color
    boxplot(log1p(x), add = TRUE,
            col     = adjustcolor(cols[i], 0.85),
            border  = adjustcolor(cols[i], 0.9),
            notch   = TRUE, boxwex = 0.6,
            medcol  = "white", medlwd = 2,
            whisklty = 1, outpch = 16,
            outcol  = adjustcolor(cols[i], 0.85),
            outbg   = adjustcolor(cols[i], 0.55),
            axes = FALSE, names = "")
  } else {
    plot.new(); title(main = paste0(nm, " (sin datos válidos para log1p)"))
  }
}

par(op)

Se optó por remover los valores atípicos en las variables parqueaderos, baños, área construida y precio, ya que en conjunto representan menos del 3 % del total de registros. Además, para los tipos de vivienda analizados, resulta poco coherente que existan valores tan elevados en el número de baños y parqueaderos.

# === Eliminar outliers (IQR en log1p) excepto la variable 'habitaciones' ===

# 1) Variables numéricas a evaluar (excluye id/zona/tipo/barrio/lat/long y 'habitaciones')
vars_excluir <- c("id","zona","tipo","barrio","latitud","longitud")
num_cols <- names(datos_imp)[sapply(datos_imp, is.numeric)]
vars_filtrar <- setdiff(intersect(num_cols, setdiff(names(datos_imp), vars_excluir)), "habitaciones")

# 2) Máscara de outliers en log1p alineada a filas
is_outlier_log1p <- function(x, k = 1.5){
  m  <- rep(FALSE, length(x))
  ok <- is.finite(x) & (x > -1)       # log1p definido para x > -1
  if (sum(ok) < 2) return(m)
  lx <- log1p(x[ok])
  Q1 <- quantile(lx, .25); Q3 <- quantile(lx, .75); I <- Q3 - Q1
  m_ok <- (lx < Q1 - k*I) | (lx > Q3 + k*I)
  m[ok] <- m_ok
  m
}

if (length(vars_filtrar) == 0) {
  message("No hay variables para filtrar (aparte de 'habitaciones').")
  datos_sin_out <- datos_imp
} else {
  masks <- lapply(vars_filtrar, function(v) is_outlier_log1p(datos_imp[[v]]))
  mask_any <- Reduce("|", masks)         # outlier en cualquiera de las variables
  cat("Filas a eliminar:", sum(mask_any), "de", nrow(datos_imp), "\n")
  datos_sin_out <- datos_imp[!mask_any, ]
}
## Filas a eliminar: 242 de 8321

DESARROLLO DE LA ACTIVIDAD

1. Análisis de Componentes Principales

library(ggplot2)
### 1) Preparar datos
excluir <- c("id", "longitud", "latitud", "preciom")
X <- datos_imp[, setdiff(names(datos_imp), excluir)]
X <- X[, sapply(X, is.numeric), drop=FALSE]

### 2) PCA con estandarización
pca <- prcomp(X, center = TRUE, scale. = TRUE)



# Obtener la importancia del PCA (porcentaje y acumulado)
pca_importancia <- summary(pca)$importance[2:3, ]

# Convertir a data frame para la tabla
tabla_pca <- data.frame(
  Componente = colnames(pca_importancia),
  Porcentaje_Varianza = round(pca_importancia[1, ] * 100, 2),
  Varianza_Acumulada  = round(pca_importancia[2, ] * 100, 2)
)

tabla_pca %>%
  kable("html", caption = "") %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed", "responsive"),
    full_width = FALSE, position = "center"
  ) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#0073C2") %>% # encabezado azul
  column_spec(1, bold = TRUE)
Componente Porcentaje_Varianza Varianza_Acumulada
PC1 PC1 63.65 63.65
PC2 PC2 19.33 82.98
PC3 PC3 9.30 92.28
PC4 PC4 7.72 100.00
# Extraer datos del PCA
var_explicada <- summary(pca)$importance[2, ] * 100  # % varianza
df_scree <- data.frame(
  Componente = factor(1:length(var_explicada)),
  Varianza   = var_explicada
)

# Graficar con ggplot2
ggplot(df_scree, aes(x = Componente, y = Varianza)) +
  geom_line(group = 1, color = "#2E86C1", size = 1) +
  geom_point(color = "#E74C3C", size = 3) +
  geom_text(aes(label = paste0(round(Varianza, 1), "%")),
            vjust = -0.8, size = 3.5) +
  labs(title = "Scree Plot - Análisis de Componentes Principales",
       x = "Componente Principal",
       y = "% de Varianza Explicada") +
  theme_minimal(base_size = 12)

El gráfico mostrado corresponde al Scree plot del Análisis de Componentes Principales (PCA). En el eje horizontal se ubican las componentes principales (PC1, PC2, PC3 y PC4), mientras que en el eje vertical se representa el porcentaje de varianza explicada por cada una. Se aprecia que la primera componente (PC1) concentra la mayor proporción de la variabilidad del conjunto de datos, seguida por la segunda (PC2), que también aporta de forma significativa. En conjunto, PC1 y PC2 explican más del 80 % de la variabilidad total, lo que indica que capturan la mayor parte de la información relevante. A partir de la tercera (PC3) y cuarta (PC4) componentes, la varianza explicada es mínima, lo que sugiere que su contribución al modelo es poco significativa.

library(factoextra)

fviz_pca_biplot(
  pca,
  invisible = "ind",        # oculta individuos
  label = "var",            # solo etiquetas de variables
  repel = TRUE,             # evita solapamientos
  col.var = "#E74C3C",      # flechas/labels de variables
  arrowsize = 0.9,
  labelsize = 5,
  title = "Biplot PCA (solo variables)",
  ggtheme = theme_minimal(base_size = 12)
)

En este biplot de PCA (solo variables) se observa que:

La Dimensión 1 (63.7 %) está fuertemente asociada a área construida, baños y habitaciones, ya que sus vectores apuntan en la misma dirección y con gran longitud, lo que indica un peso relevante en la varianza explicada.

La Dimensión 2 (19.3 %) se encuentra más vinculada a parqueaderos, cuyo vector se orienta claramente hacia arriba, aportando información diferenciada respecto a las demás variables.

La cercanía y dirección similar de área construida, baños y habitaciones sugiere una correlación positiva entre ellas, mientras que parqueaderos tiene un patrón más independiente.

En conjunto, las dos primeras dimensiones explican más del 83 % de la variabilidad total, lo que indica que gran parte de la información de los datos puede representarse en este plano bidimensional.

### 5) Cargas (qué variables pesan más en cada PC)

library(knitr)
library(kableExtra)

# Extraer las 5 variables más relevantes para PC1 y PC2
tabla_pc <- data.frame(
  Componente = rep(c("PC1", "PC2"), each = 5),
  Variable = c(
    names(sort(abs(pca$rotation[,1]), decreasing = TRUE)[1:5]),
    names(sort(abs(pca$rotation[,2]), decreasing = TRUE)[1:5])
  ),
  Carga = c(
    sort(abs(pca$rotation[,1]), decreasing = TRUE)[1:5],
    sort(abs(pca$rotation[,2]), decreasing = TRUE)[1:5]
  )
)

# Mostrar tabla con formato
tabla_pc %>%
    kable("html", caption = "") %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed", "responsive"),
    full_width = FALSE, position = "center"
  ) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#0073C2") %>% # encabezado azul
  column_spec(1, bold = TRUE)
Componente Variable Carga
PC1 banios 0.5531529
PC1 areaconst 0.5371736
PC1 habitaciones 0.4565960
PC1 parqueaderos 0.4438315
PC1 NA NA
PC2 parqueaderos 0.7221001
PC2 habitaciones 0.6871824
PC2 banios 0.0614478
PC2 areaconst 0.0507545
PC2 NA NA
### 6) Relación de PCs con precio y oferta
scores <- as.data.frame(pca$x) # coordenadas de observaciones en PCs


# Calcular correlaciones
correlaciones <- cor(scores[, 1:3], datos_imp$preciom, use = "complete.obs")

# Convertir a data.frame
tabla_cor <- data.frame(
  Componente = rownames(correlaciones),
  Correlacion_con_Precio = as.numeric(correlaciones)
)

# Tabla con formato
tabla_cor %>%
kable("html", caption = "") %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed", "responsive"),
    full_width = FALSE, position = "center"
  ) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#0073C2") %>% # encabezado azul
  column_spec(1, bold = TRUE)
Componente Correlacion_con_Precio
PC1 0.7245570
PC2 0.3344887
PC3 -0.1130539

2. Análisis de Conglomerados

# ================================
# ANÁLISIS DE CONGLOMERADOS
# ================================

# 1) Preparar datos
excluir <- c("id")

#datos_imp$estrato <- as.numeric(datos_imp$estrato) 
X <- datos_imp[, setdiff(names(datos_imp), excluir)]
X <- X[, sapply(X, is.numeric), drop = FALSE]

# Escalar variables
Xz <- scale(X)

# 2) Determinar número óptimo de clusters (método del codo)
wss <- numeric(10)
for (k in 1:10) {
  wss[k] <- sum(kmeans(Xz, centers = k, nstart = 25)$tot.withinss)
}

plot(1:10, wss, type="b", pch=19, frame=FALSE,
     xlab="Número de clusters",
     ylab="Suma de cuadrados intra-cluster",
     main="Método del codo para K óptimo")

El gráfico presenta en el eje X el número de clusters (k) y en el eje Y la suma de cuadrados intra-cluster (WSS), indicador de la compacidad de los grupos. Conforme aumenta k, la WSS disminuye, ya que los clusters se vuelven más pequeños y homogéneos. El “método del codo” busca localizar el punto a partir del cual la reducción en la WSS deja de ser significativa; en este caso, el quiebre más notorio se encuentra alrededor de k = 3 o k = 4, lo que sugiere que este rango sería apropiado para definir el número de clusters.

Se seleciona K=4

# >>> Elige el número de clusters (k) según el codo de la curva <<<
k <- 4  # ejemplo, puedes cambiarlo

# 3) Ejecutar K-means
set.seed(123)
km <- kmeans(Xz, centers = k, nstart = 25)

# 4) Resumen de cada cluster
resumen_clusters <- aggregate(X, by = list(Cluster = km$cluster), FUN = mean)

# Dar formato
resumen_clusters %>%
 kable("html", caption = "") %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed", "responsive"),
    full_width = FALSE, position = "center"
  ) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#0073C2") %>% # encabezado azul
  column_spec(1, bold = TRUE)
Cluster preciom areaconst parqueaderos banios habitaciones longitud latitud
1 280.6217 100.5312 1.282470 2.337857 2.882581 -76.53283 3.396847
2 1123.7595 414.3884 3.785482 5.152763 4.462622 -76.53832 3.399622
3 540.9791 249.3830 1.781742 4.213624 4.804912 -76.53411 3.422402
4 240.8490 105.4246 1.203806 2.207489 3.131983 -76.50645 3.467696
# 5) Añadir cluster al dataset original
datos_imp$Cluster <- factor(km$cluster)

# 6) Visualización usando PCA

pca_scores <- as.data.frame(prcomp(Xz)$x[, 1:2])
pca_scores$Cluster <- factor(km$cluster)

ggplot(pca_scores, aes(PC1, PC2, color = Cluster)) +
  geom_point(alpha = 0.8, size = 3, shape = 16) + # Puntos más grandes y sólidos
  scale_color_brewer(palette = "Set2") + # Colores suaves y diferenciados
  labs(
    title = "Clusters de propiedades (proyección PCA)",
    x = "Componente Principal 1",
    y = "Componente Principal 2",
    color = "Cluster"
  ) +
  theme_minimal(base_size = 13) + # Fuente más grande
  theme(
    plot.title = element_text(face = "bold", hjust = 0.5),
    legend.position = "right",
    panel.grid.major = element_line(color = "gray85"),
    panel.grid.minor = element_blank()
  )

library(ggplot2)
library(dplyr)

# 1) Seleccionar solo numéricas para el clustering
excluir <- c("id", "longitud", "latitud", "estrato", "zona", "barrio")
X <- datos_imp[, setdiff(names(datos_imp), excluir)]
X <- X[, sapply(X, is.numeric), drop = FALSE]

# Escalar
Xz <- scale(X)

# 2) Elegir número de clusters (puedes ajustar k según método del codo)
set.seed(123)
k <- 4
km <- kmeans(Xz, centers = k, nstart = 25)

# 3) Añadir cluster al dataset original
datos_imp$Cluster <- factor(km$cluster)

# ================================
# 4) Distribución por estrato, zona y barrio
# ================================

# Tabla por estrato
tabla_estrato <- table(datos_imp$Cluster, datos_imp$estrato)
prop_estrato <- prop.table(tabla_estrato, margin = 1) * 100

# Tabla por zona
tabla_zona <- table(datos_imp$Cluster, datos_imp$zona)
prop_zona <- prop.table(tabla_zona, margin = 1) * 100

# Tabla por barrio
tabla_barrio <- table(datos_imp$Cluster, datos_imp$barrio)
prop_barrio <- prop.table(tabla_barrio, margin = 1) * 100

# Mostrar tablas
cat("\nDistribución por Estrato (%):\n")
## 
## Distribución por Estrato (%):
print(round(prop_estrato, 1))
##    
##        3    4    5    6
##   1 41.0 22.8 30.6  5.6
##   2  0.8  3.7 19.8 75.7
##   3  4.8 15.1 38.2 41.9
##   4 24.2 36.6 33.1  6.0
# Estrato por cluster
ggplot(datos_imp, aes(x = factor(Cluster), fill = factor(estrato))) +
  geom_bar(position = "fill", color = "white", width = 0.7) + # borde blanco y barras más delgadas
  scale_y_continuous(labels = scales::percent_format(accuracy = 1)) +
  scale_fill_brewer(palette = "Set2") + # paleta más suave y diferenciada
  labs(
    title = "Distribución de estratos dentro de cada cluster",
    x = "Cluster",
    y = "% dentro del cluster",
    fill = "Estrato"
  ) +
  theme_minimal(base_size = 13) +
  theme(
    plot.title = element_text(face = "bold", hjust = 0.5),
    legend.position = "right",
    panel.grid.major.y = element_line(color = "gray90"),
    panel.grid.minor = element_blank()
  ) +
  geom_text(
    stat = "prop",
    aes(label = scales::percent(..prop.., accuracy = 0.01)),
    position = position_fill(vjust = 0.5),
    color = "black",
    size = 3
  )

Con la información de estrato y precio, la interpretación se puede enriquecer así:

Cluster 1: Con predominio de estrato 3 y precios moderados, representa viviendas más asequibles, posiblemente con menor área construida y servicios.

Cluster 2: Con alta concentración en estrato 6 y precios elevados, agrupa propiedades de lujo o alto nivel, probablemente con mayores áreas y mejores acabados.

Cluster 3: Con predominio del estrato 5 y precios intermedios-altos, corresponde a viviendas amplias y de buena calidad, pero sin llegar al nivel premium del Cluster 2.

Cluster 4: Con estratos 4 y 5 como mayoría y precios medios, parece representar viviendas de clase media-alta, equilibrando tamaño, ubicación y valor.

En resumen, la segmentación por clusters refleja una correlación entre estrato socioeconómico y precio, donde los clusters con estratos más altos tienden a agrupar las viviendas de mayor valor, mientras que los de estratos más bajos concentran precios más accesibles.

cat("\nDistribución por Zona (%):\n")
## 
## Distribución por Zona (%):
print(round(prop_zona, 1))
##    
##     zona centro zona norte zona oeste zona oriente zona sur
##   1         5.1       23.3        6.2         18.1     47.3
##   2         0.2        9.8       28.3          0.4     61.3
##   3         0.9       19.9       23.6          1.1     54.5
##   4         1.5       27.5        7.6          4.4     58.9
# Zona por cluster

ggplot(datos_imp, aes(x = factor(Cluster), fill = factor(zona))) +
  geom_bar(position = "fill", color = "white", width = 0.7) + # bordes blancos y barras más delgadas
  scale_y_continuous(labels = scales::percent_format(accuracy = 1)) +
  scale_fill_brewer(palette = "Pastel1") + # colores suaves y diferenciados
  labs(
    title = "Distribución de zonas dentro de cada cluster",
    x = "Cluster",
    y = "% dentro del cluster",
    fill = "Zona"
  ) +
  theme_minimal(base_size = 13) +
  theme(
    plot.title = element_text(face = "bold", hjust = 0.5),
    legend.position = "right",
    panel.grid.major.y = element_line(color = "gray90"),
    panel.grid.minor = element_blank()
  ) +
  geom_text(
    stat = "prop",
    aes(label = scales::percent(..prop.., accuracy = 0.01)),
    position = position_fill(vjust = 0.5),
    color = "black",
    size = 3
  )

La distribución por zona dentro de cada cluster revela un patrón marcado de concentración geográfica:

Cluster 1: Alta presencia de viviendas en la zona sur (47,3%), seguido de la zona norte (23,3%) y zona oriente (18,1%). Esto sugiere un perfil más diverso en localización, pero con predominio en el sur.

Cluster 2: Mayoritariamente concentrado en la zona sur (61,3%) y zona oeste (28,3%), lo que podría estar vinculado a áreas residenciales de precios medios-altos, según la composición del cluster.

Cluster 3: También con predominio en la zona sur (54,5%) y norte (23,6%), lo que refleja cierta similitud geográfica con el Cluster 1, aunque con posible diferencia en precios y características constructivas.

Cluster 4: Fuertemente concentrado en la zona sur (58,9%) y norte (27,5%), apuntando a un patrón más focalizado en estas dos zonas.

En general, la zona sur domina en los cuatro clusters, lo que indica que gran parte del mercado inmobiliario analizado se ubica allí. Dado que la zona norte también tiene alta presencia en varios clusters, es probable que estas dos zonas concentren la mayor parte de la oferta de viviendas, con variaciones en precio y características según el cluster.

3. Análisis de Correspondencia

library(FactoMineR)
library(factoextra)

# Seleccionar las variables categóricas
cat_vars <- datos_imp[, c("zona","tipo")]

# Asegurar que sean factores
cat_vars[] <- lapply(cat_vars, factor)

# Ejecutar MCA
mca <- MCA(cat_vars, graph = FALSE)

# Gráfico biplot (categorías y observaciones)
fviz_mca_biplot(mca, repel = TRUE, ggtheme = theme_minimal(),
                
                label = "var",      # Solo etiquetas de variables (categorías)
                invisible = "ind",   # Ocultar individuos
                title = "Análisis de Correspondencia Múltiple")

En el Eje Dim1 (25,8%) se observa una clara diferenciación entre, por un lado, la zona oeste y el apartamento (ubicados hacia la izquierda del plano) y, por otro, la casa, la zona oriente y la zona centro (hacia la derecha). Este patrón sugiere que los apartamentos tienden a concentrarse principalmente en la zona oeste, mientras que las casas presentan una mayor presencia en las zonas oriente y centro.

El Eje Dim2 (20%) distingue a la zona norte (en la parte superior) del resto de áreas, lo que indica un comportamiento particular de la oferta en esa zona.

Por su parte, la zona sur se ubica próxima al centro del gráfico, lo que revela que no mantiene una asociación marcada con un tipo de vivienda específico, a diferencia de otras zonas.

En conjunto, el análisis evidencia agrupamientos bien definidos que permiten identificar patrones geográficos en la oferta:

Apartamentos → predominan en la zona oeste.

Casas → se concentran en la zona oriente y zona centro.

CONCLUSIONES

1)

fviz_pca_biplot(
  pca,
  invisible = "ind",        # oculta individuos
  label = "var",            # solo etiquetas de variables
  repel = TRUE,             # evita solapamientos
  col.var = "#E74C3C",      # flechas/labels de variables
  arrowsize = 0.9,
  labelsize = 5,
  title = "Biplot PCA (solo variables)",
  ggtheme = theme_minimal(base_size = 12)
)

En simple: la PC1 funciona como un índice de tamaño total—cuando sube, también lo hacen el área, los baños y las habitaciones, así que hablamos de viviendas más grandes y, por lo general, más caras. La PC2 diferencia el tipo de uso a igual tamaño: hacia arriba predominan los parqueaderos (público que valora el carro y la movilidad) y hacia abajo predominan los dormitorios (familias que priorizan más cuartos). ¿Qué hacer con esto? En precios y captación, conviene priorizar inmuebles con PC1 alta para elevar el ticket promedio; y en marketing, segmentar mensajes: “comodidad con parqueadero” para los de PC2 alta y “espacio para la familia” para los de PC2 baja.

2)

library(dplyr); library(ggplot2)

centros <- pca_scores %>%
  group_by(Cluster) %>%
  summarise(PC1 = mean(PC1), PC2 = mean(PC2), n = n(), .groups = "drop")

ggplot(pca_scores, aes(PC1, PC2, color = Cluster)) +
  geom_point(alpha = 0.25, size = 1) +
  stat_ellipse(level = 0.8, linewidth = 0.8) +
  geom_label(data = centros,
             aes(label = paste0("C", Cluster, "\nN=", n)),
             color = "black", fill = "white", size = 4, label.size = 0.2) +
  labs(title = "PCA: dispersión y tamaño de cada cluster") +
  theme_minimal()

Se identifican 4 segmentos nítidos (elipses) con tamaños: C1 = 3.611, C3 = 2.158, C4 = 1.629, C2 = 923. Las elipses muestran la concentración y la dispersión de cada grupo: a menor área de la elipse, más homogéneo es el perfil.

En este PCA, la Dimensión 1 (PC1) está asociada sobre todo al tamaño/equipamiento (p. ej., área y baños) y la Dimensión 2 (PC2) a capacidad/funcionalidad (p. ej., habitaciones y parqueaderos). Con esa lectura:

C2 (verde), ubicado a la derecha (PC1 alta), representa el segmento “premium”: viviendas de mayor tamaño y dotación. Es el más caro y de menor volumen (N=923). Oportuno para ventas consultivas y campañas de alto ticket.

C1 (rojo), el segmento más grande (N=3.611), aparece con valores medios/altos en funcionalidad (PC2 ligeramente positiva) y tamaño medio. Ideal para ofertas de rotación, paquetes de financiación y promociones masivas.

C3 (celeste) se concentra cerca del centro con tamaño medio y menor funcionalidad (PC2 negativa). Perfil intermedio para familias jóvenes o compradores que priorizan precio frente a extras

C4 (morado) queda a la izquierda y abajo (PC1 y PC2 bajos): viviendas más compactas y económicas, probablemente apartamentos pequeños. Buen candidato para campañas de entrada (inversión, primera vivienda, arriendo).

3)

library(dplyr)
library(ggplot2)
library(scales)

datos_imp %>% 
  count(zona, tipo) %>%                       # conteos por zona–tipo
  group_by(zona) %>% 
  mutate(pct = n / sum(n)) %>%                # proporción dentro de cada zona
  ggplot(aes(x = zona, y = pct, fill = tipo)) +
  geom_col(width = .75, color = "white") +
  geom_text(aes(label = percent(pct, accuracy = 1)),
            position = position_stack(vjust = 0.5),
            color = "white", size = 3.6) +
  scale_y_continuous(labels = percent_format()) +
  labs(title = "Tipo de vivienda por zona (proporciones)",
       x = "Zona", y = "% dentro de la zona", fill = "Tipo") +
  theme_minimal(base_size = 12) +
  theme(panel.grid.minor = element_blank(),
        legend.position = "top")

La distribución por zonas es clara: los apartamentos se concentran en la zona oeste (86%) y en la zona sur (59%), por lo que son los territorios más convenientes para acelerar rotación y captar más inventario vertical. En cambio, las casas predominan en el centro (81%) y el oriente (82%), ideales para campañas enfocadas en familias y propuestas de valor ligadas a espacio y entorno barrial. La zona norte muestra un perfil mixto con inclinación a apartamentos (62%), lo que la convierte en un buen escenario para ofertas de apartaestudios y proyectos de vivienda en altura.

4)

# --- Paquetes
library(dplyr)
library(ggplot2)
library(forcats)
library(scales)

# === 1) Elegir los barrios con más actividad por zona (p.ej., top 7 por zona)
top_n_barrios <- 7

top_barrios_zona <- datos_imp %>%
  filter(!is.na(zona), !is.na(barrio), !is.na(tipo)) %>%
  count(zona, barrio, name = "total") %>%
  group_by(zona) %>%
  slice_max(total, n = top_n_barrios, with_ties = FALSE) %>%
  ungroup()

# === 2) Proporciones de tipo dentro de cada barrio (solo los top seleccionados)
plot_df <- datos_imp %>%
  inner_join(top_barrios_zona, by = c("zona","barrio")) %>%
  count(zona, barrio, tipo, name = "n") %>%
  group_by(zona, barrio) %>%
  mutate(prop = n / sum(n),
         # Para ordenar los barrios por volumen dentro de cada zona
         total_barrio = sum(n)) %>%
  ungroup() %>%
  group_by(zona) %>%
  mutate(barrio = fct_reorder(barrio, total_barrio)) %>%
  ungroup()

# === 3) Gráfico: barras apiladas (porcentaje por barrio), facet por zona
ggplot(plot_df, aes(x = prop, y = barrio, fill = tipo)) +
  geom_col(width = 0.8, color = "white") +
  geom_text(aes(label = percent(prop, accuracy = 1)),
            position = position_stack(vjust = 0.5),
            size = 3, color = "white") +
  facet_wrap(~ zona, scales = "free_y") +
  scale_x_continuous(labels = percent_format(accuracy = 1)) +
  scale_fill_manual(values = c("#ff7f7f","#33b1ff"), name = "Tipo") +
  labs(title = "Tipo de vivienda por barrio (top por zona)",
       subtitle = "Porcentaje dentro de cada barrio",
       x = "% dentro del barrio", y = "Barrio") +
  theme_minimal(base_size = 12) +
  theme(panel.grid.major.y = element_blank(),
        strip.text = element_text(face = "bold"),
        legend.position = "top")

El análisis por barrios muestra micro-mercados muy definidos: los apartamentos se concentran en la zona oeste (Santa Teresita, Los Cristales, Normandía, etc.) y en varios barrios de la zona norte (Torres de Comfandi, Versalles, Prados del Norte, La Flora), además de Valle del Lili y El Caney en el sur; las casas dominan en la zona centro (Junín, Guayaquil, Alameda, San Juan Bosco) y en la zona oriente (Ciudad Córdoba, Alfonso López, La Floresta). Para la inmobiliaria esto se traduce en una estrategia clara: captar y promocionar apartamentos en Oeste/Norte/Sur y casas en Centro/Oriente, con campañas geolocalizadas por barrio (“vive vertical” vs. “espacio para la familia”). Además, donde un tipo es minoría (p. ej., casas en el Oeste o apartamentos en el Centro) puede existir prima de precio; priorizar esos casos y asignar asesores por clúster de barrios debería elevar la velocidad de rotación y el margen.