Analisis de exploración.

Primero, se verifica en que consiste el data set (datos, variables y tamaño), logramos identificar un total de 8322 propiedades y con base en esto, sacamos algunas variables estadísticas y si hay datos faltantes.

# Cargar paquetes necesarios
library(skimr)
library(dplyr)
library(kableExtra)

# Función mejorada para mostrar resumen - VERSIÓN CORREGIDA
mostrar_resumen_mejorado <- function(data) {
  skim_data <- skim(data)
  
  cat("## Análisis Exploratorio de Datos Inmobiliarios\n\n")
  
  # 1. Resumen General
  cat("### 1. Resumen General\n")
  cat("- **Total de propiedades:**", format(nrow(data), big.mark = ","), "\n")
  cat("- **Variables analizadas:**", ncol(data), "(4 categóricas y 9 numéricas)\n\n")
  
  # 2. Variables Categóricas
  cat("### 2. Variables Categóricas\n")
  cat("#### Distribución de categorías:\n")
  
  categoricas <- skim_data %>% 
    filter(skim_type == "character") %>% 
    select(Variable = skim_variable, 
           `Valores únicos` = character.n_unique,
           `Long. mínima` = character.min,
           `Long. máxima` = character.max)
  
  print(kable(categoricas, align = 'c') %>% 
          kable_styling(bootstrap_options = c("striped", "hover"), 
                        full_width = FALSE))
  
  cat("\n#### Ejemplos de categorías:\n")
  cat("- **Zonas:**", toString(unique(head(data$zona, 5))), "...\n")
  cat("- **Tipos de propiedad:**", toString(unique(data$tipo)), "\n")  # CORRECCIÓN AQUÍ (quité paréntesis extra)
  cat("- **Barrios representativos:**", toString(unique(head(data$barrio, 5))), "...\n\n")
  
  # 3. Variables Numéricas (Versión Simplificada)
  cat("### 3. Variables Numéricas Clave\n")
  
  numericas <- skim_data %>% 
    filter(skim_type == "numeric") %>% 
    select(Variable = skim_variable,
           `Datos faltantes` = n_missing,
           Media = numeric.mean,
           Mediana = numeric.p50,
           Mínimo = numeric.p0,
           Máximo = numeric.p100) %>% 
    mutate(across(where(is.numeric), ~round(., 2)))
  
  print(kable(numericas, align = 'c') %>% 
          kable_styling(bootstrap_options = c("striped", "hover"), 
                        full_width = FALSE))
  
  # 4. Hallazgos Importantes
  cat("\n### 4. Hallazgos Relevantes\n")
  cat("- **Datos faltantes:**\n")
  cat("  - Pisos no especificados:", format(sum(is.na(data$piso)), big.mark = ","), 
      "(", round(mean(is.na(data$piso))*100, 1), "% de los casos)\n")
  cat("  - Parqueaderos no especificados:", format(sum(is.na(data$parqueaderos)), big.mark = ","), 
      "(", round(mean(is.na(data$parqueaderos))*100, 1), "%)\n\n")
  
  cat("- **Valores inusuales:**\n")
  cat("  - Propiedades con 0 baños:", sum(data$banios == 0, na.rm = TRUE), "\n")
  cat("  - Propiedades con 0 habitaciones:", sum(data$habitaciones == 0, na.rm = TRUE), "\n")
  cat("  - Precio máximo observado: $", format(max(data$preciom, na.rm = TRUE), big.mark = ","), " millones\n", sep = "")
  cat("  - Área más grande:", max(data$areaconst, na.rm = TRUE), "m²\n\n")
  
  cat("- **Distribución típica:**\n")
  cat("  - Precio mediano: $", format(median(data$preciom, na.rm = TRUE), big.mark = ","), " millones\n", sep = "")
  cat("  - Área mediana:", median(data$areaconst, na.rm = TRUE), "m²\n")
  cat("  - Estrato más común:", names(which.max(table(data$estrato))), "\n")
}

# Ejecutar la función mejorada
mostrar_resumen_mejorado(vivienda)

Análisis Exploratorio de Datos Inmobiliarios

1. Resumen General

  • Total de propiedades: 8,322
  • Variables analizadas: 13 (4 categóricas y 9 numéricas)

