Tarea 4 - Tópicos y Minería de Datos

Tarea

Punto 1: Con la base de viviendas, elabore una clasificación adecuada con K-medias y DBSCAN con solo variables continuas.

Punto 2: Use K-Prototypes para clasificar las viviendas.

Nota: Evitar usar variables altamente correlacionadas en el ejercicio.

library(dplyr)
library(ggplot2)
library(cluster)
library(dbscan)
library(factoextra)
library(caTools)
library(klaR)
library(patchwork)
library(reticulate)
library(corrplot)
library(FactoClass)
path <- "C:/Users/LENOVO/OneDrive/01 UE/Topicos y Mineria de Datos/04 Tarea"
setwd(path)
housing <- read.csv("housing.csv")
housing

Punto 1

Se realiza el cargue de DataFrame Housing, el cual sus columnas son de tipo:

str(housing)
## 'data.frame':    20640 obs. of  10 variables:
##  $ longitude         : num  -122 -122 -122 -122 -122 ...
##  $ latitude          : num  37.9 37.9 37.9 37.9 37.9 ...
##  $ housing_median_age: num  41 21 52 52 52 52 52 52 42 52 ...
##  $ total_rooms       : num  880 7099 1467 1274 1627 ...
##  $ total_bedrooms    : num  129 1106 190 235 280 ...
##  $ population        : num  322 2401 496 558 565 ...
##  $ households        : num  126 1138 177 219 259 ...
##  $ median_income     : num  8.33 8.3 7.26 5.64 3.85 ...
##  $ median_house_value: num  452600 358500 352100 341300 342200 ...
##  $ ocean_proximity   : chr  "NEAR BAY" "NEAR BAY" "NEAR BAY" "NEAR BAY" ...

Se seleccionan las variables continuas:

# Selección de variables continuas
housing_cont <- housing |>
  dplyr::select(longitude, latitude, housing_median_age,
                total_rooms, total_bedrooms, population,
                households, median_income, median_house_value)

head(housing_cont)

Se realiza la correlación de las varibles continuas para así definir las variables altamente correlacionadas y no tenerlas en cuenta.

# Matriz de correlación
cor_matrix <- cor(housing_cont, use = "pairwise.complete.obs")
corrplot(cor_matrix, method = "color", type = "upper", tl.cex = 0.7)

Luego de realizarse la matriz de correlación se llega a la conclusión que total_rooms, total_bedrooms y households presentan multicolinealidad, por lo cual se conserva:

housing_median_age

population

median_income

median_house_value

# Escalamiento de variables seleccionadas
vars_selecc <- housing_cont |>
                    dplyr::select(housing_median_age, population, 
                          median_income, median_house_value) |>
  scale() |>
  as.data.frame()

head(vars_selecc)
str(vars_selecc)
## 'data.frame':    20640 obs. of  4 variables:
##  $ housing_median_age: num  0.982 -0.607 1.856 1.856 1.856 ...
##  $ population        : num  -0.974 0.861 -0.821 -0.766 -0.76 ...
##  $ median_income     : num  2.3447 2.3322 1.7827 0.9329 -0.0129 ...
##  $ median_house_value: num  2.13 1.31 1.26 1.17 1.17 ...

Aplicación de métodos de clustering para definir la cantidad de grupos

# Método Silhoute
set.seed(12345)
fviz_nbclust(vars_selecc, kmeans, method = "silhouette", k.max = 10)

El método Silhoute encont ramos el número óptimo de grupos, en este caso nos recomienda 2 grupos.

## Método de distancia cuadrada dentro (método del codo)
set.seed(12345)
fviz_nbclust(vars_selecc, kmeans, method = "wss", k.max = 10)

Se aplica el método de distancia cuadrada dentro llamado método del codo, no nos suguiere la cantidad de grupos pero basados en la gráfica se define que serían 3 grupos.

Se define que se tendrán en cuenta 4 grupos

K-medias

set.seed(12345)
kmedias <- kmeans(vars_selecc, 4)
vars_selecc$grupo_kmedias <- kmedias$cluster |> as.character()

ggplot(vars_selecc, aes(x = median_income, y = median_house_value, color = grupo_kmedias)) +
  geom_point(alpha = 0.6, size = 1) +
  labs(title = "K-medias (k=4)", 
       x = "Median income (scaled)", 
       y = "Median house value (scaled)") +
  theme_minimal()

