El presente análisis tiene como objetivo comprender de manera integral el mercado mobiliario urbano a partir de una base de datos de propiedades residenciales; con el fin de tomar decisiones estratégicas más informadas sobre compra, venta, valoración y segmentación de propiedades.Para ello, se aplican técnicas multivariadas, que permite desarroillar los siguientes análisis:
Se efectúa la carga de la base vivienda del paqueteMODELOS. Así mismo, se realiza validación si el objeto R se llama vivenda o vivienda, para evitar errores por diferencias en el nombre. Finalmente, la base de convierte en data.frame, se limpian los nombres de las columnas y se excluye la variable barrio, para el análisis.
# Cargar la base de datos
data("vivenda")
# Revisar los objetos disponibles en el ambiente
ls()
## character(0)
if (exists("vivienda")) {
datos <- vivienda
} else if (exists("vivenda")) {
datos <- vivenda
} else {
stop("No se encontró el objeto vivienda ni vivenda. Revise el nombre de la base cargada.")
}
datos <- as.data.frame(datos)
datos <- datos %>% clean_names()
# Se excluye la variable barrio del ejercicio
datos <- datos %>%
select(-matches("^barrio$|barrio"))
head(datos)
dim(datos)
## [1] 8322 12
Se identifican 8322 registros con 12 variables de la revisión inicial
names(datos)
## [1] "id" "zona" "piso" "estrato" "preciom"
## [6] "areaconst" "parqueaderos" "banios" "habitaciones" "tipo"
## [11] "longitud" "latitud"
Se conocen los nombres de las 12 variables.
str(datos)
## 'data.frame': 8322 obs. of 12 variables:
## $ id : num 1147 1169 1350 5992 1212 ...
## $ zona : chr "Zona Oriente" "Zona Oriente" "Zona Oriente" "Zona Sur" ...
## $ piso : chr NA NA NA "02" ...
## $ estrato : num 3 3 3 4 5 5 4 5 5 5 ...
## $ preciom : num 250 320 350 400 260 240 220 310 320 780 ...
## $ areaconst : num 70 120 220 280 90 87 52 137 150 380 ...
## $ parqueaderos: num 1 1 2 3 1 1 2 2 2 2 ...
## $ banios : num 3 2 2 5 2 3 2 3 4 3 ...
## $ habitaciones: num 6 3 4 3 3 3 3 4 6 3 ...
## $ tipo : chr "Casa" "Casa" "Casa" "Casa" ...
## $ longitud : num -76.5 -76.5 -76.5 -76.5 -76.5 ...
## $ latitud : num 3.43 3.43 3.44 3.44 3.46 ...
Asi mismo, se determina el tipo de cada variable para efectuar el analisis, identificando 9 variables numericas y 3 categóricas.
head(datos, 10)
La siguiente tabla resume la estructura de la base y permite validar las variables que son aptas para análisis numéricos o categóricos.
estructura_variables <- tibble(
variable = names(datos),
tipo = sapply(datos, function(x) class(x)[1])
)
kable(
estructura_variables,
caption = "Estructura inicial de variables de la base de datos"
) %>%
kable_styling(full_width = FALSE)
| variable | tipo |
|---|---|
| id | numeric |
| zona | character |
| piso | character |
| estrato | numeric |
| preciom | numeric |
| areaconst | numeric |
| parqueaderos | numeric |
| banios | numeric |
| habitaciones | numeric |
| tipo | character |
| longitud | numeric |
| latitud | numeric |
A continuación, se muestra de manera preliminar la calidad y composición de los datos.
skim(datos)
| Name | datos |
| Number of rows | 8322 |
| Number of columns | 12 |
| _______________________ | |
| Column type frequency: | |
| character | 3 |
| numeric | 9 |
| ________________________ | |
| Group variables | None |
Variable type: character
| skim_variable | n_missing | complete_rate | min | max | empty | n_unique | whitespace |
|---|---|---|---|---|---|---|---|
| zona | 3 | 1.00 | 8 | 12 | 0 | 5 | 0 |
| piso | 2638 | 0.68 | 2 | 2 | 0 | 12 | 0 |
| tipo | 3 | 1.00 | 4 | 11 | 0 | 2 | 0 |
Variable type: numeric
| skim_variable | n_missing | complete_rate | mean | sd | p0 | p25 | p50 | p75 | p100 | hist |
|---|---|---|---|---|---|---|---|---|---|---|
| id | 3 | 1.00 | 4160.00 | 2401.63 | 1.00 | 2080.50 | 4160.00 | 6239.50 | 8319.00 | ▇▇▇▇▇ |
| estrato | 3 | 1.00 | 4.63 | 1.03 | 3.00 | 4.00 | 5.00 | 5.00 | 6.00 | ▅▆▁▇▆ |
| preciom | 2 | 1.00 | 433.89 | 328.65 | 58.00 | 220.00 | 330.00 | 540.00 | 1999.00 | ▇▂▁▁▁ |
| areaconst | 3 | 1.00 | 174.93 | 142.96 | 30.00 | 80.00 | 123.00 | 229.00 | 1745.00 | ▇▁▁▁▁ |
| parqueaderos | 1605 | 0.81 | 1.84 | 1.12 | 1.00 | 1.00 | 2.00 | 2.00 | 10.00 | ▇▁▁▁▁ |
| banios | 3 | 1.00 | 3.11 | 1.43 | 0.00 | 2.00 | 3.00 | 4.00 | 10.00 | ▇▇▃▁▁ |
| habitaciones | 3 | 1.00 | 3.61 | 1.46 | 0.00 | 3.00 | 3.00 | 4.00 | 10.00 | ▂▇▂▁▁ |
| longitud | 3 | 1.00 | -76.53 | 0.02 | -76.59 | -76.54 | -76.53 | -76.52 | -76.46 | ▁▅▇▂▁ |
| latitud | 3 | 1.00 | 3.42 | 0.04 | 3.33 | 3.38 | 3.42 | 3.45 | 3.50 | ▃▇▅▇▅ |
Antes de aplicar los análisis multivariados, se revisa la calidad básica de la información, especialmente valores faltantes, tipos de variables y posibles inconsistencias.
A continuación, se detallan los datos y porcentaje de los datos faltantes por cada variable, identificando que las variables “piso” y “parqueadero”, cuentan con porcentajes representativos del conjunto de datos.
faltantes <- datos %>%
summarise(across(everything(), ~ sum(is.na(.)))) %>%
pivot_longer(
cols = everything(),
names_to = "variable",
values_to = "cantidad_faltantes"
) %>%
mutate(
porcentaje_faltante = round(cantidad_faltantes / nrow(datos) * 100, 2)
) %>%
arrange(desc(porcentaje_faltante))
kable(
faltantes,
caption = "Cantidad y porcentaje de valores faltantes por variable"
) %>%
kable_styling(full_width = FALSE)
| variable | cantidad_faltantes | porcentaje_faltante |
|---|---|---|
| piso | 2638 | 31.70 |
| parqueaderos | 1605 | 19.29 |
| id | 3 | 0.04 |
| zona | 3 | 0.04 |
| estrato | 3 | 0.04 |
| areaconst | 3 | 0.04 |
| banios | 3 | 0.04 |
| habitaciones | 3 | 0.04 |
| tipo | 3 | 0.04 |
| longitud | 3 | 0.04 |
| latitud | 3 | 0.04 |
| preciom | 2 | 0.02 |
Para este análisis se aplica un tratamiento básico:
moda <- function(x) {
x <- x[!is.na(x)]
if (length(x) == 0) {
return(NA)
}
ux <- unique(x)
ux[which.max(tabulate(match(x, ux)))]
}
datos_limpios <- datos %>%
mutate(
across(
where(is.numeric),
~ ifelse(is.na(.), median(., na.rm = TRUE), .)
),
across(
where(is.character),
~ ifelse(is.na(.), moda(.), .)
),
across(
where(is.factor),
~ ifelse(is.na(.), moda(.), .)
)
)
datos_limpios <- datos_limpios %>%
mutate(across(where(is.character), as.factor))
faltantes_despues <- datos_limpios %>%
summarise(across(everything(), ~ sum(is.na(.)))) %>%
pivot_longer(
cols = everything(),
names_to = "variable",
values_to = "cantidad_faltantes"
) %>%
arrange(desc(cantidad_faltantes))
kable(
faltantes_despues,
caption = "Valores faltantes después del tratamiento"
) %>%
kable_styling(full_width = FALSE)
| variable | cantidad_faltantes |
|---|---|
| id | 0 |
| zona | 0 |
| piso | 0 |
| estrato | 0 |
| preciom | 0 |
| areaconst | 0 |
| parqueaderos | 0 |
| banios | 0 |
| habitaciones | 0 |
| tipo | 0 |
| longitud | 0 |
| latitud | 0 |
El análisis exploratorio permite conocer el comportamiento inicial de las variables, identificar relaciones y detectar patrones preliminares dentro de la base de viviendas.
Se separan las variables en dos grupos:
variables_numericas <- datos_limpios %>%
select(where(is.numeric))
variables_categoricas <- datos_limpios %>%
select(where(is.factor))
names(variables_numericas)
## [1] "id" "estrato" "preciom" "areaconst" "parqueaderos"
## [6] "banios" "habitaciones" "longitud" "latitud"
names(variables_categoricas)
## [1] "zona" "piso" "tipo"
La siguiente tabla permite conocer la tendencia central y dispersión de las variables
summary(variables_numericas)
## id estrato preciom areaconst
## Min. : 1 Min. :3.000 Min. : 58.0 Min. : 30.0
## 1st Qu.:2081 1st Qu.:4.000 1st Qu.: 220.0 1st Qu.: 80.0
## Median :4160 Median :5.000 Median : 330.0 Median : 123.0
## Mean :4160 Mean :4.634 Mean : 433.9 Mean : 174.9
## 3rd Qu.:6239 3rd Qu.:5.000 3rd Qu.: 540.0 3rd Qu.: 229.0
## Max. :8319 Max. :6.000 Max. :1999.0 Max. :1745.0
## parqueaderos banios habitaciones longitud
## Min. : 1.000 Min. : 0.000 Min. : 0.000 Min. :-76.59
## 1st Qu.: 1.000 1st Qu.: 2.000 1st Qu.: 3.000 1st Qu.:-76.54
## Median : 2.000 Median : 3.000 Median : 3.000 Median :-76.53
## Mean : 1.867 Mean : 3.111 Mean : 3.605 Mean :-76.53
## 3rd Qu.: 2.000 3rd Qu.: 4.000 3rd Qu.: 4.000 3rd Qu.:-76.52
## Max. :10.000 Max. :10.000 Max. :10.000 Max. :-76.46
## latitud
## Min. :3.333
## 1st Qu.:3.381
## Median :3.416
## Mean :3.418
## 3rd Qu.:3.452
## Max. :3.498
resumen_numericas <- variables_numericas %>%
summarise(
across(
everything(),
list(
minimo = ~ min(.x, na.rm = TRUE),
promedio = ~ mean(.x, na.rm = TRUE),
mediana = ~ median(.x, na.rm = TRUE),
maximo = ~ max(.x, na.rm = TRUE),
desviacion = ~ sd(.x, na.rm = TRUE)
)
)
) %>%
pivot_longer(
cols = everything(),
names_to = "indicador",
values_to = "valor"
)
kable(
resumen_numericas,
digits = 2,
caption = "Resumen estadístico de variables numéricas"
) %>%
kable_styling(full_width = FALSE)
| indicador | valor |
|---|---|
| id_minimo | 1.00 |
| id_promedio | 4160.00 |
| id_mediana | 4160.00 |
| id_maximo | 8319.00 |
| id_desviacion | 2401.20 |
| estrato_minimo | 3.00 |
| estrato_promedio | 4.63 |
| estrato_mediana | 5.00 |
| estrato_maximo | 6.00 |
| estrato_desviacion | 1.03 |
| preciom_minimo | 58.00 |
| preciom_promedio | 433.87 |
| preciom_mediana | 330.00 |
| preciom_maximo | 1999.00 |
| preciom_desviacion | 328.61 |
| areaconst_minimo | 30.00 |
| areaconst_promedio | 174.92 |
| areaconst_mediana | 123.00 |
| areaconst_maximo | 1745.00 |
| areaconst_desviacion | 142.94 |
| parqueaderos_minimo | 1.00 |
| parqueaderos_promedio | 1.87 |
| parqueaderos_mediana | 2.00 |
| parqueaderos_maximo | 10.00 |
| parqueaderos_desviacion | 1.01 |
| banios_minimo | 0.00 |
| banios_promedio | 3.11 |
| banios_mediana | 3.00 |
| banios_maximo | 10.00 |
| banios_desviacion | 1.43 |
| habitaciones_minimo | 0.00 |
| habitaciones_promedio | 3.61 |
| habitaciones_mediana | 3.00 |
| habitaciones_maximo | 10.00 |
| habitaciones_desviacion | 1.46 |
| longitud_minimo | -76.59 |
| longitud_promedio | -76.53 |
| longitud_mediana | -76.53 |
| longitud_maximo | -76.46 |
| longitud_desviacion | 0.02 |
| latitud_minimo | 3.33 |
| latitud_promedio | 3.42 |
| latitud_mediana | 3.42 |
| latitud_maximo | 3.50 |
| latitud_desviacion | 0.04 |
if (ncol(variables_numericas) > 0) {
variables_numericas %>%
pivot_longer(
cols = everything(),
names_to = "variable",
values_to = "valor"
) %>%
ggplot(aes(x = valor)) +
geom_histogram(
bins = 30,
fill = "blue",
color = "gray"
) +
facet_wrap(~ variable, scales = "free") +
labs(
title = "Distribución de variables numéricas",
x = "Valor",
y = "Frecuencia"
) +
theme_minimal()
} else {
print("No se identificaron variables numéricas para graficar.")
}
Interpretación
if (ncol(variables_numericas) > 0) {
variables_numericas %>%
pivot_longer(
cols = everything(),
names_to = "variable",
values_to = "valor"
) %>%
ggplot(aes(x = variable, y = valor)) +
geom_boxplot(fill = "blue", alpha = 0.8) +
facet_wrap(~ variable, scales = "free") +
labs(
title = "Diagramas de caja de variables numéricas",
x = "Variable",
y = "Valor"
) +
theme_minimal() +
theme(axis.text.x = element_blank())
} else {
print("No se identificaron variables numéricas para construir diagramas de caja.")
}
Interpretación
Los anteriores boxplots premiten visualizar lo mencionado en el numeral anterior, es decir, la presencia de valores atipicos en variables como área o previo.
if (ncol(variables_numericas) >= 2) {
matriz_correlacion <- cor(
variables_numericas,
use = "complete.obs"
)
corrplot(
matriz_correlacion,
method = "color",
type = "upper",
tl.cex = 0.8,
addCoef.col = "black",
number.cex = 0.6,
title = "Matriz de correlación entre variables numéricas",
mar = c(0, 0, 2, 0)
)
} else {
print("No hay suficientes variables numéricas para calcular una matriz de correlación.")
}
Interpretación
Lo más relevante de la matriz es que el precio de la vivienda está principalmente relacionado con sus características físicas y socioeconómicas. En particular, el área construida (0.69), número de baños (0.67), parqueaderos (0.62) y estrato (0.61) presentan las correlaciones positivas más importantes con el precio.
Además, las variables área, baños, habitaciones y parqueaderos están relacionadas entre sí, lo que indica que las viviendas de mayor tamaño generalmente tienen mejores características y, en consecuencia, tienden a ser más costosas.
Por otro lado, la correlación extremadamente alta entre id y longitud (-0.96) debe tomarse con precaución, ya que probablemente refleja el orden de los registros y no una relación causal o significativa entre ambas variables.
if (ncol(variables_categoricas) > 0) {
for (var in names(variables_categoricas)) {
cat("\n\nVariable:", var, "\n")
print(table(variables_categoricas[[var]]))
}
} else {
print("No se identificaron variables categóricas en la base.")
}
##
##
## Variable: zona
##
## Zona Centro Zona Norte Zona Oeste Zona Oriente Zona Sur
## 124 1920 1198 351 4729
##
##
## Variable: piso
##
## 01 02 03 04 05 06 07 08 09 10 11 12
## 860 4088 1097 607 567 245 204 211 146 130 84 83
##
##
## Variable: tipo
##
## Apartamento Casa
## 5103 3219
if (ncol(variables_categoricas) > 0) {
variables_categoricas %>%
pivot_longer(
cols = everything(),
names_to = "variable",
values_to = "categoria"
) %>%
ggplot(aes(x = categoria)) +
geom_bar(fill = "lightgreen") +
facet_wrap(~ variable, scales = "free") +
coord_flip() +
labs(
title = "Distribución de variables categóricas",
x = "Categoría",
y = "Frecuencia"
) +
theme_minimal()
} else {
print("No existen variables categóricas para graficar.")
}
Interpretación
Se evidencia que existe una mayor concentración de la oferta inmobiliaria en el piso No. 2, apartamento en la zona sur.
A partir de esta primera revisión se pueden identificar algunos elementos relevantes para el análisis posterior:
El Análisis de Componentes Principales permite reducir la dimensionalidad del conjunto de datos numéricos, conservando la mayor cantidad posible de información. Esta técnica ayuda a identificar las variables que explican la mayor variabilidad de las propiedades residenciales.
Se seleccionan unicamente las variables numéricas y se eliminan variables con varianza 0.
datos_acp <- datos_limpios %>%
select(where(is.numeric))
# Eliminar variables con varianza cero
if (ncol(datos_acp) > 0) {
datos_acp <- datos_acp[, apply(datos_acp, 2, stats::var, na.rm = TRUE) > 0, drop = FALSE]
}
# Validación mínima
if (ncol(datos_acp) < 2) {
stop("No hay suficientes variables numéricas con varianza para ejecutar el ACP.")
}
summary(datos_acp)
## id estrato preciom areaconst
## Min. : 1 Min. :3.000 Min. : 58.0 Min. : 30.0
## 1st Qu.:2081 1st Qu.:4.000 1st Qu.: 220.0 1st Qu.: 80.0
## Median :4160 Median :5.000 Median : 330.0 Median : 123.0
## Mean :4160 Mean :4.634 Mean : 433.9 Mean : 174.9
## 3rd Qu.:6239 3rd Qu.:5.000 3rd Qu.: 540.0 3rd Qu.: 229.0
## Max. :8319 Max. :6.000 Max. :1999.0 Max. :1745.0
## parqueaderos banios habitaciones longitud
## Min. : 1.000 Min. : 0.000 Min. : 0.000 Min. :-76.59
## 1st Qu.: 1.000 1st Qu.: 2.000 1st Qu.: 3.000 1st Qu.:-76.54
## Median : 2.000 Median : 3.000 Median : 3.000 Median :-76.53
## Mean : 1.867 Mean : 3.111 Mean : 3.605 Mean :-76.53
## 3rd Qu.: 2.000 3rd Qu.: 4.000 3rd Qu.: 4.000 3rd Qu.:-76.52
## Max. :10.000 Max. :10.000 Max. :10.000 Max. :-76.46
## latitud
## Min. :3.333
## 1st Qu.:3.381
## Median :3.416
## Mean :3.418
## 3rd Qu.:3.452
## Max. :3.498
Con el ACP se reduce la dimensionalidad de la base, identificando combinaciones de variables que explican las principales diferencias entre las variables analizadas.
acp <- PCA(
datos_acp,
scale.unit = TRUE,
graph = FALSE
)
summary(acp)
##
## Call:
## PCA(X = datos_acp, scale.unit = TRUE, graph = FALSE)
##
##
## Eigenvalues
## Dim.1 Dim.2 Dim.3 Dim.4 Dim.5
## Variance 3.820 1.887 0.972 0.901 0.580
## % of var. 42.442 20.962 10.797 10.013 6.440
## Cumulative % of var. 42.442 63.404 74.201 84.214 90.655
##
## Individuals (the 10 first)
## Dist Dim.1 ctr cos2 Dim.2 ctr cos2 Dim.3
## 1 | 3.077 | -1.771 0.010 0.331 | 1.835 0.021 0.355 | 1.175
## 2 | 2.620 | -2.268 0.016 0.749 | 0.871 0.005 0.111 | 0.034
## 3 | 2.347 | -1.403 0.006 0.357 | 1.469 0.014 0.392 | 0.198
## 4 | 2.298 | 1.250 0.005 0.296 | 0.123 0.000 0.003 | 0.526
## 5 | 2.339 | -1.824 0.010 0.608 | 0.400 0.001 0.029 | -0.486
## 6 | 2.122 | -1.173 0.004 0.306 | -0.121 0.000 0.003 | -1.009
## 7 | 1.795 | -1.616 0.008 0.810 | 0.145 0.000 0.007 | -0.352
## 8 | 1.061 | 0.101 0.000 0.009 | -0.387 0.001 0.133 | -0.180
## 9 | 2.787 | -0.284 0.000 0.010 | 2.090 0.028 0.562 | 0.534
## 10 | 2.786 | 0.298 0.000 0.011 | 1.525 0.015 0.300 | -0.656
## ctr cos2
## 1 0.017 0.146 |
## 2 0.000 0.000 |
## 3 0.000 0.007 |
## 4 0.003 0.052 |
## 5 0.003 0.043 |
## 6 0.013 0.226 |
## 7 0.002 0.038 |
## 8 0.000 0.029 |
## 9 0.004 0.037 |
## 10 0.005 0.055 |
##
## Variables
## Dim.1 ctr cos2 Dim.2 ctr cos2 Dim.3 ctr
## id | 0.621 10.087 0.385 | -0.668 23.635 0.446 | 0.367 13.843
## estrato | 0.659 11.366 0.434 | -0.340 6.139 0.116 | -0.404 16.767
## preciom | 0.866 19.639 0.750 | 0.124 0.818 0.015 | -0.237 5.766
## areaconst | 0.747 14.591 0.557 | 0.437 10.116 0.191 | 0.047 0.231
## parqueaderos | 0.664 11.537 0.441 | 0.253 3.394 0.064 | -0.309 9.843
## banios | 0.803 16.870 0.644 | 0.342 6.210 0.117 | 0.083 0.711
## habitaciones | 0.430 4.851 0.185 | 0.598 18.926 0.357 | 0.510 26.809
## longitud | -0.600 9.410 0.359 | 0.694 25.551 0.482 | -0.359 13.269
## latitud | -0.251 1.650 0.063 | 0.314 5.210 0.098 | 0.352 12.760
## cos2
## id 0.135 |
## estrato 0.163 |
## preciom 0.056 |
## areaconst 0.002 |
## parqueaderos 0.096 |
## banios 0.007 |
## habitaciones 0.261 |
## longitud 0.129 |
## latitud 0.124 |
fviz_eig(
acp,
addlabels = TRUE,
ylim = c(0, 60),
barfill = "#2E86AB",
barcolor = "#2E86AB"
) +
labs(
title = "Porcentaje de varianza explicada por componente principal",
x = "Componentes principales",
y = "Porcentaje de varianza explicada"
) +
theme_minimal()
Interpretación
Se evidencia cuánto aporta cada componente al principal, para el caso los dos primeros componentes explican un % importante de la varianza.
varianza <- as.data.frame(acp$eig)
varianza <- varianza %>%
rownames_to_column("componente") %>%
rename(
eigenvalor = eigenvalue,
porcentaje_varianza = `percentage of variance`,
porcentaje_acumulado = `cumulative percentage of variance`
)
kable(
varianza,
digits = 2,
caption = "Varianza explicada por cada componente"
) %>%
kable_styling(full_width = FALSE)
| componente | eigenvalor | porcentaje_varianza | porcentaje_acumulado |
|---|---|---|---|
| comp 1 | 3.82 | 42.44 | 42.44 |
| comp 2 | 1.89 | 20.96 | 63.40 |
| comp 3 | 0.97 | 10.80 | 74.20 |
| comp 4 | 0.90 | 10.01 | 84.21 |
| comp 5 | 0.58 | 6.44 | 90.65 |
fviz_pca_var(
acp,
col.var = "contrib",
gradient.cols = c("#00AFBB", "#E7B800", "#FC4E07"),
repel = TRUE
) +
labs(
title = "Mapa de variables en el ACP",
subtitle = "Color según contribución a los componentes principales"
) +
theme_minimal()
Interpretración
En términos sencillos, este ACP muestra que la principal fuente de variabilidad de los datos está relacionada con las características físicas y económicas de las viviendas. El primer componente está fuertemente asociado con precio, área construida, baños, habitaciones y parqueaderos, mientras que el segundo componente recoge diferencias adicionales relacionadas principalmente con estrato y ubicación geográfica.
Además, el gráfico confirma los resultados obtenidos anteriormente mediante la matriz de correlación: las viviendas más grandes tienden a tener más habitaciones, baños y parqueaderos y, generalmente, un precio más elevado.
fviz_contrib(
acp,
choice = "var",
axes = 1,
top = min(10, ncol(datos_acp)),
fill = "#2E86AB",
color = "#2E86AB"
) +
labs(
title = "Variables con mayor contribución al componente principal 1"
) +
theme_minimal()
Interpretación
Las variables que explican mas el primer componente 1, corresponden a precio, área y baños.
fviz_contrib(
acp,
choice = "var",
axes = 2,
top = min(10, ncol(datos_acp)),
fill = "#F18F01",
color = "#F18F01"
) +
labs(
title = "Variables con mayor contribución al componente principal 2"
) +
theme_minimal()
Interpretación
Las variables que aportan mas al componente 2 corresponden a longitud, id, y habotaciones.
fviz_pca_ind(
acp,
geom.ind = "point",
col.ind = "cos2",
gradient.cols = c("#00AFBB", "#E7B800", "#FC4E07"),
repel = TRUE
) +
labs(
title = "Representación de propiedades en el plano factorial",
subtitle = "Color según calidad de representación"
) +
theme_minimal()
Interpretación
La distribución de las propiedades permite evidenciar agrupaciones naturales y posibles casos atipicos, insumo para el análisis de conglomerados.
Los primeros componentes principales concentran la mayor parte de la variabilidad de las variables numéricas. Si los dos o tres primeros componentes explican un porcentaje importante de la variabilidad acumulada, es posible representar la estructura del mercado inmobiliario en un plano reducido sin perder demasiada información.
La interpretación debe centrarse en las variables con mayor contribución:
El análisis de conglomerados permite agrupar las propiedades en segmentos homogéneos. Estos segmentos ayudan a interpretar diferentes perfiles de vivienda y pueden ser utilizados para decisiones comerciales, inversión, valoración y estrategia territorial.
Se utilizan variables numéricas estandarizadas.
datos_cluster <- scale(datos_acp)
sum(is.na(datos_cluster))
## [1] 0
fviz_nbclust(
datos_cluster,
kmeans,
method = "wss"
) +
labs(
title = "Método del codo para determinar número óptimo de clusters",
x = "Número de clusters",
y = "Suma de cuadrados intra-cluster"
) +
theme_minimal()
Interpretación
Este gráfico muestra una reducción de hasta 3 clusters y luego la curva se aplana, es decir que disminuye la variabilidad a medida que aumenta el numero de cluster.
fviz_nbclust(
datos_cluster,
kmeans,
method = "silhouette"
) +
labs(
title = "Método de la silueta para determinar número óptimo de clusters",
x = "Número de clusters",
y = "Ancho promedio de silueta"
) +
theme_minimal()
Interpretación
Este gráfico muestra que tan bien separados están los cluster
set.seed(123)
sil_width <- map_dbl(2:8, function(k) {
km <- kmeans(datos_cluster, centers = k, nstart = 25)
ss <- silhouette(km$cluster, dist(datos_cluster))
mean(ss[, 3])
})
k_silueta <- tibble(
k = 2:8,
silueta_promedio = sil_width
)
kable(
k_silueta,
digits = 3,
caption = "Ancho promedio de silueta por número de clusters"
) %>%
kable_styling(full_width = FALSE)
| k | silueta_promedio |
|---|---|
| 2 | 0.283 |
| 3 | 0.218 |
| 4 | 0.233 |
| 5 | 0.211 |
| 6 | 0.210 |
| 7 | 0.218 |
| 8 | 0.232 |
k_optimo <- k_silueta$k[which.max(k_silueta$silueta_promedio)]
k_optimo
## [1] 2
Interpretación
La tabla permite comparar objetivamente las alternativas de segmentación y seleccionar los conglomerados que ofrecen mayor calidad estasdistica, para el caso el numéro sugerido de cluster es 2.
Se ejecuta el modelo k-mens con los 2 cluster óptimos, lo que permite segmentar el mercado en grupos homogéneos de propiedades.
set.seed(123)
modelo_kmeans <- kmeans(
datos_cluster,
centers = k_optimo,
nstart = 25
)
datos_segmentados <- datos_limpios %>%
mutate(cluster = as.factor(modelo_kmeans$cluster))
table(datos_segmentados$cluster)
##
## 1 2
## 2975 5347
fviz_cluster(
modelo_kmeans,
data = datos_cluster,
geom = "point",
ellipse.type = "convex",
palette = "jco",
ggtheme = theme_minimal()
) +
labs(
title = "Segmentación de propiedades mediante K-means",
subtitle = "Clusters proyectados en el plano factorial"
)
Interpretación
Se evidencia la existencia de grupos de vivienda con caracteristicas diferenciales, lo que permite orientar estrategias comerciales y de inversión de segmento.
perfil_numerico <- datos_segmentados %>%
group_by(cluster) %>%
summarise(
across(
where(is.numeric),
list(
promedio = ~ mean(.x, na.rm = TRUE),
mediana = ~ median(.x, na.rm = TRUE)
)
),
cantidad = n(),
.groups = "drop"
)
kable(
perfil_numerico,
digits = 2,
caption = "Perfil numérico de los clusters"
) %>%
kable_styling(full_width = FALSE)
| cluster | id_promedio | id_mediana | estrato_promedio | estrato_mediana | preciom_promedio | preciom_mediana | areaconst_promedio | areaconst_mediana | parqueaderos_promedio | parqueaderos_mediana | banios_promedio | banios_mediana | habitaciones_promedio | habitaciones_mediana | longitud_promedio | longitud_mediana | latitud_promedio | latitud_mediana | cantidad |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 5779.55 | 6025 | 5.41 | 6 | 747.62 | 650 | 288.61 | 250 | 2.59 | 2 | 4.42 | 4 | 4.31 | 4 | -76.54 | -76.54 | 3.41 | 3.42 | 2975 |
| 2 | 3258.90 | 2852 | 4.20 | 4 | 259.30 | 250 | 111.66 | 90 | 1.47 | 1 | 2.38 | 2 | 3.21 | 3 | -76.52 | -76.52 | 3.42 | 3.41 | 5347 |
El perfil numérico permite asignar una interpretación de negocio de cada cluster, identificando caracteristicas en términos de precio, área, etc.
variables_categoricas_segmentadas <- datos_segmentados %>%
select(where(is.factor)) %>%
select(-cluster)
if (ncol(variables_categoricas_segmentadas) > 0) {
for (var in names(variables_categoricas_segmentadas)) {
cat("\n\nVariable categórica:", var, "\n")
tabla <- datos_segmentados %>%
tabyl(cluster, !!sym(var)) %>%
adorn_percentages("row") %>%
adorn_pct_formatting(digits = 1)
print(tabla)
}
} else {
print("No existen variables categóricas adicionales para perfilar los clusters.")
}
##
##
## Variable categórica: zona
## cluster Zona Centro Zona Norte Zona Oeste Zona Oriente Zona Sur
## 1 0.7% 13.6% 29.3% 0.8% 55.5%
## 2 1.9% 28.3% 6.1% 6.1% 57.5%
##
##
## Variable categórica: piso
## cluster 01 02 03 04 05 06 07 08 09 10 11 12
## 1 9.3% 57.9% 12.8% 5.1% 3.2% 2.9% 2.2% 1.9% 1.7% 1.3% 1.1% 0.7%
## 2 10.9% 44.2% 13.4% 8.5% 8.8% 3.0% 2.6% 2.9% 1.8% 1.7% 1.0% 1.2%
##
##
## Variable categórica: tipo
## cluster Apartamento Casa
## 1 43.3% 56.7%
## 2 71.4% 28.6%
Se identifica lo siguiente en función de los resultados observados:
Esta segmentación permite diseñar estrategias diferenciadas por tipo de propiedad, precio esperado, ubicación, perfil de comprador y objetivo comercial.
El Análisis de Correspondencias permite examinar asociaciones entre variables categóricas. En este caso, se utiliza para identificar patrones entre variables como tipo de vivienda, zona, barrio, estrato u otras variables cualitativas disponibles.
se seleccionan las variables categóricas con un numero razonable de categórias; para el caso, estas corresponde a “zona”, “piso” y “tipo”.
cat_validas <- datos_limpios %>%
select(where(is.factor)) %>%
select(where(~ n_distinct(.) >= 2 & n_distinct(.) <= 25))
names(cat_validas)
## [1] "zona" "piso" "tipo"
if (ncol(cat_validas) >= 2) {
pares <- combn(names(cat_validas), 2, simplify = FALSE)
pares_mostrar <- pares[1:min(length(pares), 5)]
for (par in pares_mostrar) {
cat("\n\nAnalizando correspondencias entre:", par[1], "y", par[2], "\n")
tabla <- table(
datos_limpios[[par[1]]],
datos_limpios[[par[2]]]
)
# Eliminar filas y columnas con frecuencia cero
tabla <- tabla[rowSums(tabla) > 0, colSums(tabla) > 0]
# Validar que la tabla tenga al menos 2 filas y 2 columnas
if (all(dim(tabla) > 1)) {
tryCatch({
ac <- CA(tabla, graph = FALSE)
# Validar que el análisis tenga al menos dos dimensiones para graficar
if (
!is.null(ac$row$coord) &&
!is.null(ac$col$coord) &&
ncol(as.matrix(ac$row$coord)) >= 2 &&
ncol(as.matrix(ac$col$coord)) >= 2
) {
print(
fviz_ca_biplot(
ac,
axes = c(1, 2),
repel = TRUE
) +
labs(
title = paste("Análisis de Correspondencias:", par[1], "vs", par[2]),
x = "Dimensión 1",
y = "Dimensión 2"
) +
theme_minimal()
)
} else {
cat(
"No se genera gráfico para", par[1], "vs", par[2],
"porque el análisis tiene menos de dos dimensiones.\n"
)
print(ac$eig)
}
}, error = function(e) {
cat(
"No fue posible ejecutar el análisis para", par[1], "vs", par[2], "\n"
)
cat("Detalle del error:", e$message, "\n")
})
} else {
cat(
"No se analiza", par[1], "vs", par[2],
"porque la tabla no tiene al menos 2 filas y 2 columnas válidas.\n"
)
}
}
} else {
print("No existen suficientes variables categóricas válidas para el análisis de correspondencias.")
}
##
##
## Analizando correspondencias entre: zona y piso
##
##
## Analizando correspondencias entre: zona y tipo
## No se genera gráfico para zona vs tipo porque el análisis tiene menos de dos dimensiones.
## eigenvalue percentage of variance cumulative percentage of variance
## dim 1 0.08300733 100 100
##
##
## Analizando correspondencias entre: piso y tipo
## No se genera gráfico para piso vs tipo porque el análisis tiene menos de dos dimensiones.
## eigenvalue percentage of variance cumulative percentage of variance
## dim 1 0.2036611 100 100
Interpretación
Este análisis permite observar si ciertas categórias tienden a relacionarse ente sí.
En este caso se intenta identificar nombres probables.
nombres_base <- names(datos_limpios)
variable_tipo <- nombres_base[str_detect(nombres_base, "tipo")][1]
variable_zona <- nombres_base[str_detect(nombres_base, "zona|sector|localidad")][1]
variable_tipo
## [1] "tipo"
variable_zona
## [1] "zona"
if (!is.na(variable_tipo) & !is.na(variable_zona)) {
tabla_tipo_zona <- table(
datos_limpios[[variable_tipo]],
datos_limpios[[variable_zona]]
)
# Eliminar filas y columnas sin frecuencia
tabla_tipo_zona <- tabla_tipo_zona[
rowSums(tabla_tipo_zona) > 0,
colSums(tabla_tipo_zona) > 0
]
# Mostrar tabla de contingencia
print(tabla_tipo_zona)
if (all(dim(tabla_tipo_zona) > 1)) {
tryCatch({
ac_tipo_zona <- CA(tabla_tipo_zona, graph = FALSE)
# Mostrar eigenvalores para evaluar número de dimensiones disponibles
print(ac_tipo_zona$eig)
n_dim_filas <- ncol(as.matrix(ac_tipo_zona$row$coord))
n_dim_columnas <- ncol(as.matrix(ac_tipo_zona$col$coord))
n_dim <- min(n_dim_filas, n_dim_columnas)
if (n_dim >= 2) {
# Caso 1: existen dos dimensiones, se grafica biplot tradicional
print(
fviz_ca_biplot(
ac_tipo_zona,
axes = c(1, 2),
repel = TRUE
) +
labs(
title = "Análisis de Correspondencias: Tipo de vivienda vs Zona",
x = "Dimensión 1",
y = "Dimensión 2"
) +
theme_minimal()
)
} else if (n_dim == 1) {
# Caso 2: solo existe una dimensión, se grafica representación unidimensional
coord_filas <- as.data.frame(ac_tipo_zona$row$coord)
coord_columnas <- as.data.frame(ac_tipo_zona$col$coord)
coord_filas$categoria <- rownames(coord_filas)
coord_columnas$categoria <- rownames(coord_columnas)
names(coord_filas)[1] <- "Dim1"
names(coord_columnas)[1] <- "Dim1"
coord_filas$variable <- variable_tipo
coord_columnas$variable <- variable_zona
coords_1d <- bind_rows(
coord_filas %>% select(categoria, Dim1, variable),
coord_columnas %>% select(categoria, Dim1, variable)
)
print(
ggplot(coords_1d, aes(x = Dim1, y = variable, color = variable, label = categoria)) +
geom_point(size = 4) +
geom_text_repel(size = 3.5, show.legend = FALSE) +
geom_vline(xintercept = 0, linetype = "dashed", color = "gray50") +
labs(
title = "Análisis de Correspondencias Unidimensional: Tipo de vivienda vs Zona",
subtitle = "El cruce solo genera una dimensión factorial; por eso se presenta en un eje",
x = "Dimensión 1",
y = "Variable"
) +
theme_minimal()
)
} else {
print("El análisis no generó dimensiones suficientes para graficar.")
}
# Gráfico alternativo: mapa de calor de frecuencias relativas
# Este gráfico siempre es útil para interpretar la distribución del tipo de vivienda por zona.
tabla_prop <- prop.table(tabla_tipo_zona, margin = 1) * 100
tabla_prop_df <- as.data.frame(tabla_prop)
names(tabla_prop_df) <- c("tipo_vivienda", "zona", "porcentaje")
print(
ggplot(tabla_prop_df, aes(x = zona, y = tipo_vivienda, fill = porcentaje)) +
geom_tile(color = "white") +
geom_text(aes(label = paste0(round(porcentaje, 1), "%")), size = 3) +
scale_fill_gradient(low = "#E8F4F8", high = "#2E86AB") +
labs(
title = "Distribución porcentual de tipo de vivienda por zona",
subtitle = "Porcentaje calculado dentro de cada tipo de vivienda",
x = "Zona",
y = "Tipo de vivienda",
fill = "%"
) +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
)
}, error = function(e) {
print(paste("Error en el análisis tipo vs zona:", e$message))
})
} else {
print("La tabla tipo vs zona no tiene suficientes filas y columnas válidas.")
}
} else {
print("No se encontraron automáticamente las variables tipo y zona. Ajuste manualmente los nombres.")
}
##
## Zona Centro Zona Norte Zona Oeste Zona Oriente Zona Sur
## Apartamento 24 1198 1029 62 2790
## Casa 100 722 169 289 1939
## eigenvalue percentage of variance cumulative percentage of variance
## dim 1 0.08300733 100 100
Interpretación
El análisis de correspondencia mostró que la relación entre tipo de vivienda y zona se representa principalmente en una dimensión. Por esta razón, se utilizó una visualización unidimensional que permite identificar la posición de cada categória sobre el eje principal de diferenciación. Por ejemplo se observa una mayor participación en la zona sur de tipo casa y apartamento.
En los mapas de correspondencia:
Este análisis es útil para identificar patrones de localización, concentración de oferta y perfiles inmobiliarios por sector de la ciudad.
variable_precio <- names(datos_segmentados)[str_detect(names(datos_segmentados), "precio|valor|venta")][1]
variable_area <- names(datos_segmentados)[str_detect(names(datos_segmentados), "area|área|metros|m2")][1]
variable_zona <- names(datos_segmentados)[str_detect(names(datos_segmentados), "zona|sector|localidad")][1]
variable_estrato <- names(datos_segmentados)[str_detect(names(datos_segmentados), "estrato")][1]
variable_precio
## [1] "preciom"
variable_area
## [1] "areaconst"
variable_zona
## [1] "zona"
variable_estrato
## [1] "estrato"
if (!is.na(variable_precio)) {
ggplot(datos_segmentados, aes(x = cluster, y = .data[[variable_precio]], fill = cluster)) +
geom_boxplot(alpha = 0.8) +
labs(
title = "Distribución del precio por segmento de vivienda",
x = "Cluster",
y = "Precio"
) +
theme_minimal() +
theme(legend.position = "none")
} else {
print("No se identificó automáticamente una variable de precio. Ajuste variable_precio manualmente.")
}
Interpretación
El gráfico permite comparar el comportamiento económico de cada segmento, para el cluster 1 se puede identificar un segmento premium por tener precios mas altos. Diferente al cluster 2 que puede representar vivienda mas económica.
if (!is.na(variable_area)) {
ggplot(datos_segmentados, aes(x = cluster, y = .data[[variable_area]], fill = cluster)) +
geom_boxplot(alpha = 0.8) +
labs(
title = "Distribución del área por segmento de vivienda",
x = "Cluster",
y = "Área"
) +
theme_minimal() +
theme(legend.position = "none")
} else {
print("No se identificó automáticamente una variable de área. Ajuste variable_area manualmente.")
}
Interpretación
El gráfico muestra que a mayor área en el cluster 1 pueden ser viviendas familiares o premium, y el cluster 2 pueden representar apartamentos pequeños o viviendas de menor costo
if (!is.na(variable_precio) & !is.na(variable_area)) {
ggplot(
datos_segmentados,
aes(
x = .data[[variable_area]],
y = .data[[variable_precio]],
color = cluster
)
) +
geom_point(alpha = 0.7) +
geom_smooth(method = "lm", se = FALSE) +
labs(
title = "Relación entre precio y área por segmento",
x = "Área",
y = "Precio",
color = "Cluster"
) +
theme_minimal()
} else {
print("No se identificaron automáticamente precio y área. Ajuste los nombres manualmente.")
}
Interpretación
La relación entre previo y área confirma que el tamaño de la vivienda es un facor relevante en el precio del inmueble.
if (!is.na(variable_zona)) {
ggplot(datos_segmentados, aes(x = .data[[variable_zona]], fill = cluster)) +
geom_bar(position = "dodge") +
coord_flip() +
labs(
title = "Distribución de la oferta por zona y cluster",
x = "Zona",
y = "Número de propiedades",
fill = "Cluster"
) +
theme_minimal()
} else {
print("No se identificó automáticamente una variable de zona. Ajuste variable_zona manualmente.")
}
Interpretación
Se identifica que las zonas con mayor concentración de ofertas corresponde a la zona sur y Norte para el cluster 2
if (!is.na(variable_precio) & !is.na(variable_zona)) {
datos_segmentados %>%
group_by(.data[[variable_zona]]) %>%
summarise(
precio_promedio = mean(.data[[variable_precio]], na.rm = TRUE),
cantidad = n(),
.groups = "drop"
) %>%
arrange(desc(precio_promedio)) %>%
ggplot(aes(x = reorder(.data[[variable_zona]], precio_promedio), y = precio_promedio)) +
geom_col(fill = "#2E86AB") +
coord_flip() +
labs(
title = "Precio promedio por zona",
x = "Zona",
y = "Precio promedio"
) +
theme_minimal()
} else {
print("No se identificaron automáticamente precio y zona. Ajuste los nombres manualmente.")
}
Interpretación
El precio promedio por zona permite identificar diferencias en la valorización de los inmuebles, lo cual implica decisiones de inversión, compara y venta.
A partir del análisis realizado, se plantean las siguientes conclusiones generales:
El mercado inmobiliario presenta estructuras diferenciadas según características físicas, económicas y de ubicación: El ACP permite identificar las variables que explican la mayor variabilidad de la oferta inmobiliaria, tales como precio, área, número de habitaciones, número de baños, estrato o ubicación, dependiendo de las variables disponibles en la base.
La segmentación mediante conglomerados permite clasificar las viviendas en grupos homogéneos: Estos grupos pueden interpretarse como segmentos de mercado, por ejemplo vivienda económica, vivienda intermedia y vivienda premium; esta clasificación facilita decisiones comerciales más focalizadas.
Las variables categóricas muestran patrones de asociación relevantes: El análisis de correspondencias permite identificar si ciertos tipos de vivienda se concentran en determinadas zonas, ayudando a entender la dinámica territorial de la oferta.
La visualización de resultados fortalece la toma de decisiones: Los gráficos de componentes principales, clusters, distribuciones de precio y análisis por zona permiten comunicar los hallazgos de forma clara a la dirección de la empresa.
El análisis ofrece una base para decisiones estratégicas: La combinación de ACP, clustering y correspondencias permite comprender el mercado desde varias dimensiones: atributos físicos, segmentación económica y comportamiento territorial.
Con base en los resultados del análisis, se proponen las siguientes recomendaciones:
Diseñar estrategias comerciales diferenciadas por segmento: Cada cluster debe abordarse con una estrategia distinta de precio, promoción, canal comercial y perfil de cliente objetivo.
Priorizar zonas con alta concentración de oferta y mejor relación precio-características: Las zonas con propiedades bien posicionadas en términos de área, precio y atributos pueden representar oportunidades de inversión o comercialización.
Utilizar el ACP como herramienta de monitoreo del mercado: La empresa puede actualizar periódicamente el análisis para identificar cambios en los factores que explican el comportamiento del mercado inmobiliario.
Incorporar el análisis de correspondencias en la estrategia territorial: Las asociaciones entre tipo de vivienda, zona y barrio permiten detectar nichos de mercado y oportunidades específicas por ubicación.
Profundizar el análisis predictivo de precios: Como siguiente etapa, se recomienda construir modelos de regresión, árboles de decisión o modelos de machine learning para estimar el precio esperado de una vivienda según sus características.
Crear tableros ejecutivos de seguimiento: Se recomienda implementar visualizaciones en Power BI, Tableau o Shiny para monitorear indicadores como precio promedio, oferta por zona, comportamiento por cluster y evolución de segmentos.
Validar la calidad de datos de forma periódica:La empresa debe establecer controles sobre completitud, consistencia y actualización de la información inmobiliaria para mejorar la confiabilidad de los análisis.