2. Variables Categóricas

Distribución de categorías:

Variable Valores únicos Long. mínima Long. máxima
zona 5 8 12
piso 12 2 2
tipo 2 4 11
barrio 436 4 29

Ejemplos de categorías:

  • Zonas: Zona Oriente, Zona Sur, Zona Norte …
  • Tipos de propiedad: Casa, Apartamento, NA
  • Barrios representativos: 20 de julio, 3 de julio, acopi …

3. Variables Numéricas Clave

Variable Datos faltantes Media Mediana Mínimo Máximo
id 3 4160.00 4160.00 1.00 8319.00
estrato 3 4.63 5.00 3.00 6.00
preciom 2 433.89 330.00 58.00 1999.00
areaconst 3 174.93 123.00 30.00 1745.00
parqueaderos 1605 1.84 2.00 1.00 10.00
banios 3 3.11 3.00 0.00 10.00
habitaciones 3 3.61 3.00 0.00 10.00
longitud 3 -76.53 -76.53 -76.59 -76.46
latitud 3 3.42 3.42 3.33 3.50

4. Hallazgos Relevantes

  • Datos faltantes:
    • Pisos no especificados: 2,638 ( 31.7 % de los casos)
    • Parqueaderos no especificados: 1,605 ( 19.3 %)
  • Valores inusuales:
    • Propiedades con 0 baños: 45
    • Propiedades con 0 habitaciones: 66
    • Precio máximo observado: $1,999 millones
    • Área más grande: 1745 m²
  • Distribución típica:
    • Precio mediano: $330 millones
    • Área mediana: 123 m²
    • Estrato más común: 5

Limpieza

Para la limpieza de datos, ordenamos los datos en categorías definidas, para facilitar el análisis estadístico además se rellena los datos faltantes con la mediana para evitar distorsionar el análisis, por último, Se generaron métricas estandarizadas para cumplir supuestos de modelos lineales y permitir comparaciones espaciales.

# Convertir variables categóricas
vivienda_clean <- vivienda %>%
  mutate(
    estrato_cat = factor(estrato, ordered = TRUE),
    zona = factor(zona, levels = unique(zona)),
    tipo = factor(tipo)
  )

# Imputación de valores faltantes
numeric_cols <- vivienda_clean %>%
  select(where(is.numeric)) %>%
  colnames()

vivienda_clean <- vivienda_clean %>%
  mutate(across(all_of(numeric_cols), 
               ~ifelse(is.na(.), median(., na.rm = TRUE), .)))

# Filtrado de outliers
vivienda_clean <- vivienda_clean %>%
  filter(
    preciom > quantile(preciom, 0.01, na.rm = TRUE) & 
    preciom < quantile(preciom, 0.99, na.rm = TRUE),
    areaconst > quantile(areaconst, 0.01, na.rm = TRUE) & 
    areaconst < quantile(areaconst, 0.99, na.rm = TRUE),
    parqueaderos >= 0
  )

# Creación de nuevas variables
vivienda_clean <- vivienda_clean %>%
  mutate(
    precio_m2 = preciom/areaconst,
    log_precio = log(preciom + 1)
  )

PCA

Para identificar patrones y reducir la complejidad de las variables del mercado inmobiliario, se realizó un análisis de componentes principales (PCA) por medio de las variables: precio, área, parqueadero, número de baños, habitaciones, estrato y precio por metro cuadrado. Previamente, los datos fueron estandarizados para garantizar que todas las variables tuvieran igual peso en el análisis.

El gráfico de varianza explicada muestra que el componente 1 concentra aproximadamente un 45% de la variabilidad total de los datos, mientras que el segundo componente explica un 25% adicional. Esto implica que, utilizando únicamente estos dos ejes, es posible representar la mayor parte de las diferencias entre las viviendas, preservando gran parte de la estructura original de los datos.

# Preparación de datos para PCA
pca_data <- vivienda_clean %>%
  select(preciom, areaconst, parqueaderos, banios, habitaciones, estrato, precio_m2) %>%
  scale()

# PCA
pca_result <- PCA(pca_data, ncp = 5, graph = FALSE)

# Visualización
fviz_eig(pca_result, addlabels = TRUE, 
        main = "Varianza explicada por componentes") +
  theme_minimal()