El gráfico de K-medias con k = 4 muestra cómo los datos se dividieron en cuatro grupos diferenciados a partir de las variables de ingreso medio y valor medio de la vivienda las cuales fueron escaladas. Se observa que a mayores ingresos corresponden viviendas de mayor valor, y que el algoritmo logró separar distintos segmentos socioeconómicos dentro de la base. Aunque los grupos se traslapan en las zonas intermedias, se distinguen bien cuatro perfiles: hogares de bajo ingreso y bajo valor de vivienda, grupos de ingresos y valores medios, y un grupo más reducido de alto ingreso con viviendas más costosas. En conjunto, el modelo captura patrones lógicos de relación entre ingreso y precio de vivienda, resumiendo la estructura de la base en cuatro categorías representativas.

DBSCAN

## DBSCAN

# Selección de variables continuas
datos_db <- housing |>
  dplyr::select(housing_median_age, population, median_income, median_house_value) |>
  na.omit()

# Escalamiento
X <- scale(datos_db)

# Ajuste de DBSCAN
set.seed(123)
dbscan_model <- dbscan(X, eps = 0.3, minPts = 8)

# Resultados
table(dbscan_model$cluster)
## 
##     0     1     2     3     4     5     6     7     8     9    10    11    12 
##  3718 16583     5    36     8    11    14     8     7     7     7    25    18 
##    13    14    15    16    17    18    19    20    21    22    23    24    25 
##    20    10     5    17     7    25     8    17     6     9     8     7     4 
##    26    27    28    29    30    31 
##     8    11     8    11     8     4
# Agregar clúster al dataset
datos_db$cluster_db <- as.factor(dbscan_model$cluster)

# Visualización
ggplot(datos_db, aes(x = median_income, y = median_house_value, color = cluster_db)) +
  geom_point(alpha = 0.7, size = 1.5) +
  labs(title = "Clasificación con DBSCAN (4 grupos)",
       x = "median_income",
       y = "median_house_value") +
  theme_minimal()

El gráfico de DBSCAN de 4 grupos, evidencia que la mayor parte de los datos quedaron agrupados en un solo grupo grande, mientras que el resto se fragmenta en grupos muy pequeños y puntos considerados como ruido. A diferencia de K-medias, aquí no se observan cuatro segmentos bien balanceados, sino un patrón donde predomina un conglomerado principal de viviendas con ingresos y valores similares, acompañado de subgrupos minoritarios que representan casos atípicos o zonas menos densas de la muestra.

Punto 2

Curva de Codo

##Curva de Codo para encontrar el valor óptimo de K (K-PROTOTYPES)

if(!exists("data")||is.function(data)){
  if(exists("housing")) data<-housing else if(exists("housing_cat")) data<-housing_cat else stop("Define 'data' con tu dataset (p.ej., data <- housing).")
}
data<-as.data.frame(data)
if(!NROW(data)) stop("'data' no tiene filas.")

cost<-numeric()#Vector para almacenar el costo
K<-1:4#Rango de K a probar

set.seed(123)#Para reproducibilidad

#Identificar columnas numéricas y categóricas
num_cols<-names(data)[sapply(data,is.numeric)]
cat_cols<-names(data)[sapply(data,is.factor)]
lambda<-1#peso de variables categóricas

