La dinámica del mercado inmobiliario en las ciudades se ha vuelto cada vez más compleja debido a la multiplicidad de factores que influyen en la oferta y la demanda de vivienda. La ciudad de Cali, no es ajena a esta realidad. Entender las características del mercado inmobiliario de esta ciudad es esencial para poder ofrecer una oferta adecuada y alineada con las necesidades y preferencias de los potenciales compradores.
En este contexto, el presente trabajo tiene como objetivo realizar una evaluación detallada de la oferta inmobiliaria urbana en Cali utilizando modelos multivariados. Se analizaron diversos factores como la zona, el número de pisos, la cantidad de baños, el precio, el área, el número de parqueaderos y habitaciones, entre otros, para identificar patrones y segmentar el mercado en clústeres que permiten una mejor comprensión de las tendencias actuales.
Para lograr estos objetivos, se emplearon enfoques estadísticos avanzados y de machine learning, incluyendo técnicas como el análisis de componentes principales (PCA), el clustering y el análisis de correspondencia. Este último fue fundamental para explorar y visualizar las asociaciones entre las diferentes variables categóricas, ofreciendo una perspectiva más profunda sobre cómo interactúan ciertos atributos de las propiedades. Estos métodos permitieron no solo reducir la dimensionalidad de los datos, sino también agrupar las viviendas en categorías similares, facilitando la identificación de patrones en la oferta inmobiliaria.
Antes de llevar a cabo los análisis mencionados, es fundamental realizar un preprocesamiento de los datos para asegurar que la base esté preparada para los distintos procesos. Este paso inicial incluye una exploración exhaustiva de la base de datos con el objetivo de conocer las variables presentes y determinar la mejor manera de alistarlas para los análisis posteriores.
## spc_tbl_ [8,322 × 13] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
## $ id : num [1:8322] 1147 1169 1350 5992 1212 ...
## $ zona : chr [1:8322] "Zona Oriente" "Zona Oriente" "Zona Oriente" "Zona Sur" ...
## $ piso : chr [1:8322] NA NA NA "02" ...
## $ estrato : num [1:8322] 3 3 3 4 5 5 4 5 5 5 ...
## $ preciom : num [1:8322] 250 320 350 400 260 240 220 310 320 780 ...
## $ areaconst : num [1:8322] 70 120 220 280 90 87 52 137 150 380 ...
## $ parqueaderos: num [1:8322] 1 1 2 3 1 1 2 2 2 2 ...
## $ banios : num [1:8322] 3 2 2 5 2 3 2 3 4 3 ...
## $ habitaciones: num [1:8322] 6 3 4 3 3 3 3 4 6 3 ...
## $ tipo : chr [1:8322] "Casa" "Casa" "Casa" "Casa" ...
## $ barrio : chr [1:8322] "20 de julio" "20 de julio" "20 de julio" "3 de julio" ...
## $ longitud : num [1:8322] -76.5 -76.5 -76.5 -76.5 -76.5 ...
## $ latitud : num [1:8322] 3.43 3.43 3.44 3.44 3.46 ...
## - attr(*, "spec")=
## .. cols(
## .. id = col_double(),
## .. zona = col_character(),
## .. piso = col_character(),
## .. estrato = col_double(),
## .. preciom = col_double(),
## .. areaconst = col_double(),
## .. parqueaderos = col_double(),
## .. banios = col_double(),
## .. habitaciones = col_double(),
## .. tipo = col_character(),
## .. barrio = col_character(),
## .. longitud = col_double(),
## .. latitud = col_double()
## .. )
## - attr(*, "problems")=<externalptr>
## # A tibble: 6 × 13
## id zona piso estrato preciom areaconst parqueaderos banios habitaciones
## <dbl> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 1147 Zona O… <NA> 3 250 70 1 3 6
## 2 1169 Zona O… <NA> 3 320 120 1 2 3
## 3 1350 Zona O… <NA> 3 350 220 2 2 4
## 4 5992 Zona S… 02 4 400 280 3 5 3
## 5 1212 Zona N… 01 5 260 90 1 2 3
## 6 1724 Zona N… 01 5 240 87 1 3 3
## # ℹ 4 more variables: tipo <chr>, barrio <chr>, longitud <dbl>, latitud <dbl>
## id zona piso estrato
## Min. : 1 Length:8322 Length:8322 Min. :3.000
## 1st Qu.:2080 Class :character Class :character 1st Qu.:4.000
## Median :4160 Mode :character Mode :character Median :5.000
## Mean :4160 Mean :4.634
## 3rd Qu.:6240 3rd Qu.:5.000
## Max. :8319 Max. :6.000
## NA's :3 NA's :3
## preciom areaconst parqueaderos banios
## Min. : 58.0 Min. : 30.0 Min. : 1.000 Min. : 0.000
## 1st Qu.: 220.0 1st Qu.: 80.0 1st Qu.: 1.000 1st Qu.: 2.000
## Median : 330.0 Median : 123.0 Median : 2.000 Median : 3.000
## Mean : 433.9 Mean : 174.9 Mean : 1.835 Mean : 3.111
## 3rd Qu.: 540.0 3rd Qu.: 229.0 3rd Qu.: 2.000 3rd Qu.: 4.000
## Max. :1999.0 Max. :1745.0 Max. :10.000 Max. :10.000
## NA's :2 NA's :3 NA's :1605 NA's :3
## habitaciones tipo barrio longitud
## Min. : 0.000 Length:8322 Length:8322 Min. :-76.59
## 1st Qu.: 3.000 Class :character Class :character 1st Qu.:-76.54
## Median : 3.000 Mode :character Mode :character Median :-76.53
## Mean : 3.605 Mean :-76.53
## 3rd Qu.: 4.000 3rd Qu.:-76.52
## Max. :10.000 Max. :-76.46
## NA's :3 NA's :3
## latitud
## Min. :3.333
## 1st Qu.:3.381
## Median :3.416
## Mean :3.418
## 3rd Qu.:3.452
## Max. :3.498
## NA's :3
Después de la exploración inicial, se procede a preparar la base de datos para los análisis. El primer paso en este proceso es definir las variables categóricas como datos de tipo factor.
vivienda$tipo =tolower(vivienda$tipo)
vivienda$barrio =tolower(vivienda$barrio)
vivienda$piso <- as.numeric(vivienda$piso)
vivienda$zona <- as.factor(vivienda$zona)
vivienda$estrato <- as.factor(vivienda$estrato)
vivienda$tipo <- as.factor(vivienda$tipo)
vivienda$barrio <- as.factor(vivienda$barrio)
summary(vivienda)## id zona piso estrato
## Min. : 1 Zona Centro : 124 Min. : 1.000 3 :1453
## 1st Qu.:2080 Zona Norte :1920 1st Qu.: 2.000 4 :2129
## Median :4160 Zona Oeste :1198 Median : 3.000 5 :2750
## Mean :4160 Zona Oriente: 351 Mean : 3.771 6 :1987
## 3rd Qu.:6240 Zona Sur :4726 3rd Qu.: 5.000 NA's: 3
## Max. :8319 NA's : 3 Max. :12.000
## NA's :3 NA's :2638
## preciom areaconst parqueaderos banios
## Min. : 58.0 Min. : 30.0 Min. : 1.000 Min. : 0.000
## 1st Qu.: 220.0 1st Qu.: 80.0 1st Qu.: 1.000 1st Qu.: 2.000
## Median : 330.0 Median : 123.0 Median : 2.000 Median : 3.000
## Mean : 433.9 Mean : 174.9 Mean : 1.835 Mean : 3.111
## 3rd Qu.: 540.0 3rd Qu.: 229.0 3rd Qu.: 2.000 3rd Qu.: 4.000
## Max. :1999.0 Max. :1745.0 Max. :10.000 Max. :10.000
## NA's :2 NA's :3 NA's :1605 NA's :3
## habitaciones tipo barrio longitud
## Min. : 0.000 apartamento:5100 valle del lili:1009 Min. :-76.59
## 1st Qu.: 3.000 casa :3219 ciudad jardín : 518 1st Qu.:-76.54
## Median : 3.000 NA's : 3 pance : 412 Median :-76.53
## Mean : 3.605 la flora : 368 Mean :-76.53
## 3rd Qu.: 4.000 santa teresita: 263 3rd Qu.:-76.52
## Max. :10.000 (Other) :5749 Max. :-76.46
## NA's :3 NA's : 3 NA's :3
## latitud
## Min. :3.333
## 1st Qu.:3.381
## Median :3.416
## Mean :3.418
## 3rd Qu.:3.452
## Max. :3.498
## NA's :3
Posteriormente, se eliminan las filas que contienen principalmente datos nulos.
nulos_por_fila <- apply(vivienda, 1, function(x) sum(is.na(x)))
nulos_df <- data.frame(fila = 1:nrow(vivienda), nulos = nulos_por_fila)
nulos_df <- nulos_df[order(-nulos_df$nulos),]
top_5_nulos <- head(nulos_df, 5)
print(top_5_nulos)## fila nulos
## 8320 8320 13
## 8321 8321 13
## 8322 8322 12
## 28 28 2
## 29 29 2
## id zona piso estrato preciom
## Min. : 1 Zona Centro : 124 Min. : 1.000 3:1453 Min. : 58.0
## 1st Qu.:2080 Zona Norte :1920 1st Qu.: 2.000 4:2129 1st Qu.: 220.0
## Median :4160 Zona Oeste :1198 Median : 3.000 5:2750 Median : 330.0
## Mean :4160 Zona Oriente: 351 Mean : 3.771 6:1987 Mean : 433.9
## 3rd Qu.:6240 Zona Sur :4726 3rd Qu.: 5.000 3rd Qu.: 540.0
## Max. :8319 Max. :12.000 Max. :1999.0
## NA's :2635
## areaconst parqueaderos banios habitaciones
## Min. : 30.0 Min. : 1.000 Min. : 0.000 Min. : 0.000
## 1st Qu.: 80.0 1st Qu.: 1.000 1st Qu.: 2.000 1st Qu.: 3.000
## Median : 123.0 Median : 2.000 Median : 3.000 Median : 3.000
## Mean : 174.9 Mean : 1.835 Mean : 3.111 Mean : 3.605
## 3rd Qu.: 229.0 3rd Qu.: 2.000 3rd Qu.: 4.000 3rd Qu.: 4.000
## Max. :1745.0 Max. :10.000 Max. :10.000 Max. :10.000
## NA's :1602
## tipo barrio longitud latitud
## apartamento:5100 valle del lili:1009 Min. :-76.59 Min. :3.333
## casa :3219 ciudad jardín : 518 1st Qu.:-76.54 1st Qu.:3.381
## pance : 412 Median :-76.53 Median :3.416
## la flora : 368 Mean :-76.53 Mean :3.418
## santa teresita: 263 3rd Qu.:-76.52 3rd Qu.:3.452
## el caney : 209 Max. :-76.46 Max. :3.498
## (Other) :5540
Luego, se realizan ajustes a las variables de tipo carácter (char) para asegurarse de que no existan errores de digitación en la base de datos.
mapeo <- c("á" = "a", "é" = "e", "í" = "i", "ó" = "o", "ú" = "u", "ü" = "u")
vivienda$barrio = chartr(paste(names(mapeo), collapse = ""), paste(mapeo, collapse = ""), vivienda$barrio)
rm(mapeo)
vivienda$barrio <- gsub("√∫", "u", vivienda$barrio)
vivienda$barrio <- gsub("é", "e", vivienda$barrio)
vivienda$barrio <- gsub("ó", "o", vivienda$barrio)
vivienda$barrio <- gsub("ò", "a", vivienda$barrio)
vivienda$barrio <- gsub("√≠", "i", vivienda$barrio)Se da inicio a la imputación de datos faltantes. Para ello, primero se asigna el piso uno a todas las viviendas de tipo casa que presentan datos faltantes en la variable “piso”.
en este punto se procede a verificar la cantidad de datos faltantes actuales a travez de la siguiente grafica
captured_output <- capture.output({
md.pattern(vivienda[, 1:13], plot = TRUE, rotate.names = TRUE)
})En este punto, se procede a realizar la imputación de la variable “parqueqderos” mediante un código que determina la moda de los parqueaderos por zonas y asigna la cantidad en función del precio de la vivienda. La hipótesis es que las viviendas con parqueadero deben tener un precio mayor en comparación con las que no lo tienen.
vivienda$NAPISO = ifelse(is.na(vivienda$piso),1,0)
vivienda$NAPARQUEA = ifelse(is.na(vivienda$parqueaderos),1,0)
vivienda$costo_metro_m = vivienda$preciom / vivienda$areaconst
# Calcular la moda y la cantidad de valores faltantes en parquea
df_resultado_estrato_parquea <- vivienda %>%
group_by(barrio, tipo,estrato) %>%
reframe(nv_parqueo = sum(NAPARQUEA, na.rm = TRUE),
N_MODE = Mode(parqueaderos, na.rm =TRUE))
df_resultado_estrato_parquea <- df_resultado_estrato_parquea %>%
rename(barrio1=barrio,
tipo1=tipo,
estrato1=estrato)
df_resultado_estrato_parquea$N_MODE[is.na(df_resultado_estrato_parquea$N_MODE)] <- 0
df_resultado_estrato_parquea$COSTO_METRO_CUADRADO <- NA
m<-max(df_resultado_estrato_parquea$N_MODE)
for(a in 1:m){
for (i in 1:nrow(df_resultado_estrato_parquea)) {
filtros <- filter(vivienda,
barrio == df_resultado_estrato_parquea[[i, "barrio1"]]
,tipo == df_resultado_estrato_parquea[[i, "tipo1"]]
,estrato == df_resultado_estrato_parquea[[i, "estrato1"]]
,parqueaderos == df_resultado_estrato_parquea[[i, "N_MODE"]]
)
if (nrow(filtros) > 0) {
filtros$costo_metro<-filtros$preciom / filtros$areaconst
val=sd(filtros$costo_metro)/mean(filtros$costo_metro)
#print(paste("entro", val))
costo_medio <-ifelse(val>0.20,median(filtros$costo_metro, na.rm=TRUE),mean(filtros$costo_metro, na.rm=TRUE))
df_resultado_estrato_parquea[i, "COSTO_METRO_CUADRADO"] <- costo_medio
}
}
df_resultado_estrato_parquea<- filter(df_resultado_estrato_parquea,
!is.na(COSTO_METRO_CUADRADO))
vivienda$NAPARQUEA = ifelse(is.na(vivienda$parqueaderos),1,0)
for (i in 1:nrow(df_resultado_estrato_parquea)) {
vivienda$parqueaderos <- ifelse(vivienda$barrio == df_resultado_estrato_parquea[[i, "barrio1"]] &
vivienda$tipo == df_resultado_estrato_parquea[[i, "tipo1"]] &
vivienda$estrato == df_resultado_estrato_parquea[[i, "estrato1"]] &
vivienda$costo_metro_m >= df_resultado_estrato_parquea[[i, "COSTO_METRO_CUADRADO"]] &
vivienda$NAPARQUEA == 1,
df_resultado_estrato_parquea[[i, "N_MODE"]],
vivienda$parqueaderos)
}
df_resultado_estrato_parquea$N_MODE = ifelse(df_resultado_estrato_parquea$N_MODE == 0, 0 ,df_resultado_estrato_parquea$N_MODE-1)
}
df_resultado_estrato_parquea <- vivienda %>%
group_by(zona, tipo,estrato) %>%
reframe(nv_parqueo = sum(NAPARQUEA, na.rm = TRUE),
N_MODE = Mode(parqueaderos, na.rm =TRUE))
df_resultado_estrato_parquea <- df_resultado_estrato_parquea %>%
rename(zona1=zona,
tipo1=tipo,
estrato1=estrato)
df_resultado_estrato_parquea$N_MODE[is.na(df_resultado_estrato_parquea$N_MODE)] <- 0
df_resultado_estrato_parquea$COSTO_METRO_CUADRADO <- NA
m<-max(df_resultado_estrato_parquea$N_MODE)
for(a in 1:m){
for (i in 1:nrow(df_resultado_estrato_parquea)) {
filtros <- filter(vivienda,
zona == df_resultado_estrato_parquea[[i, "zona1"]]
,tipo == df_resultado_estrato_parquea[[i, "tipo1"]]
,estrato == df_resultado_estrato_parquea[[i, "estrato1"]]
,parqueaderos == df_resultado_estrato_parquea[[i, "N_MODE"]]
)
if (nrow(filtros) > 0) {
filtros$costo_metro<-filtros$preciom / filtros$areaconst
val=sd(filtros$costo_metro)/mean(filtros$costo_metro)
#print(paste("entro", val))
costo_medio <-ifelse(val>0.20,median(filtros$costo_metro, na.rm=TRUE),mean(filtros$costo_metro, na.rm=TRUE))
df_resultado_estrato_parquea[i, "COSTO_METRO_CUADRADO"] <- costo_medio
}
}
df_resultado_estrato_parquea<- filter(df_resultado_estrato_parquea,
!is.na(COSTO_METRO_CUADRADO))
vivienda$NAPARQUEA = ifelse(is.na(vivienda$parqueaderos),1,0)
for (i in 1:nrow(df_resultado_estrato_parquea)) {
vivienda$parqueaderos <- ifelse(vivienda$zona == df_resultado_estrato_parquea[[i, "zona1"]] &
vivienda$tipo == df_resultado_estrato_parquea[[i, "tipo1"]] &
vivienda$estrato == df_resultado_estrato_parquea[[i, "estrato1"]] &
vivienda$costo_metro_m >= df_resultado_estrato_parquea[[i, "COSTO_METRO_CUADRADO"]] &
vivienda$NAPARQUEA == 1,
df_resultado_estrato_parquea[[i, "N_MODE"]],
vivienda$parqueaderos)
}
df_resultado_estrato_parquea$N_MODE = ifelse(df_resultado_estrato_parquea$N_MODE == 0, 0 ,df_resultado_estrato_parquea$N_MODE-1)
}
##asigno 0 a los demas
vivienda$parqueaderos <- ifelse(is.na(vivienda$parqueaderos),0,vivienda$parqueaderos)
rm(df_resultado_estrato_parquea,a,costo_medio,i,m,val,filtros)
captured_output <- capture.output({
md.pattern(vivienda[, 1:13], plot = TRUE, rotate.names = TRUE)
})En este punto, se han imputado todos los datos de parqueadero; sin embargo, aún se observan valores nulos para la variable “piso”. Por lo tanto, se realiza un proceso similar al de imputación del parqueadero, identificando las zonas por latitud y longitud. Se asume que, en dos apartamentos situados en el mismo edificio, los de los pisos superiores tienen un costo mayor.
## tratamiento de piso
# Calcular la moda y la cantidad de valores faltantes en piso
#rm(df_resultado_piso)
vivienda$todos = 1
filtro<-filter(vivienda,tipo == "apartamento")
df_resultado_piso <- filtro %>%
group_by(latitud, longitud, estrato) %>%
summarise(nv_piso = sum(NAPISO),
num_total = sum(todos))## `summarise()` has grouped output by 'latitud', 'longitud'. You can override
## using the `.groups` argument.
df_resultado_piso$DIF <- df_resultado_piso$nv_piso - df_resultado_piso$num_total
df_resultado_piso <- filter(df_resultado_piso,DIF<0)
df_resultado_piso <- filter(df_resultado_piso,nv_piso!=0)
#print(sum(df_resultado_piso$nv_piso))
for(a in 1:nrow(df_resultado_piso)){
filtro<- filter(vivienda,
latitud == df_resultado_piso[[a,"latitud"]] &
longitud == df_resultado_piso[[a,"longitud"]] &
tipo == "apartamento" &
estrato == df_resultado_piso[[a,"estrato"]]
)
listadepisos=sort(unique(filtro$piso))
elmax=max(listadepisos)
for(i in listadepisos){
#print(paste("iter ", a," piso ",i))
filtroa<-filter(vivienda,
piso==i &
latitud == df_resultado_piso[[a,"latitud"]] &
longitud == df_resultado_piso[[a,"longitud"]] &
tipo == "apartamento" &
estrato == df_resultado_piso[[a,"estrato"]])
if(nrow(filtroa)>0){
val = sd(filtroa$costo_metro_m)/mean(filtroa$costo_metro_m)
cost= ifelse(val>0.20 | is.na(val) ,median(filtroa$costo_metro_m),mean(filtroa$costo_metro_m))
vivienda$piso <- ifelse(vivienda$costo_metro_m<=cost &
vivienda$NAPISO == 1 &
vivienda$latitud == df_resultado_piso[[a,"latitud"]] &
vivienda$longitud == df_resultado_piso[[a,"longitud"]] &
vivienda$tipo == "apartamento" &
vivienda$estrato == df_resultado_piso[[a,"estrato"]]
,
ifelse(i==1,1,i-1),vivienda$piso)
vivienda$NAPISO = ifelse(is.na(vivienda$piso),1,0)
#print(sum(vivienda$NAPISO))
if(i==elmax){
#print(paste("elmax ",elmax))
vivienda$piso <-ifelse(vivienda$costo_metro_m>cost &
vivienda$NAPISO == 1 &
i==elmax &
vivienda$latitud == df_resultado_piso[[a,"latitud"]] &
vivienda$longitud == df_resultado_piso[[a,"longitud"]] &
vivienda$tipo == "apartamento" &
vivienda$estrato == df_resultado_piso[[a,"estrato"]]
,
i+1,vivienda$piso)
vivienda$NAPISO = ifelse(is.na(vivienda$piso),1,0)
#print(sum(vivienda$NAPISO))
}
}
}
}
vivienda$todos = 1
filtro<-filter(vivienda,tipo == "apartamento")
df_resultado_piso <- filtro %>%
group_by(barrio, estrato) %>%
summarise(nv_piso = sum(NAPISO),
num_total = sum(todos))## `summarise()` has grouped output by 'barrio'. You can override using the
## `.groups` argument.
df_resultado_piso$DIF <- df_resultado_piso$nv_piso - df_resultado_piso$num_total
df_resultado_piso <- filter(df_resultado_piso,DIF<0)
df_resultado_piso <- filter(df_resultado_piso,nv_piso!=0)
#print(sum(df_resultado_piso$nv_piso))
for(a in 1:nrow(df_resultado_piso)){
filtro<- filter(vivienda,
barrio == df_resultado_piso[[a,"barrio"]] &
tipo == "apartamento" &
estrato == df_resultado_piso[[a,"estrato"]]
)
listadepisos=sort(unique(filtro$piso))
elmax=max(listadepisos)
for(i in listadepisos){
#print(paste("iter ", a," piso ",i))
filtroa<-filter(vivienda,
piso==i &
barrio == df_resultado_piso[[a,"barrio"]] &
tipo == "apartamento" &
estrato == df_resultado_piso[[a,"estrato"]])
if(nrow(filtroa)>0){
val = sd(filtroa$costo_metro_m)/mean(filtroa$costo_metro_m)
cost= ifelse(val>0.20 | is.na(val) ,median(filtroa$costo_metro_m),mean(filtroa$costo_metro_m))
vivienda$piso <- ifelse(vivienda$costo_metro_m<=cost &
vivienda$NAPISO == 1 &
vivienda$barrio == df_resultado_piso[[a,"barrio"]] &
vivienda$tipo == "apartamento" &
vivienda$estrato == df_resultado_piso[[a,"estrato"]]
,
ifelse(i==1,1,i-1),vivienda$piso)
#print("aca1")
vivienda$NAPISO = ifelse(is.na(vivienda$piso),1,0)
#print(sum(vivienda$NAPISO))
if(i==elmax){
#print(paste("elmax ",elmax))
vivienda$piso <-ifelse(vivienda$costo_metro_m>cost &
vivienda$NAPISO == 1 &
i==elmax &
vivienda$barrio == df_resultado_piso[[a,"barrio"]] &
vivienda$tipo == "apartamento" &
vivienda$estrato == df_resultado_piso[[a,"estrato"]]
,
i+1,vivienda$piso)
vivienda$NAPISO = ifelse(is.na(vivienda$piso),1,0)
#print(sum(vivienda$NAPISO))
}
}
}
}
rm(filtro,filtroa,df_resultado_piso,cost,elmax,elmin,listadepisos,a,i,val)## Warning in rm(filtro, filtroa, df_resultado_piso, cost, elmax, elmin,
## listadepisos, : objeto 'elmin' no encontrado
vivienda$piso<-ifelse(is.na(vivienda$piso),1,vivienda$piso)
vivienda <- subset(vivienda, select = -c(NAPISO,NAPARQUEA,costo_metro_m,todos))
captured_output <- capture.output({
md.pattern(vivienda[, 1:13], plot = TRUE, rotate.names = TRUE)
})Al finalizar estos procesos, se ha completado la imputación de los datos y la base de datos está lista para realizar los respectivos análisis.
En este punto, se realiza un análisis de componentes principales (PCA) con el objetivo de reducir la dimensionalidad de los datos. Para ello, primero se normalizan las variables numéricas, y luego se llevan a cabo los diferentes procesos del PCA.
rm(elim,top_5_nulos,filas,nulos_por_fila)
vivienda_normalizada <- scale(vivienda[, c(3, 5:9)])
vivienda_normalizada <- as.data.frame(vivienda_normalizada)
row.names(vivienda_normalizada) <- vivienda$id
prcomp(vivienda_normalizada)## Standard deviations (1, .., p=6):
## [1] 1.7859444 1.0801856 0.8616258 0.6398383 0.5514201 0.4332962
##
## Rotation (n x k) = (6 x 6):
## PC1 PC2 PC3 PC4 PC5
## piso 0.1179789 -0.74788297 0.62880855 0.06749209 -0.1477085
## preciom -0.4700814 -0.32071206 -0.19581705 0.30978866 0.2208753
## areaconst -0.4819893 0.08404774 -0.02956444 0.61311674 -0.4557695
## parqueaderos -0.4103275 -0.35258478 -0.37135471 -0.65144988 -0.3539325
## banios -0.4887511 0.01178265 0.24020757 -0.13214321 0.7132220
## habitaciones -0.3543226 0.45419815 0.60809328 -0.28585817 -0.2961698
## PC6
## piso -0.07056096
## preciom 0.70218803
## areaconst -0.41965490
## parqueaderos -0.14055542
## banios -0.42087574
## habitaciones 0.35909702
En la primera componente principal (PC1), las variables con mayor carga son el numero de baños y el área de construcción, con cargas negativas altas de -0.489 y -0.482, respectivamente, lo que indica que estas variables influyen significativamente en esta componente. En contraste, la variable con menor carga es el piso, con una carga baja de 0.118, sugiriendo una influencia menor en esta componente.
Para la segunda componente principal (PC2), el piso tiene la carga más alta en valor absoluto, con una carga negativa de -0.748, lo que sugiere una fuerte influencia del piso en esta componente. Por otro lado, la variable con la carga más baja es el área de construcción, con una carga muy baja de 0.084, indicando una mínima influencia en esta componente.
En la figura se observa que las dos primeras componentes explican el 72.6% de la varianza de los datos, por lo que se opta por utilizar únicamente estas dos componentes.
Finalmente, se procede a crear un gráfico de representación de los vectores de las componentes principales para visualizar gráficamente la información contenida en la tabla.
fviz_pca_var(res.pca,
col.var = "contrib", # Color by contributions to the PC
gradient.cols = c("#FF7F00", "#034D94"),
repel = TRUE # Avoid text overlapping
)El Análisis de Componentes Principales (PCA) ha demostrado ser efectivo para reducir la dimensionalidad del conjunto de datos, explicando el 72.6% de la varianza total con las dos primeras componentes. Esto simplifica el análisis al enfocarse en las variables más influyentes y facilita la visualización y la interpretación de patrones clave en los datos.
Con el objetivo de identificar segmentos de similitud en las viviendas, se realiza un análisis de conglomerados utilizando la misma base de datos normalizada que se usó para el PCA. Para este análisis, se decide emplear la distancia de Minkowski. Se obtienen los valores de la prueba cutree para los 6 primeros clusters. Aunque la organización en dos clusters presenta el valor más alto en esta prueba, no se forman grupos grandes. Por lo tanto, se opta por utilizar el segundo valor más alto y se configuran 4 clusters.
# deu <- dist(vivienda_normalizada, method = "euclidean")
# dma <- dist(vivienda_normalizada, method = "manhattan")
dmi <- dist(vivienda_normalizada, method = "minkowski")
hc_emp <- hclust(dmi, method = 'complete')
for(a in 2:6){
cluster_assigments <- cutree(hc_emp, k = a )
sil <- silhouette(cluster_assigments, dist(vivienda_normalizada))
sil_avg <- mean(sil[,3])
cat("Coeficiente de Silhouette promedio k =", a, ": ", sil_avg,"\n")
}## Coeficiente de Silhouette promedio k = 2 : 0.7080745
## Coeficiente de Silhouette promedio k = 3 : 0.4041728
## Coeficiente de Silhouette promedio k = 4 : 0.4198973
## Coeficiente de Silhouette promedio k = 5 : 0.4132474
## Coeficiente de Silhouette promedio k = 6 : 0.3761095
Con la configuración descrita anteriormente, se obtienen los siguientes grupos:
cluster_assigments <- cutree(hc_emp, k = 4)
assigned_cluster <- vivienda_normalizada %>% mutate(cluster = as.factor(cluster_assigments))
table(assigned_cluster$cluster)##
## 1 2 3 4
## 7161 658 493 7
assigned_cluster$rownames <- rownames(assigned_cluster)
names(assigned_cluster)[names(assigned_cluster) == "cluster"] <- "cluster_id"
merged_df <- merge(vivienda, assigned_cluster, by.x = "id", by.y = "rownames")
merged_df <- merged_df[, -c(12:19)]
split_dfs <- split(merged_df, merged_df$cluster_id)
# Describir estadísticamente cada subdata frame
descriptions <- lapply(split_dfs, function(df) {
list(
Summary = summary(df),
Description = describe(df)
)
})
# Verificar los resultados
for (i in seq_along(descriptions)) {
cat("Cluster ID:", names(descriptions)[i], "\n")
print(descriptions[[i]]$Summary)
print(descriptions[[i]]$Description)
cat("\n\n")
}## Cluster ID: 1
## id zona piso.x estrato preciom.x
## Min. : 1 Zona Centro : 95 Min. : 1.000 3:1231 Min. : 58.0
## 1st Qu.:1984 Zona Norte :1732 1st Qu.: 1.000 4:1975 1st Qu.: 200.0
## Median :3939 Zona Oeste :1061 Median : 3.000 5:2470 Median : 300.0
## Mean :4064 Zona Oriente: 262 Mean : 3.621 6:1485 Mean : 364.8
## 3rd Qu.:6184 Zona Sur :4011 3rd Qu.: 5.000 3rd Qu.: 450.0
## Max. :8318 Max. :14.000 Max. :1850.0
## areaconst.x parqueaderos.x banios.x habitaciones.x
## Min. : 30 Min. :0.000 Min. : 0.00 Min. :0.000
## 1st Qu.: 75 1st Qu.:1.000 1st Qu.: 2.00 1st Qu.:3.000
## Median :108 Median :1.000 Median : 3.00 Median :3.000
## Mean :139 Mean :1.376 Mean : 2.78 Mean :3.243
## 3rd Qu.:178 3rd Qu.:2.000 3rd Qu.: 4.00 3rd Qu.:4.000
## Max. :932 Max. :5.000 Max. :10.00 Max. :7.000
## tipo barrio cluster_id
## apartamento:4999 Length:7161 1:7161
## casa :2162 Class :character 2: 0
## Mode :character 3: 0
## 4: 0
##
##
## vars n mean sd median trimmed mad min max range
## id 1 7161 4064.14 2419.87 3939 4040.73 3097.15 1 8318 8317
## zona* 2 7161 3.89 1.34 5 4.00 0.00 1 5 4
## piso.x 3 7161 3.62 2.94 3 3.11 2.97 1 14 13
## estrato* 4 7161 2.59 1.00 3 2.61 1.48 1 4 3
## preciom.x 5 7161 364.79 238.35 300 327.64 177.91 58 1850 1792
## areaconst.x 6 7161 139.01 89.90 108 123.64 62.27 30 932 902
## parqueaderos.x 7 7161 1.38 0.80 1 1.37 1.48 0 5 5
## banios.x 8 7161 2.78 1.12 3 2.71 1.48 0 10 10
## habitaciones.x 9 7161 3.24 0.95 3 3.19 0.00 0 7 7
## tipo* 10 7161 1.30 0.46 1 1.25 0.00 1 2 1
## barrio* 11 7161 204.31 106.98 213 208.59 143.81 1 361 360
## cluster_id* 12 7161 1.00 0.00 1 1.00 0.00 1 1 0
## skew kurtosis se
## id 0.09 -1.21 28.60
## zona* -0.54 -1.44 0.02
## piso.x 1.29 1.00 0.03
## estrato* -0.14 -1.04 0.01
## preciom.x 1.74 3.92 2.82
## areaconst.x 1.84 4.66 1.06
## parqueaderos.x 0.59 0.96 0.01
## banios.x 0.62 0.30 0.01
## habitaciones.x 0.51 2.16 0.01
## tipo* 0.86 -1.26 0.01
## barrio* -0.14 -1.23 1.26
## cluster_id* NaN NaN 0.00
##
##
## Cluster ID: 2
## id zona piso.x estrato preciom.x
## Min. : 2 Zona Centro : 27 Min. :1.000 3:213 Min. : 150.0
## 1st Qu.:1903 Zona Norte :144 1st Qu.:1.000 4:136 1st Qu.: 370.0
## Median :4574 Zona Oeste : 42 Median :2.000 5:192 Median : 500.0
## Mean :4210 Zona Oriente: 85 Mean :1.758 6:117 Mean : 635.2
## 3rd Qu.:6206 Zona Sur :360 3rd Qu.:2.000 3rd Qu.: 750.0
## Max. :8319 Max. :7.000 Max. :1999.0
## areaconst.x parqueaderos.x banios.x habitaciones.x
## Min. : 70.0 Min. :0.000 Min. : 0.000 Min. : 3.000
## 1st Qu.: 249.2 1st Qu.:1.000 1st Qu.: 4.000 1st Qu.: 6.000
## Median : 344.5 Median :2.000 Median : 5.000 Median : 7.000
## Mean : 399.2 Mean :2.005 Mean : 5.246 Mean : 7.036
## 3rd Qu.: 479.0 3rd Qu.:3.000 3rd Qu.: 6.000 3rd Qu.: 8.000
## Max. :1440.0 Max. :8.000 Max. :10.000 Max. :10.000
## tipo barrio cluster_id
## apartamento: 8 Length:658 1: 0
## casa :650 Class :character 2:658
## Mode :character 3: 0
## 4: 0
##
##
## vars n mean sd median trimmed mad min max range
## id 1 658 4210.16 2350.98 4573.5 4264.32 2821.39 2 8319 8317
## zona* 2 658 3.92 1.36 5.0 4.08 0.00 1 5 4
## piso.x 3 658 1.76 0.89 2.0 1.65 1.48 1 7 6
## estrato* 4 658 2.32 1.11 2.0 2.28 1.48 1 4 3
## preciom.x 5 658 635.24 379.78 500.0 577.56 252.04 150 1999 1849
## areaconst.x 6 658 399.19 219.20 344.5 369.99 155.67 70 1440 1370
## parqueaderos.x 7 658 2.00 1.59 2.0 1.84 1.48 0 8 8
## banios.x 8 658 5.25 1.57 5.0 5.18 1.48 0 10 10
## habitaciones.x 9 658 7.04 1.62 7.0 7.02 1.48 3 10 7
## tipo* 10 658 1.99 0.11 2.0 2.00 0.00 1 2 1
## barrio* 11 658 81.43 48.56 77.0 80.72 60.05 1 168 167
## cluster_id* 12 658 2.00 0.00 2.0 2.00 0.00 2 2 0
## skew kurtosis se
## id -0.24 -1.19 91.65
## zona* -0.77 -1.03 0.05
## piso.x 1.04 1.20 0.03
## estrato* 0.13 -1.36 0.04
## preciom.x 1.35 1.18 14.81
## areaconst.x 1.32 1.72 8.55
## parqueaderos.x 1.01 1.11 0.06
## banios.x 0.39 0.44 0.06
## habitaciones.x 0.05 -0.78 0.06
## tipo* -8.88 77.02 0.00
## barrio* 0.11 -1.20 1.89
## cluster_id* NaN NaN 0.00
##
##
## Cluster ID: 3
## id zona piso.x estrato preciom.x
## Min. : 4 Zona Centro : 2 Min. : 1.000 3: 7 Min. : 190
## 1st Qu.:4588 Zona Norte : 44 1st Qu.: 1.000 4: 18 1st Qu.: 870
## Median :5651 Zona Oeste : 95 Median : 2.000 5: 88 Median :1150
## Mean :5491 Zona Oriente: 2 Mean : 2.018 6:380 Mean :1158
## 3rd Qu.:6641 Zona Sur :350 3rd Qu.: 2.000 3rd Qu.:1400
## Max. :8310 Max. :13.000 Max. :1950
## areaconst.x parqueaderos.x banios.x habitaciones.x
## Min. : 50.0 Min. : 1.000 Min. : 0.000 Min. : 0.000
## 1st Qu.:300.0 1st Qu.: 3.000 1st Qu.: 4.000 1st Qu.: 4.000
## Median :367.0 Median : 4.000 Median : 5.000 Median : 4.000
## Mean :378.5 Mean : 4.195 Mean : 5.053 Mean : 4.288
## 3rd Qu.:450.0 3rd Qu.: 5.000 3rd Qu.: 6.000 3rd Qu.: 5.000
## Max. :630.0 Max. :10.000 Max. :10.000 Max. :10.000
## tipo barrio cluster_id
## apartamento: 93 Length:493 1: 0
## casa :400 Class :character 2: 0
## Mode :character 3:493
## 4: 0
##
##
## vars n mean sd median trimmed mad min max range
## id 1 493 5491.27 1714.11 5651 5618.35 1497.43 4 8310 8306
## zona* 2 493 4.33 1.09 5 4.53 0.00 1 5 4
## piso.x 3 493 2.02 1.71 2 1.67 1.48 1 13 12
## estrato* 4 493 3.71 0.61 4 3.84 0.00 1 4 3
## preciom.x 5 493 1158.03 378.63 1150 1152.08 400.30 190 1950 1760
## areaconst.x 6 493 378.50 103.19 367 374.73 123.06 50 630 580
## parqueaderos.x 7 493 4.19 1.56 4 4.05 1.48 1 10 9
## banios.x 8 493 5.05 1.17 5 5.00 1.48 0 10 10
## habitaciones.x 9 493 4.29 1.12 4 4.17 1.48 0 10 10
## tipo* 10 493 1.81 0.39 2 1.89 0.00 1 2 1
## barrio* 11 493 31.49 17.55 39 30.91 25.20 1 66 65
## cluster_id* 12 493 3.00 0.00 3 3.00 0.00 3 3 0
## skew kurtosis se
## id -0.80 0.84 77.20
## zona* -1.16 -0.29 0.05
## piso.x 3.57 15.18 0.08
## estrato* -2.28 5.34 0.03
## preciom.x 0.15 -0.74 17.05
## areaconst.x 0.25 -0.51 4.65
## parqueaderos.x 1.28 2.26 0.07
## banios.x 0.19 1.80 0.05
## habitaciones.x 0.94 3.10 0.05
## tipo* -1.59 0.52 0.02
## barrio* 0.05 -1.38 0.79
## cluster_id* NaN NaN 0.00
##
##
## Cluster ID: 4
## id zona piso.x estrato preciom.x
## Min. : 315 Zona Centro :0 Min. :1 3:2 Min. : 200.0
## 1st Qu.:2170 Zona Norte :0 1st Qu.:1 4:0 1st Qu.: 877.5
## Median :5016 Zona Oeste :0 Median :1 5:0 Median :1500.0
## Mean :3746 Zona Oriente:2 Mean :1 6:5 Mean :1215.0
## 3rd Qu.:5432 Zona Sur :5 3rd Qu.:1 3rd Qu.:1625.0
## Max. :5684 Max. :1 Max. :1800.0
## areaconst.x parqueaderos.x banios.x habitaciones.x
## Min. :1250 Min. : 0.000 Min. :1.000 Min. :2.000
## 1st Qu.:1432 1st Qu.: 2.500 1st Qu.:3.500 1st Qu.:3.000
## Median :1500 Median : 4.000 Median :5.000 Median :3.000
## Mean :1507 Mean : 4.143 Mean :4.143 Mean :3.714
## 3rd Qu.:1593 3rd Qu.: 5.000 3rd Qu.:5.000 3rd Qu.:4.500
## Max. :1745 Max. :10.000 Max. :6.000 Max. :6.000
## tipo barrio cluster_id
## apartamento:0 Length:7 1:0
## casa :7 Class :character 2:0
## Mode :character 3:0
## 4:7
##
##
## vars n mean sd median trimmed mad min max range
## id 1 7 3745.57 2252.79 5016 3745.57 990.38 315 5684 5369
## zona* 2 7 4.71 0.49 5 4.71 0.00 4 5 1
## piso.x 3 7 1.00 0.00 1 1.00 0.00 1 1 0
## estrato* 4 7 3.14 1.46 4 3.14 0.00 1 4 3
## preciom.x 5 7 1215.00 682.39 1500 1215.00 222.39 200 1800 1600
## areaconst.x 6 7 1506.57 162.20 1500 1506.57 148.26 1250 1745 495
## parqueaderos.x 7 7 4.14 3.13 4 4.14 1.48 0 10 10
## banios.x 8 7 4.14 1.68 5 4.14 1.48 1 6 5
## habitaciones.x 9 7 3.71 1.38 3 3.71 1.48 2 6 4
## tipo* 10 7 2.00 0.00 2 2.00 0.00 2 2 0
## barrio* 11 7 3.29 1.38 4 3.29 1.48 1 5 4
## cluster_id* 12 7 4.00 0.00 4 4.00 0.00 4 4 0
## skew kurtosis se
## id -0.52 -1.74 851.47
## zona* -0.75 -1.60 0.18
## piso.x NaN NaN 0.00
## estrato* -0.75 -1.60 0.55
## preciom.x -0.69 -1.62 257.92
## areaconst.x -0.16 -1.33 61.30
## parqueaderos.x 0.56 -0.78 1.18
## banios.x -0.74 -0.96 0.63
## habitaciones.x 0.43 -1.45 0.52
## tipo* NaN NaN 0.00
## barrio* -0.43 -1.45 0.52
## cluster_id* NaN NaN 0.00
Dados los resultados anteriores, se procede a realizar una descripción de cada uno de los clusters:
Después de los análisis anteriores, se procede a realizar un análisis de correspondencia para identificar relaciones entre las variables categóricas. Primero, se decide realizar asociaciones entre pares de variables, obteniendo los siguientes resultados.
En primer lugar, se analiza la asociación entre las variables zona y estrato.
# Tabla de contingencia para las variables zona y estrato
tabla_zona_estrato <- table(vivienda$zona, vivienda$estrato)
print(tabla_zona_estrato)##
## 3 4 5 6
## Zona Centro 105 14 4 1
## Zona Norte 572 407 769 172
## Zona Oeste 54 84 290 770
## Zona Oriente 340 8 2 1
## Zona Sur 382 1616 1685 1043
##
## Pearson's Chi-squared test
##
## data: tabla_zona_estrato
## X-squared = 3830.4, df = 12, p-value < 2.2e-16
La prueba de chi-cuadrado muestra un valor p de significancia inferior a 0.05, lo que indica que las variables están correlacionadas.
La gráfica revela que la Zona Sur está estrechamente relacionada con los estratos 4 y 5, mientras que las zonas Oriente y Centro se asocian principalmente con el estrato 3 y, en menor medida, con los otros estratos.
Posteriormente, se procede a analizar las variables zona y tipo, obteniendo los siguientes resultados:
# Tabla de contingencia para las variables zona y tipo
tabla_zona_tipo <- table(vivienda$zona, vivienda$tipo)
print(tabla_zona_tipo)##
## apartamento casa
## Zona Centro 24 100
## Zona Norte 1198 722
## Zona Oeste 1029 169
## Zona Oriente 62 289
## Zona Sur 2787 1939
##
## Pearson's Chi-squared test
##
## data: tabla_zona_tipo
## X-squared = 690.93, df = 4, p-value < 2.2e-16
resultados_ac_2 <- CA(tabla_zona_tipo)
##filas
coords_rows <- as.data.frame(resultados_ac_2$row$coord)
coords_rows$category <- rownames(coords_rows)
names(coords_rows)[1] <- "V1"
ggplot(coords_rows, aes(x = V1, y = 0, label = category)) +
geom_point(color = 'blue', size = 4) +
geom_text(angle = 90, hjust = 1.1, vjust = -0.5) +
#geom_text(vjust = -0.5) +
theme_minimal() +
labs(title = "Coordenadas de las Filas en la Dimensión 1",
x = "Coordenada Dimensión 1",
y = "") +
theme(axis.text.y = element_blank(),
axis.ticks.y = element_blank())De la prueba chi-cuadrado se deduce que las variables tienen correlación. Sin embargo, dado que la variable tipo solo contiene dos categorías, la reducción de la dimensionalidad resulta en una sola dimensión, lo que genera la gráfica presentada.
La gráfica muestra que las categorías Zona Oriente y Zona Centro tienen una fuerte correlación con la categoría casa. En contraste, las Zonas Sur y Norte no presentan una alta correlación con ninguna de las categorías de la variable tipo. Por otro lado, la Zona Oeste muestra una fuerte relación con la categoría apartamento.
Finalmente, se procede a analizar las variables estrato y tipo, obteniendo los siguientes resultados:
# Tabla de contingencia para las variables estrato y tipo
tabla_estrato_tipo <- table(vivienda$estrato, vivienda$tipo)
print(tabla_estrato_tipo)##
## apartamento casa
## 3 639 814
## 4 1404 725
## 5 1766 984
## 6 1291 696
##
## Pearson's Chi-squared test
##
## data: tabla_estrato_tipo
## X-squared = 224.33, df = 3, p-value < 2.2e-16
resultados_ac_3 <- CA(tabla_estrato_tipo)
# filas
coords_rows <- as.data.frame(resultados_ac_3$row$coord)
coords_rows$category <- rownames(coords_rows)
names(coords_rows)[1] <- "V1"
print(names(coords_rows))## [1] "V1" "category"
ggplot(coords_rows, aes(x = V1, y = 0, label = category)) +
geom_point(color = 'blue', size = 4) +
geom_text(angle = 90, hjust = 1.1, vjust = -0.5) +
#geom_text(vjust = -0.5) +
theme_minimal() +
labs(title = "Coordenadas de las Filas en la Dimensión 1",
x = "Coordenada Dimensión 1",
y = "") +
theme(axis.text.y = element_blank(),
axis.ticks.y = element_blank())Nuevamente, la prueba chi-cuadrado muestra que existe correlación entre las variables categóricas. Sin embargo, debido al reducido número de categorías en la variable tipo, se genera una sola dimensión, que está representada en la gráfica mostrada en pantalla. En esta gráfica, se observa que el estrato 3 tiene una alta correlación con la categoría casa, mientras que los estratos 4, 5 y 6 muestran una mayor presencia de apartamentos.
Para finalizar, se realiza el análisis conjunto de las tres variables, obteniendo los siguientes resultados.
vivienda_cat = subset(vivienda, select = c(tipo,zona,estrato))
tipo = vivienda_cat$tipo
zona = vivienda_cat$zona
estrato = as.factor(vivienda_cat$estrato)
viv_cat <- cbind(vivienda_cat,tipo,zona,estrato)
viv_cat[,1:3] <- NULL
summary(viv_cat)## tipo zona estrato
## apartamento:5100 Zona Centro : 124 3:1453
## casa :3219 Zona Norte :1920 4:2129
## Zona Oeste :1198 5:2750
## Zona Oriente: 351 6:1987
## Zona Sur :4726
uni.mca <- MCA(viv_cat, graph = FALSE)
fviz_mca_biplot(uni.mca,
repel = TRUE,
ggtheme = theme_grey(),
invisible = "ind") +
labs(title ="Representación de las categorías")var_coord <- uni.mca$var$coord
# Obtener la contribución de cada variable a las dimensiones
var_contrib <- uni.mca$var$contrib
# # Ver los resultados
# print(var_coord)
# print(var_contrib)En la gráfica se puede representar la relación entre las tres variables, mostrando comportamientos similares a los descritos en los análisis individuales de pares de variables. Se observa que las casas en los estratos 4 y 5, así como las zonas Norte y Sur, presentan la mayor relación entre las distintas categorías.
Segmentación del Mercado: El clustering ha identificado varios segmentos clave en el mercado inmobiliario de Cali, que reflejan diferentes necesidades y características de los compradores.
Dimensionalidad Reducida: El PCA ha permitido reducir la complejidad del conjunto de datos y ha destacado las principales variables que explican la variabilidad en los precios de las viviendas.
Análisis de Correspondencia: El análisis de correspondencia ha proporcionado una comprensión más profunda de las relaciones entre las variables categóricas.