fviz_pca_var(pca_result, col.var = "contrib",
            gradient.cols = c("#2ecc71", "#f1c40f", "#e74c3c"),
            repel = TRUE,
            title = "Contribución de variables") +
  theme_minimal()

fviz_pca_biplot(pca_result, label = "var", 
               habillage = vivienda_clean$estrato_cat,
               title = "Biplot PCA") +
  scale_color_viridis_d() +
  theme_minimal()

Clustering

Se aplicó el algoritmo K-means sobre variables estandarizadas de precio total, área construida, precio por metro cuadrado y estrato socioeconómico. El método del codo indicó que el número óptimo de grupos era tres, que fueron denominados Económico, Intermedio y clase alta.

*El segmento Económico se caracteriza por precios bajos, áreas construidas reducidas y estratos más bajos. ####

*El Intermedio presenta valores medios en todas las variables. ####

*Clase alta agrupa propiedades de alto valor, mayor superficie y estratos altos.####

Modelización Predictiva

set.seed(123)
kmeans_data <- vivienda_clean %>%
  select(preciom, areaconst, precio_m2, estrato) %>%
  scale()

# Número óptimo de clusters
fviz_nbclust(kmeans_data, kmeans, method = "wss") +
  labs(title = "Número óptimo de clusters")

# K-means clustering
kmeans_result <- kmeans(kmeans_data, centers = 3)
vivienda_clean$cluster <- factor(
  kmeans_result$cluster,
  levels = 1:3,
  labels = c("Económico", "Intermedio", "Premium")
)

# Resumen por cluster
cluster_summary <- vivienda_clean %>%
  group_by(cluster) %>%
  summarise(
    N = n(),
    across(
      c(preciom, areaconst, precio_m2, estrato),
      list(mean = mean, median = median, sd = sd),
      .names = "{.col}_{.fn}"
    )
  )

knitr::kable(cluster_summary, caption = "Resumen por Segmento")
Resumen por Segmento
cluster N preciom_mean preciom_median preciom_sd areaconst_mean areaconst_median areaconst_sd precio_m2_mean precio_m2_median precio_m2_sd estrato_mean estrato_median estrato_sd
Económico 4011 240.0052 226 97.56115 122.0731 92 72.11696 2.205948 2.237762 0.6457226 3.897283 4 0.7256746
Intermedio 2623 499.2989 430 245.80052 130.4813 120 48.68696 3.785413 3.658537 0.8163638 5.451773 5 0.5515560
Premium 1349 798.3039 730 315.38200 373.4558 350 100.62406 2.232404 2.067184 0.9247024 5.269830 5 0.7958451
# Visualización
ggplot(vivienda_clean, aes(x = areaconst, y = preciom, color = cluster)) +
  geom_point(alpha = 0.5) +
  labs(title = "Distribución de Propiedades por Segmento",
       x = "Área Construida (m²)",
       y = "Precio (millones)") +
  theme_minimal()

correspondencia

Se llevó a cabo un análisis de correspondencia para identificar asociaciones entre el tipo de vivienda y su localización por zonas. El resultado gráfico (biplot) muestra que algunos tipos de vivienda tienden a concentrarse en zonas específicas, mientras que otros están más distribuidos

## 5. Análisis de Correspondencia
# Crear tabla de contingencia entre tipo de vivienda y zona
tabla_correspondencia <- with(vivienda_clean, table(tipo, zona))

# Filtrar filas y columnas con conteo cero
tabla_filtrada <- tabla_correspondencia[rowSums(tabla_correspondencia) > 0, 
                                      colSums(tabla_correspondencia) > 0]