for(num_clusters in K){
  if(NROW(data)<num_clusters) stop("K > nrow(data).")#FIX:evita muestrear más filas de las que hay
  
  #Inicializar prototipos aleatorios
  proto_idx<-sample.int(NROW(data),num_clusters)
  proto_num<-if(length(num_cols)) data[proto_idx,num_cols,drop=FALSE] else NULL
  proto_cat<-if(length(cat_cols)) data[proto_idx,cat_cols,drop=FALSE] else NULL
  
  #Asignar clústeres
  clusters<-rep(NA_integer_,NROW(data))
  for(i in 1:NROW(data)){
    dists<-numeric(num_clusters)
    for(k in 1:num_clusters){
      d_num<-if(length(num_cols)) sum((as.numeric(data[i,num_cols])-as.numeric(proto_num[k,,drop=FALSE]))^2,na.rm=TRUE) else 0
      mism<-if(length(cat_cols)) {
        xi<-as.character(data[i,cat_cols,drop=FALSE][1,])
        pj<-as.character(proto_cat[k,cat_cols,drop=FALSE][1,])
        sum(xi!=pj,na.rm=TRUE)
      } else 0
      dists[k]<-d_num+lambda*mism
    }
    if(anyNA(dists)) dists[is.na(dists)]<-Inf#FIX:evita which.min con NA
    clusters[i]<-which.min(dists)
  }
  
  #CÁLCULO MANUAL DEL COSTO
  current_cost<-0
  for(i in 1:NROW(data)){
    k<-clusters[i]
    d_num<-if(length(num_cols)) sum((as.numeric(data[i,num_cols])-as.numeric(proto_num[k,,drop=FALSE]))^2,na.rm=TRUE) else 0
    mism<-if(length(cat_cols)) {
      xi<-as.character(data[i,cat_cols,drop=FALSE][1,])
      pj<-as.character(proto_cat[k,cat_cols,drop=FALSE][1,])
      sum(xi!=pj,na.rm=TRUE)
    } else 0
    current_cost<-current_cost+d_num+lambda*mism
  }
  
  cost[num_clusters]<-current_cost
}

#Dataframe
elbow_data<-data.frame(K=K,Cost=cost)

#Gráfica de codo
ggplot(elbow_data,aes(x=K,y=Cost))+
  geom_line(color="blue")+
  geom_point(color="blue",size=3)+
  labs(
    title="Método del Codo para K óptimo (K-Prototypes)",
    x="Número de clústeres (K)",
    y="Costo total"
  )+
  theme_minimal()

La gráfica del método del codo muestra cómo disminuye el costo total que suma de distancias mixtas, a medida que aumenta el número de grupos en el modelo K-Prototypes. Se observa una fuerte caída del costo al pasar de K = 1 a K = 2, lo que indica que dividir la base en dos grupos mejora significativamente la homogeneidad interna. Sin embargo, al aumentar a K = 3 o K = 4 la reducción adicional del costo es mucho menor y la curva tiende a estabilizarse. Por lo tanto, el “codo” se identifica alrededor de K = 2, lo que sugiere que este es el número óptimo de grupos para clasificar las viviendas.

Modelo final con K-Prototypes (3 grupos)

### Modelo final con K-Prototypes (3 grupos)

set.seed(42)#Semilla para reproducibilidad
num_clusters <- 3
lambda <- 1#Peso de categóricas

#Inicializar prototipos aleatorios
proto_idx <- sample.int(nrow(data), num_clusters) #FIX
proto_num <- data[proto_idx, num_cols, drop = FALSE]
proto_cat <- data[proto_idx, cat_cols, drop = FALSE]

#Asignar clúster a cada observación
clusters <- rep(NA, nrow(data))
for (i in 1:nrow(data)) {
  dists <- numeric(num_clusters)
  for (k in 1:num_clusters) {
    d_num <- sum((as.numeric(data[i, num_cols]) - as.numeric(proto_num[k, ]))^2, na.rm = TRUE) 
    mism  <- sum(as.character(data[i, cat_cols]) != as.character(proto_cat[k, ]), na.rm = TRUE)
    dists[k] <- d_num + lambda * mism }
  if (anyNA(dists)) dists[is.na(dists)] <- Inf 
  clusters[i] <- which.min(dists)
}
print(table(clusters))
## clusters
##     1     2     3 
##  3046  3800 13794
#Añadir el clúster al dataset
data_clustered <- data |>
  mutate(Cluster = clusters)

print(head(data_clustered,10))
##    longitude latitude housing_median_age total_rooms total_bedrooms population
## 1    -122.23    37.88                 41         880            129        322
## 2    -122.22    37.86                 21        7099           1106       2401
## 3    -122.24    37.85                 52        1467            190        496
## 4    -122.25    37.85                 52        1274            235        558
## 5    -122.25    37.85                 52        1627            280        565
## 6    -122.25    37.85                 52         919            213        413
## 7    -122.25    37.84                 52        2535            489       1094
## 8    -122.25    37.84                 52        3104            687       1157
## 9    -122.26    37.84                 42        2555            665       1206
## 10   -122.25    37.84                 52        3549            707       1551
##    households median_income median_house_value ocean_proximity Cluster
## 1         126        8.3252             452600        NEAR BAY       3
## 2        1138        8.3014             358500        NEAR BAY       3
## 3         177        7.2574             352100        NEAR BAY       3
## 4         219        5.6431             341300        NEAR BAY       3
## 5         259        3.8462             342200        NEAR BAY       3
## 6         193        4.0368             269700        NEAR BAY       3
## 7         514        3.6591             299200        NEAR BAY       3
## 8         647        3.1200             241400        NEAR BAY       3
## 9         595        2.0804             226700        NEAR BAY       3
## 10        714        3.6912             261100        NEAR BAY       3