# Verificar dimensiones
if(nrow(tabla_filtrada) >= 2 && ncol(tabla_filtrada) >= 2) {
  # Realizar análisis de correspondencia
  ca_result <- FactoMineR::CA(tabla_filtrada, graph = FALSE)
  
  # Calcular número de dimensiones disponibles
  ndim <- min(nrow(tabla_filtrada)-1, ncol(tabla_filtrada)-1)
  
  # Visualización segura
  if(ndim >= 2) {
    print(factoextra::fviz_ca_biplot(ca_result, 
                                   axes = c(1, 2),
                                   title = "Relación entre Tipo de Vivienda y Zona",
                                   repel = TRUE) +
            theme_minimal())
  } else {
    message("Advertencia: Solo hay ", ndim, " dimensión(es) disponibles - no se puede graficar biplot 2D")
    
    # Alternativa: gráfico de mosaico
    mosaicplot(tabla_filtrada, 
              main = "Distribución de Tipos de Vivienda por Zona",
              shade = TRUE,
              las = 2)
  }
} else {
  message("Advertencia: La tabla filtrada tiene dimensiones insuficientes (", 
          nrow(tabla_filtrada), "x", ncol(tabla_filtrada), ")")
  
  # Mostrar tabla de contingencia
  knitr::kable(tabla_filtrada, caption = "Tabla de Contingencia Filtrada")
}

#### Modelización #### ### Se desarrollaron dos modelos para predecir el precio de las propiedades: uno de regresión lineal y otro de árbol de decisión, usando como predictores las características físicas y de ubicación. La evaluación en datos de prueba mostró que ambos modelos ofrecen un ajuste razonable, con valores de R² que indican que capturan una proporción significativa de la variabilidad del precio. El RMSE, indicador del error promedio de predicción, fue similar en ambos casos, aunque el modelo [aquí indicarías según resultados: “lineal”/“árbol”] mostró un desempeño ligeramente mejor. El gráfico comparativo confirma que las predicciones más cercanas a la línea de referencia representan estimaciones más precisas. Estos modelos constituyen una herramienta útil para estimar precios y apoyar la toma de decisiones en valuaciones o negociaciones. ###

## 6. Modelización Predictiva 
# 1. Preparación de datos definitiva
model_data <- vivienda_clean %>%
  select(preciom, areaconst, banios, habitaciones, estrato, zona, precio_m2, log_precio) %>%
  na.omit() %>%
  filter(complete.cases(.))

# Verificación de datos
if(nrow(model_data) < 50) stop("Datos insuficientes para modelar (n < 50)")

# 2. Partición de datos
set.seed(123)
train_index <- sample(1:nrow(model_data), size = 0.8 * nrow(model_data))
train_data <- model_data[train_index, ]
test_data <- model_data[-train_index, ]

# 3. Función segura para modelado
safe_model <- function(formula, data, method) {
  tryCatch({
    model <- train(
      formula,
      data = data,
      method = method,
      trControl = trainControl(method = "cv", number = 5),
      na.action = na.omit
    )
    return(model)
  }, error = function(e) {
    message("Error en modelo ", method, ": ", e$message)
    return(NULL)
  })
}

# 4. Modelado lineal seguro
lm_model <- safe_model(
  log_precio ~ areaconst + banios + habitaciones + estrato + zona,
  train_data,
  "lm"
)

# 5. Modelado de árbol seguro
tree_model <- safe_model(
  log_precio ~ areaconst + banios + habitaciones + estrato + zona,
  train_data,
  "rpart"
)

# 6. Generación de predicciones si los modelos existen
if(!is.null(lm_model)) {
  test_data$pred_lm <- exp(predict(lm_model, newdata = test_data))
  print(summary(lm_model$finalModel))
}
## 
## Call:
## lm(formula = .outcome ~ ., data = dat)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -1.62508 -0.18347 -0.00599  0.17153  1.74997 
## 
## Coefficients:
##                     Estimate Std. Error t value Pr(>|t|)    
## (Intercept)        3.834e+00  2.548e-02 150.494  < 2e-16 ***
## areaconst          2.208e-03  4.237e-05  52.121  < 2e-16 ***
## banios             1.142e-01  4.146e-03  27.553  < 2e-16 ***
## habitaciones      -9.951e-03  3.464e-03  -2.873  0.00409 ** 
## estrato            2.814e-01  4.587e-03  61.354  < 2e-16 ***
## `zonaZona Sur`    -1.940e-02  1.922e-02  -1.009  0.31288    
## `zonaZona Norte`  -1.582e-02  1.953e-02  -0.810  0.41787    
## `zonaZona Oeste`   1.639e-01  2.206e-02   7.430 1.22e-13 ***
## `zonaZona Centro`  2.134e-01  3.353e-02   6.364 2.10e-10 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.2753 on 6375 degrees of freedom
## Multiple R-squared:  0.8064, Adjusted R-squared:  0.8062 
## F-statistic:  3320 on 8 and 6375 DF,  p-value: < 2.2e-16
if(!is.null(tree_model)) {
  test_data$pred_tree <- exp(predict(tree_model, newdata = test_data))
  print(plot(varImp(tree_model), main = "Importancia de Variables - Árbol"))
}

# 7. Comparación final si ambos modelos existen
if(!is.null(lm_model) && !is.null(tree_model)) {
  # Métricas de evaluación
  results <- data.frame(
    Modelo = c("Regresión Lineal", "Árbol de Decisión"),
    RMSE = c(
      sqrt(mean((test_data$preciom - test_data$pred_lm)^2)),
      sqrt(mean((test_data$preciom - test_data$pred_tree)^2))
    ),
    R2 = c(
      cor(test_data$preciom, test_data$pred_lm)^2,
      cor(test_data$preciom, test_data$pred_tree)^2
    )
  )
  
  print(knitr::kable(results, caption = "Comparación de Modelos"))
  
  # Gráfico comparativo
  comparison_plot <- ggplot(test_data, aes(x = preciom)) +
    geom_point(aes(y = pred_lm, color = "Regresión Lineal"), alpha = 0.6) +
    geom_point(aes(y = pred_tree, color = "Árbol de Decisión"), alpha = 0.6) +
    geom_abline(intercept = 0, slope = 1, color = "red", linetype = "dashed") +
    labs(title = "Comparación de Predicciones vs Valores Reales",
         x = "Precio Real (millones)",
         y = "Precio Predicho (millones)") +
    scale_color_manual(values = c("#3498db", "#2ecc71")) +
    theme_minimal()
  
  print(comparison_plot)
} else {
  message("Advertencia: No se pudo completar la comparación de modelos")
}
## 
## 
## Table: Comparación de Modelos
## 
## |Modelo            |     RMSE|        R2|
## |:-----------------|--------:|---------:|
## |Regresión Lineal  | 162.3645| 0.7009038|
## |Árbol de Decisión | 194.7301| 0.5556413|

geografico

Para comprender la distribución espacial de los segmentos de mercado, se elaboró un mapa interactivo con datos de ubicación de las propiedades. Los marcadores, codificados por color según el segmento (Económico, Intermedio, clase alta) y con tamaño proporcional al precio, permitieron identificar patrones claros: las propiedades Premium se concentran en zonas específicas de alta valorización, mientras que las del segmento Económico están más dispersas en áreas periféricas.

## 7. Visualización Geográfica
# Versión corregida del muestreo
sample_size <- min(50, nrow(vivienda_clean))  # Calculamos el tamaño fuera del pipe

sample_data <- vivienda_clean %>%
  group_by(zona, cluster) %>%
  slice_sample(n = sample_size) %>%  # Usamos la variable precalculada
  ungroup()

# Mapa interactivo mejorado
mapa <- leaflet(sample_data) %>%
  addTiles() %>%
  addCircleMarkers(
    lng = ~longitud,
    lat = ~latitud,
    radius = ~sqrt(preciom)/20,  # Radio ajustado para mejor visualización
    color = ~case_when(
      cluster == "Económico" ~ "#3498db",
      cluster == "Intermedio" ~ "#2ecc71",
      TRUE ~ "#e74c3c"
    ),
    fillOpacity = 0.8,
    stroke = FALSE,
    popup = ~paste(
      "<b>Tipo:</b> ", tipo, "<br>",
      "<b>Precio:</b> $", format(round(preciom, 1), big.mark = ","), "M<br>",
      "<b>Área:</b> ", round(areaconst, 1), "m²<br>",
      "<b>Precio/m²:</b> $", format(round(precio_m2, 1), big.mark = ","), "K<br>",
      "<b>Estrato:</b> ", estrato, "<br>",
      "<b>Segmento:</b> ", cluster
    )
  ) %>%
  addLegend(
    position = "bottomright",
    colors = c("#3498db", "#2ecc71", "#e74c3c"),
    labels = c("Económico", "Intermedio", "clase alta"),
    title = "Segmento de Propiedades"
  )

# Mostrar el mapa
mapa