El 66.8% de las viviendas quedaron en el grupo 3 (13 794 casos), 18.4% en el grupo 2 (3 800) y 14.8% en el grupo 1 (3 046), sobre un total de 20 640 observaciones. La partición está cargada hacia el grupo 3.

Es recomendable primero perfilar cada grupo (medias/modas) y si para conseguir más equilibrio se debe pruebar otros valores de K o ajustar el peso de las variable categóricas.

Aplicación cluster.carac

library(FactoClass)

#Usa la columna de clúster que creaste
cl <- as.factor(data_clustered$Cluster)

#Variable continuas
cluster.carac(tabla = data_clustered[sapply(data_clustered, is.numeric)],
              class = cl, tipo.v = "continuas")
## class: 1
##                    Test.Value Class.Mean Frequency Global.Mean
## latitude                5.444     35.826      3046      35.632
## population              4.736   1515.208      3046    1425.477
## longitude               3.896   -119.439      3046    -119.570
## housing_median_age     -8.375     26.876      3046      28.639
## median_income         -28.483      2.965      3046       3.871
## median_house_value    -43.849 122208.470      3046  206855.817
## Cluster              -123.166      1.000      3046       2.521
## ------------------------------------------------------------ 
## class: 2
##                    Test.Value Class.Mean Frequency Global.Mean
## latitude               32.739     36.657      3800      35.632
## longitude              -7.496   -119.790      3800    -119.570
## population            -12.835   1212.495      3800    1425.477
## households            -17.050    404.022      3800     499.540
## total_rooms           -18.963   2029.573      3800    2635.763
## Cluster               -48.150      2.000      3800       2.521
## median_income         -55.677      2.321      3800       3.871
## median_house_value    -76.540  77435.262      3800  206855.817
## ------------------------------------------------------------ 
## class: 3
##                    Test.Value Class.Mean Frequency Global.Mean
## Cluster               132.421      3.000     13794       2.521
## median_house_value     96.040 261200.744     13794  206855.817
## median_income          67.289      4.498     13794       3.871
## total_rooms            17.077   2818.455     13794    2635.763
## households             13.885    525.572     13794     499.540
## population              6.997   1464.335     13794    1425.477
## housing_median_age      6.092     29.015     13794      28.639
## longitude               3.235   -119.538     13794    -119.570
## latitude              -31.051     35.307     13794      35.632

Grupo 1: Este grupo se caracteriza por viviendas con valores y condiciones por debajo del promedio general. Se observa que la edad mediana de las viviendas es menor al global, al igual que el ingreso mediano y el valor medio de la casa, lo que sugiere zonas con construcciones más recientes pero de bajo nivel socioeconómico. Aunque la población es ligeramente más alta que la media, los ingresos y precios de vivienda están significativamente por debajo, indicando un grupo con concentración de hogares de bajos recursos.

Grupo 2: En este grupo predominan viviendas con menor número de habitaciones y hogares respecto al promedio, así como ingresos y valores de vivienda más bajos. La latitud es mayor, lo que puede estar asociado a una localización geográfica distinta más hacia el norte. El perfil refleja comunidades con mayor densidad poblacional, viviendas de menor tamaño y valor, y menores ingresos, lo cual corresponde a áreas más vulnerables o con desarrollo urbano limitado.

Grupo 3: Este grupo reúne las viviendas de mayor nivel socioeconómico. Aquí el valor medio de la vivienda y el ingreso mediano son significativamente superiores al promedio general. Además, el número de habitaciones y hogares es mayor, lo que denota viviendas más grandes y con mayor capacidad habitacional. Aunque la latitud es más baja que la media posible ubicación hacia el sur, el conjunto muestra características de zonas consolidadas, con mayor poder adquisitivo y precios de vivienda elevados.