Como continuación del estudio iniciado en la Práctica 1, procedemos a aplicar modelos analíticos, tanto no supervisados como supervisados, sobre el juego de datos seleccionado y ya preparado. En esta Práctica 2 tendréis que cargar los datos previamente preparados en la Práctica 1.
El objetivo es que pongáis en práctica con vuestros propios datos todos los modelos no supervisados y supervisados que se han utilizado en las 3 PECs previas. Además, se propone que se utilicen métricas y algoritmos alternativos a los propuestos en las PECs ya realizadas.
Punto común para todos los ejercicios
En todos los apartados de los ejercicios de esta práctica se pide al estudiante, además de aplicar los diferentes métodos, analizar correctamente el problema, detallarlo de manera exhaustiva, resaltando el por qué del análisis y cómo se ha realizado, incluir elementos visuales, explicar los resultados y realizar las comparativas oportunas con sus conclusiones.
En toda la práctica es necesario documentar cada apartado del ejercicio que se ha hecho, el por qué y como se ha realizado. Asimismo, todas las decisiones y conclusiones deberán ser presentados de forma razonada y clara, contextualizando los resultados, es decir, especificando todos y cada uno de los pasos que se hayan llevado a cabo para su resolución.
En definitiva, se pide al estudiante que complete los siguientes pasos con el juego de datos preparado en la Práctica 1:
Modelos no supervisados
Aplicar el algoritmo no supervisado k-means basado en el concepto de distancia entre las medias de los grupos, sobre el juego de datos originales y los datos normalizados. Se recuerda que se deben utilizar las variables cuantiativas o binarias que formen parte de la base de datos. También, en ese apartado se debe decidir si los grupos se definen a partir de las variables normalizados o no y se debe seleccionar el número de clusters que mejor se ajuste a los datos.
Utilizando el número de clusters y los datos (normalizados o no)
seleccionados en el punto 1, utilizar el algoritmo k-medians
(basado en las medianas como centros de los clusters) para definir cada
uno de los grupos. Comparar los resultados obtenidos con ambos
algoritmos, k-means y k-medians, y comentad qué método
os parece el más adecuado para vuestros datos. Tener en cuenta que el
algoritmo basado en la mediana no es lo mismo que el around
medoids que se implementa con la función pam()
del
paquete “cluster” the R. Por lo tanto, adicionalmente también podéis
comparar los resultados con los obtenidos con el método around
medoids.
Entrenar de nuevo el modelo basado en k-means que habéis seleccionado en el punto 1 pero usando una métrica de distancia diferente a la distancia euclidiana y comparad los resultados con los obtenidos en los puntos anteriores.
Utilizar los algoritmos DBSCAN, probando con
diferentes valores del parámetro eps
y minPts
,
y comparar los resultados con los métodos anteriores. Comentad si el
número de clusters coincide con el punto 1 y si los casos que los forman
son similares.
Modelos supervisados
Seleccionar una muestra de entrenamiento y una de test utilizando las proporciones que se consideren más adecudas en función de la disponibilidad de datos. Justificar dicha selección.
Una vez definida la variable objeto que se desea predecir, aplicar un modelo de generación de reglas a partir de árboles de decisión y ajustar las diferentes opciones (tamaño mínimo de los nodos, criterios de división, …) para su obtención. Obtener el árbol sin y con opciones de poda. Obtener la matriz de confusión. Finalmente, comparar los resultados obtenidos con y sin opciones de poda. Alternativamente, si la variable objeto de estudio es cuantitativa pura se obtienen los criterios de error que nos permitan determinar la capacidad predictiva.
Aplicar un modelo supervisado diferente al del punto 6 (puede ser un algoritmo de árboles de decisión distinto u otro algoritmo supervisado alternativo). Comparar el resultado con el modelo generado anteriormente. Se pueden utilizar los criterios de evaluación de modelos descritos en el material docente de la asignatura.
Identificar eventuales limitaciones del dataset seleccionado y analizar los riesgos en el caso de utilizar el modelo para clasificar un nuevo caso. Por ejemplo, puede haber dificultades de sobreajuste, en el caso de clasificar, los porcentajes de falsos positivos y falsos negativos son similares, etc..
NOTA IMPORTANTE: Recordad que si las variables en vuestra base de datos tienen unidades de medida muy distintas es recomendable transformar las variables para evitar el efecto escala debido a las diferentes unidades de medida.
Incluimos en este apartado una lista de recursos de programación para minería de datos donde podréis encontrar ejemplos, ideas e inspiración:
La fecha límite de entrega es el 11/06/2025.
Este documento describe un flujo de trabajo analítico para la segunda práctica: PRA2 de la asignatura Minería de Datos. Como continuación del estudio iniciado en la Práctica 1: PRA1, se aplicarán modelos analíticos, tanto no supervisados como supervisados sobre el juego de datos previamente utilizados en la PRA1: **Diabetes_prediction_dataset*.
Precedente
La diabetes es una condición crónica definida por un nivel elevado de glucosa en sangre. La diabetes causa daño progresivo a los riñones, los ojos y el corazón con el tiempo. La detección temprana de la diabetes es una tarea desafiante. La enfermedad de la diabetes se puede dividir en tres categorías:
Contexto y Objetivos
Este proyecto responde a una necesidad creciente de desarrollar sistemas predictivos capaces de identificar pacientes en riesgo de desarrollar diabetes de tipo 2 basados en factores clínicos, de estilo de vida y antecedentes médicos. Sabido es, que la diabetes es la causante de enfermedades cardíacas, problemas renales y dificultades oculares, y es por ello, que es fundamental prevenir, controlar y crear conciencia al respecto.
El objetivo es la mejora de la predicción y diagnóstico temprano de diabetes tipo 2 proporcionando a profesionales de la salud y sistemas sanitarios una herramienta predictiva que permita identificar personas en riesgo antes de que desarrollen síntomas graves. Este conocimiento permitirá reducir costos sanitarios, optimizar recursos preventivos y diseñar campañas de concienciación más efectivas en una primera instancia.
Además, se plantea como objetivo secundario, la autoalimentación o agregación de datos recopilados de los pacientes detectados con la variante de tipo 2, con la finalidad de construir una base de conocimiento que pueda ser utilizada para apoyar al diseño computacional de nuevas moléculas terapéuticas o la optimización y mejora de los tratamientos actuales.
Para ello, se explora en profundidad el dataset mediante la aplicación de modelos tanto no supervisados como supervisados, buscando responder a preguntas clave relacionadas con el dominio del problema.
Modelos No Supervisados:
Modelos Supervisados:
Para lograr estos objetivos, utilizaremos una variedad de algoritmos de minería de datos, incluyendo k-means, k-medians, DBSCAN (para modelos no supervisados), y árboles de decisión (para modelos supervisados). La selección y la configuración de estos algoritmos se basarán en las características específicas del dataset y en los resultados de la fase de análisis exploratorio.
El éxito de este proyecto se medirá en función de la capacidad para generar insights relevantes sobre los factores de riesgo de la diabetes, construir modelos predictivos precisos para la detección temprana y comunicar los resultados de manera clara y efectiva a los profesionales de la salud y a los responsables de la toma de decisiones en el ámbito de salud.
Con la entrega del presente documento se incluye la definición del problema desde la perspectiva de negocio, incluyendo objetivos, impacto esperado y justificación.
El conjunto de datos utilizado en este estudio es el
El archivo está en formato CSV (Comma Separated Values) y contiene 10000 observaciones y 21 variables distintas y bien estructuradas. El dataset presenta una combinación de variables numéricas continuas, categóricas y binarias.
El conjunto de las variables nos permite abordar tanto los problemas de modelado supervisado (clasificación de riesgo de diabetes) como los no supervisado (identificación de perfiles de riesgo mediante técnicas de agrupamiento).
El dataset de Diabetes Prediction es apropiado para el objetivo de este proyecto, ya que ofrece una representación realista. Contiene más de 5 variables numéricas continuas: Age, BMI, Waist_Circumference, Fasting_Blood_Glucose, HbA1c, entre otras, al menos 2 variables categóricas: Sex, Ethnicity, Physical_Activity_Level, etc., y más de 1 variable binaria: Family_History_of_Diabetes, Previous_Gestational_Diabetes, cumpliendo todos los requisitos mínimos solicitados para la elección de los datos.
La fase de preparación de los datos tiene como objetivo transformar el conjunto de datos crudo en una estructura limpia, balanceada y normalizada para el aprendizaje automático, tanto supervisado como no supervisado. Para ello, en primer lugar se procederá a la eliminación o de valores faltantes, garantizando así la integridad del dataset. Posteriormente, se realizará la conversión de las variables categóricas, como Sex y Ethnicity, asegurando su correcto tratamiento en los modelos de clasificación. Las variables numéricas continuas Age, BMI, HbA1c, Fasting_Blood_Glucose serán normalizadas si fueran necesario. Además, se analiza el balance de clases de la variable objetivo diabetes y, si se detectara un desbalanceo significativo, se aplicarán técnicas de sobremuestreo como SMOTE para equilibrar la representación de las clases. El producto final de esta fase será un dataset limpio, codificado, balanceado y estructurado, preparado para el modelado predictivo y de segmentación.
En esta fase, se construirán modelos tanto no supervisados como supervisados. Para el aprendizaje no supervisado, se aplicarán algoritmos de clustering como k-means, k-medians y DBSCAN, buscando identificar grupos de individuos con características similares. Además, se utilizarán técnicas de reducción de dimensionalidad como PCA y SVD para simplificar el análisis y resaltar las variables más relevantes. Para el aprendizaje supervisado, se construirán modelos de clasificación como árboles de decisión, regresión logística y, potencialmente, SVM o redes neuronales. Los hiperparámetros de estos modelos se optimizarán mediante técnicas de validación cruzada para maximizar su rendimiento predictivo.
La evaluación se centrará en determinar la calidad y utilidad de los modelos construidos. Para los modelos no supervisados, se utilizarán métricas de cohesión y separación de clusters, como el índice de Silhouette y el índice de Davies-Bouldin, para cuantificar la calidad de las agrupaciones. Los resultados del clustering se analizarán para identificar patrones clínicamente relevantes. Para los modelos supervisados, se calcularán métricas como accuracy, precision, recall, F1-score y AUC-ROC, utilizando la matriz de confusión para evaluar el rendimiento en diferentes clases y detectar posibles sesgos. Además, se analizará la importancia relativa de las variables en la predicción de la variable objetivo.
La implementación se centrará en presentar los resultados de forma accesible y reproducible, documentando el flujo de trabajo en detalle y creando visualizaciones claras. Se elaborará un informe ejecutivo con los hallazgos clave, incluyendo factores de riesgo, perfiles de pacientes y rendimiento de los modelos. Además, se analizarán las limitaciones del dataset y los riesgos asociados al uso de los modelos, considerando aspectos éticos, de privacidad y posibles sesgos en los datos. Se discutirán las implicaciones de los resultados y se propondrán recomendaciones para futuras investigaciones.
En este apartado se describen y analizan los datos extraídos del open data donde se descarga a través del API el dataset y tal como hemos mencionado en el apartado anterior de comprensión de los datos, este dataset fue creado para estudiar los factores de riesgo asociados a la diabetes tipo 2, proporcionando una base de datos que contiene características demográficas, clínicas y de hábitos de vida de los pacientes.
La recopilación de los datos se realizó mediante registros médicos y encuestas clínicas, integrando las variables de la edad, género, antecedentes de hipertensión, enfermedades cardíacas, niveles de glucosa en sangre, niveles de colesterol, consumo de alcohol, entre otros. El propósito original de este conjunto de datos es apoyar el desarrollo de modelos predictivos, así como la detección de patrones asociados al riesgo de desarrollar diabetes.
El dataset está disponible públicamente bajo una , lo que permite su uso libre para fines académicos, de investigación y de desarrollo de proyectos de ciencia de datos. Su estructura, número de observaciones y la diversidad de variables permiten la aplicación de técnicas de aprendizaje automático y supervisadas como no supervisadas.
# Limpiar el entorno
rm(list = ls())
Instalación y carga de librerías necesarias para descargar los datos y realizar el estudio.
# Librerías necesarias para la conexión, manipulación y limpieza de datos
if (!require(httr)) install.packages("httr")
if (!require(jsonlite)) install.packages("jsonlite")
if (!require(utils)) install.packages("utils")
if (!require(dplyr)) install.packages("dplyr")
if (!require(tidyr)) install.packages("tidyr")
if (!require(knitr)) install.packages("knitr")
library(httr) # Conexión con APIs externas a través de dolicitudes http
library(jsonlite) # Conversión de objetos R a Json y viceversa
library(utils) # Importar y exportar datos
library(dplyr) # Preprocesar y limpiar datos
library(tidyr) # Reordenar y transformar estructuras de datos
library(knitr) # crear salidas de tablas, graficos, resultados..
# Librerías necesarias para la visualización de datos
if (!require(ggplot2)) install.packages("ggplot2")
if (!require(ggfortify)) install.packages("ggfortify")
if (!require(GGally)) install.packages("GGally")
if (!require(corrplot)) install.packages("corrplot")
if (!require(broom)) install.packages("broom")
if (!require(ggridges)) install.packages("ggridges")
if (!require(fmsb)) install.packages("fmsb")
if (!require(factoextra)) install.packages("factoextra")
library(ggplot2) # Gráficos (violin plot, boxplot, scatterplot...)
library(ggfortify) # Graficar objetos estadísticos complejos de forma automática
library(ggridges) # Gráficos density ridge plots
library(GGally) # Graficar ggpairs scatterplot matrices
library(corrplot) # Graficar matrices de correlación
library(broom) # Presentar resultados estadísticos en tabla
library(fmsb) # Gráficos de radar (spider plots)
library(factoextra) # Visualizar analisis multivariados (clustering)
# Librerías necesarias para los modelos de minería de datos
if (!require(dbscan)) install.packages("dbscan")
if (!require(stats)) install.packages("stats")
if (!require(cluster)) install.packages("cluster")
library(dbscan) # Implementación de DBSCAN
library(stats) # Métodos estadísticos y de distancia
library(cluster) # Algoritmos de clustering: k-means, k-medians y PAM
# Librerías necesarias para los modelos supervisados:
if (!require(rpart)) install.packages("rpart")
if (!require(rpart.plot)) install.packages("rpart.plot")
if (!require(randomForest)) install.packages("randomForest")
library(rpart) # Algoritmo de árboles de decisión
library(rpart.plot) # Visualización de árboles de decisión
library(randomForest) # Algoritmo de bosque aleatorio
# Librerías necesarias para la evaluación de modelos:
if (!require(caret)) install.packages("caret")
if (!require(e1071)) install.packages("e1071")
library(caret) # Evaluación y validación de modelos
library(e1071) # Métricas de evaluación y modelos supervisados como SVM
A posteriori, configuramos la API KAGGLE para la descarga del dataset.
# API Key
kaggle_api <- fromJSON("kaggle.json")
# Credenciales
Sys.setenv(KAGGLE_USERNAME = kaggle_api$username)
Sys.setenv(KAGGLE_KEY = kaggle_api$key)
Creamos un nuevo directorio / carpeta donde guardaremos el archivo CSV
# Crearemos un nuevo directorio donde guardar el archivo a descargar
if (!dir.exists("data")) {
dir.create("data")
}
Descargamos el archivo con el que vamos a trabajar con los sigueintes
comandos:
# Definimos el nombre del archivo zip y la ruta de extracción
zip_path <- "data/diabetes-prediction-dataset.zip"
# Descargamos el dataset en la carpeta creada solo si nO existe
if (!file.exists(zip_path)) {
system("kaggle datasets download -d marshalpatel3558/diabetes-prediction-dataset -p data")
} else {
message("El archivo fue descargado previamente.")
}
# Descomprimimos solo si no ha sido descomprimido antes
if (!file.exists(file.path("data", "diabetes_dataset.csv"))) {
unzip(zipfile = zip_path, exdir = "data")
} else {
message("El archivo ZIP fue descomprimido previamente..")
}
# Listar archivos de la carpeta data
list.files("data", recursive = TRUE)
## [1] "diabetes-prediction-dataset.zip" "diabetes_dataset.csv"
Exploración del conjunto de datos
#
df_origin <- read.csv("data/diabetes_dataset.csv")
# Dimensiones
cat("Número de observaciones:", nrow(df_origin), "\n")
## Número de observaciones: 10000
cat("Número de variables:", ncol(df_origin), "\n")
## Número de variables: 21
# Imprimimos las primeras filas
head(df_origin, n=10)
## X Age Sex Ethnicity BMI Waist_Circumference Fasting_Blood_Glucose HbA1c
## 1 0 58 Female White 35.8 83.4 123.9 10.9
## 2 1 48 Male Asian 24.1 71.4 183.7 12.8
## 3 2 34 Female Black 25.0 113.8 142.0 14.5
## 4 3 62 Male Asian 32.7 100.4 167.4 8.8
## 5 4 27 Female Asian 33.5 110.8 146.4 7.1
## 6 5 40 Female Asian 33.6 96.1 75.0 13.5
## 7 6 58 Male Black 33.2 100.0 97.7 13.3
## 8 7 38 Female Hispanic 26.9 105.0 80.2 10.9
## 9 8 42 Male White 27.0 115.4 83.9 7.0
## 10 9 30 Male White 24.0 74.6 72.0 14.0
## Blood_Pressure_Systolic Blood_Pressure_Diastolic Cholesterol_Total
## 1 152 114 197.8
## 2 103 91 261.6
## 3 179 104 261.0
## 4 176 118 183.4
## 5 122 97 203.2
## 6 170 90 152.3
## 7 131 80 199.8
## 8 121 83 154.0
## 9 132 118 280.9
## 10 146 83 250.0
## Cholesterol_HDL Cholesterol_LDL GGT Serum_Urate Physical_Activity_Level
## 1 50.2 99.2 37.5 7.2 Moderate
## 2 62.0 146.4 88.5 6.1 Moderate
## 3 32.1 164.1 56.2 6.9 Low
## 4 41.1 84.0 34.4 5.4 Low
## 5 53.9 92.8 81.9 7.4 Moderate
## 6 44.5 190.0 77.5 6.4 Low
## 7 77.9 73.4 52.1 4.7 High
## 8 69.7 122.2 72.0 5.6 Moderate
## 9 73.2 97.4 76.4 6.2 Low
## 10 53.3 170.7 14.5 6.9 High
## Dietary_Intake_Calories Alcohol_Consumption Smoking_Status
## 1 1538 Moderate Never
## 2 2653 Moderate Current
## 3 1684 Heavy Former
## 4 3796 Moderate Never
## 5 3161 Heavy Current
## 6 3460 None Never
## 7 3107 Moderate Never
## 8 2390 Heavy Current
## 9 3844 None Former
## 10 2230 Moderate Former
## Family_History_of_Diabetes Previous_Gestational_Diabetes
## 1 0 1
## 2 0 1
## 3 1 0
## 4 1 0
## 5 0 0
## 6 1 1
## 7 0 0
## 8 0 1
## 9 1 0
## 10 1 0
La variable X corresponde al index de cada uno de los registros y los valores decimales están redondeados a una décima con lo que perdemos precisión de cálculo.
Verificamos la estructura del juego de datos, el número de columnas y el tipo de datos que contiene, así como un ejemplo de los valores.
# Mostramos la estructura de los datos
str(df_origin)
## 'data.frame': 10000 obs. of 21 variables:
## $ X : int 0 1 2 3 4 5 6 7 8 9 ...
## $ Age : int 58 48 34 62 27 40 58 38 42 30 ...
## $ Sex : chr "Female" "Male" "Female" "Male" ...
## $ Ethnicity : chr "White" "Asian" "Black" "Asian" ...
## $ BMI : num 35.8 24.1 25 32.7 33.5 33.6 33.2 26.9 27 24 ...
## $ Waist_Circumference : num 83.4 71.4 113.8 100.4 110.8 ...
## $ Fasting_Blood_Glucose : num 124 184 142 167 146 ...
## $ HbA1c : num 10.9 12.8 14.5 8.8 7.1 13.5 13.3 10.9 7 14 ...
## $ Blood_Pressure_Systolic : int 152 103 179 176 122 170 131 121 132 146 ...
## $ Blood_Pressure_Diastolic : int 114 91 104 118 97 90 80 83 118 83 ...
## $ Cholesterol_Total : num 198 262 261 183 203 ...
## $ Cholesterol_HDL : num 50.2 62 32.1 41.1 53.9 44.5 77.9 69.7 73.2 53.3 ...
## $ Cholesterol_LDL : num 99.2 146.4 164.1 84 92.8 ...
## $ GGT : num 37.5 88.5 56.2 34.4 81.9 77.5 52.1 72 76.4 14.5 ...
## $ Serum_Urate : num 7.2 6.1 6.9 5.4 7.4 6.4 4.7 5.6 6.2 6.9 ...
## $ Physical_Activity_Level : chr "Moderate" "Moderate" "Low" "Low" ...
## $ Dietary_Intake_Calories : int 1538 2653 1684 3796 3161 3460 3107 2390 3844 2230 ...
## $ Alcohol_Consumption : chr "Moderate" "Moderate" "Heavy" "Moderate" ...
## $ Smoking_Status : chr "Never" "Current" "Former" "Never" ...
## $ Family_History_of_Diabetes : int 0 0 1 1 0 1 0 0 1 1 ...
## $ Previous_Gestational_Diabetes: int 1 1 0 0 0 1 0 1 0 0 ...
Descripción de las variables contenidas en el fichero y contruimos un diccionario de datos utilizando la documentación auxiliar.
Partimos de los datos originales y observamos que las variables identificadas como numéricas en realidad son categóricas o binomiales, por concepto. Por ejemplo, “Family_History_of_Diabetes” y “Previous_Gestational_Diabetes” sólo tienen 2 resultados posibles. Al igual, ocurre con la variable “Sex” definida como categórica cuando por concepto es binomial.
# Creamos una copia del original antes de tratar los datos
df <- df_origin
# Eliminamos la primera columna (índice=X) que no necesitamos
df <- df[, -1]
# Dividimos el dataset en v. numéricas, categóricas y binomiales
df_num <- df[sapply(df, is.numeric)]
cat("Variables numéricas:\n")
## Variables numéricas:
print(names(df_num))
## [1] "Age" "BMI"
## [3] "Waist_Circumference" "Fasting_Blood_Glucose"
## [5] "HbA1c" "Blood_Pressure_Systolic"
## [7] "Blood_Pressure_Diastolic" "Cholesterol_Total"
## [9] "Cholesterol_HDL" "Cholesterol_LDL"
## [11] "GGT" "Serum_Urate"
## [13] "Dietary_Intake_Calories" "Family_History_of_Diabetes"
## [15] "Previous_Gestational_Diabetes"
df_cat <- df[sapply(df, is.character)]
cat("\nVariables categóricas:\n")
##
## Variables categóricas:
print(names(df_cat))
## [1] "Sex" "Ethnicity"
## [3] "Physical_Activity_Level" "Alcohol_Consumption"
## [5] "Smoking_Status"
#Identificamos las variables binarias
binarias <- names(df_num)[sapply(df_num, function(x) length(unique(x)) == 2)]
cat("\nVariables binarias detectadas:\n")
##
## Variables binarias detectadas:
print(binarias)
## [1] "Family_History_of_Diabetes" "Previous_Gestational_Diabetes"
A modo de resumen del análisis exploratorio, concluímos en la PRA1 que el conjunto de datos original no presenta valores nulos ni espacios en blanco, y referente, a las variables numéricas muestran distribuciones cercanas a uniformes o levemente asimétricas, lo que indica que los datos están bien distribuidos a lo largo de sus rangos. Es decir, no se observa distribuciones extremadamente sesgadas, y no existe grandes concentraciones de valores ni presencia de masiva de outliers, lo que sugiere calidad en los datos. Además, presentan una correlación lineal o nula, por tanto, no existe relaciones fuertes ni colinealidad significativa.
Lo que significa que las variables numéricas presentan distribuciones homogéneas, sin la existencia de dominios dominantes ni asimetrías marcadas. Aunque se indica una alta calidad y ausencia de sesgos o valores atípicos extremos, implica que las diferencias entre observaciones suelen ser pequeñas y están repartidas de manera uniforme en todo el espacio de datos. Como consecuencia, los algoritmos de clustering tradicionales, como k-means o DBSCAN, tienden a formar agrupaciones basadas en proximidad global y pueden no ser capaces de detectar subgrupos clínicos diferenciados por pequeños cambios en una o pocas variables.
Esta limitación se traduce en la obtención de clusters equilibrados pero difícilmente interpretables clínicamente, ya que ninguna variable destaca lo suficiente como para definir de forma robusta un perfil de riesgo específico. Además, técnicas como DBSCAN pueden no identificar áreas de densidad diferenciada, y el análisis de componentes principales revela que la varianza está muy repartida, dificultando la visualización y extracción de patrones relevantes.
Para abordar esta casuística, se han aplicarán técnicas multivariantes (PCA, clustering jerárquico), evaluan posibles scores compuestos y se realizarán análisis post-clustering de la distribución de variables en cada grupo. Pese a estos esfuerzos, la homogeneidad observada sugiere la necesidad de explorar nuevas variables clínicas, mayor tamaño muestral o incluso la integración de datos longitudinales para poder identificar patrones de agrupamiento clínicamente relevantes.
Por tanto, aunque la ausencia de asimetrías y outliers facilita el análisis desde el punto de vista estadístico, plantea el reto de la escasa diferenciación entre subgrupos, lo que condiciona la robustez de las conclusiones y la aplicabilidad clínica de los perfiles extraídos.
Antes de entrar en la agrupación k-means hay que puntualizar que los algoritmos de clustering como k-means y k-medians están diseñados para trabajar exclusivamente con variables numéricas, ya que tanto la media k-means como la mediana k-medians no están definidas para datos categóricos. Por este motivo, no es posible incluir variables categóricas directamente en el proceso de agrupamiento con estos métodos.
Para realizar un análisis de clustering, existen varias alternativas. Una opción es transformar las variables categóricas en variables numéricas mediante técnicas como el one-hot encoding variables dummy o el label encoding (solo recomendable si las categorías tienen un orden natural). Otra posibilidad es utilizar una métrica de distancia que acepte datos mixtos, como la distancia de Gower, y aplicar posteriormente el algoritmo PAM sobre la matriz de distancias resultante. Finalmente, existen algoritmos específicamente diseñados para el tratamiento de datos mixtos, como k-modes y k-prototypes de la libreria klaR, que permiten agrupar observaciones de las variables numéricas y categóricas.
En este estudio, siguiendo las limitaciones comentadas y para garantizar la coherencia y validez del análisis, se utilizan exclusivamente las variables numéricas para el diseño de los clústeres.
Para el preprocesamiento de las variables numéricas se evaluaron dos técnicas de normalización: Min-Max y z-score. La normalización Min-Max transforma los valores de cada variable al rango [0,1], garantizando que ninguna domine el análisis por diferencia de escala. La estandarización z-score, por su parte, centra los valores en cero y ajusta la dispersión a una desviación estándar unitaria, siendo más robusta en presencia de outliers.
En nuestro caso, el análisis exploratorio mostró que las variables numéricas presentan distribuciones homogéneas y carecen de outliers significativos o asimetrías marcadas. Por ello, la normalización Min-Max resulta apropiada, pues permite interpretar los resultados en términos relativos al rango de cada variable y facilita la visualización e interpretación clínica de los clusters. Además, para reforzar la solidez metodológica, se realizó una comparación preliminar con la estandarización z-score, comprobando que las agrupaciones obtenidas son consistentes entre ambos métodos.
Así, la elección de Min-Max como técnica principal se justifica por la estructura y calidad del dataset, asegurando una contribución equilibrada de todas las variables al análisis de clustering
# Normalizamos las variables numéricas:
df_norm <- as.data.frame(lapply(df_num, function(x) (x - min(x)) / (max(x) - min(x))))
Las variables numéricas del dataset presentan rangos y varianzas muy distintos, lo que puede hacer que el clustering y el análisis PCA estén dominados por la variable con mayor escala. Para evitar este sesgo y permitir que todas las variables contribuyan equitativamente a la agrupación, se aplica una normalización Min-Max que también Z-score
# Determinamos el número óptimo de clusters
# Método del codo (inercia total intra-cluster)
fviz_nbclust(df_norm, kmeans, method = "wss") + labs(title = "Método del codo")
# Método de la silueta
fviz_nbclust(df_norm, kmeans, method = "silhouette") + labs(title = "Silhouette")
### Método del codo Para determinar el número óptimo de clusters
utilizaremos el método del codo donde analizamos la suma de distancias
cuadradas intra-cluster (WSS) e identificamos, después de la reducción
de WSS deja de ser significativa al incrementar k, la cuál se observa
que en k=4 es el número ótimo de clusters, aunque mejora a partir de
k=3.
Aplicamos el método silhoutte que evalúa la calidad de la agrupación midiendo, para cada observación, lo bien asignada que está a su propio cluster en comparación con los demás. El valor medio de silueta varía entre -1 y 1: cuanto más alto, mejor separados y cohesionados están los clusters. El número óptimo de clusters se selecciona donde la media de la silueta es máxima. Observamos que el valor máximo también se alcanza en k = 4, confirmando que el número óptimo de clusters.
En resumen, la convergencia entre los métodos del codo y la silueta en k = 4 respalda la decisión de emplear cuatro clusters para la segmentación, asegurando una buena separación entre grupos y una reducción significativa de la variabilidad intra-cluster
#Aplicamos el método k-means con k=3 a los datos originales y normalizados
set.seed(123)
k3 <- 3 # probamos con 3 clusteres
# K-means sobre datos originales
kmeans3_orig <- kmeans(df_num, centers = k3, nstart = 25)
# K-means sobre datos normalizados
kmeans3_norm <- kmeans(df_norm, centers = k3, nstart = 25)
El parámetro nstart = 25 en la función kmeans() especifica que el algoritmo de agrupamiento debe ejecutarse 25 veces, cada una con una inicialización aleatoria diferente de los centroides. De este modo, se selecciona como resultado final la partición que minimiza la suma de distancias intra-cluster.
#Aplicamos el método k-means con k=4 a los datos originales y normalizados
set.seed(123)
k4 <- 4 # probamos con 4 clusteres
# K-means sobre datos originales
kmeans4_orig <- kmeans(df_num, centers = k4, nstart = 25)
# K-means sobre datos normalizados
kmeans4_norm <- kmeans(df_norm, centers = k4, nstart = 25)
k-means es un algoritmo de agrupamiento no supervisado que busca dividir un conjunto de datos en k grupos (clusters), de forma que los elementos de cada grupo sean lo más similares posible entre sí y lo más diferentes posible de los otros grupos, de manera que cada observación pertenezca al cluster con el centroide más cercano. Repite ciclos de asignación y actualización de centroides hasta lograr una partición estable para la exploración de patrones y la identificación de perfiles.
# Creamos una función para resumir métricas clave:
cluster_sizes <- function(obj, label, caption) {
sizes <- as.data.frame(t(obj$size))
colnames(sizes) <- paste0("Cluster ", seq_along(obj$size))
rownames(sizes) <- label
kable(sizes, caption = caption)
}
cluster_sizes(kmeans3_orig, "Original, k=3", "Tamaños de clusters (Original, k=3)")
Cluster 1 | Cluster 2 | Cluster 3 | |
---|---|---|---|
Original, k=3 | 3387 | 3278 | 3335 |
cluster_sizes(kmeans3_norm, "Normalizado, k=3", "Tamaños de clusters (Normalizado, k=3)")
Cluster 1 | Cluster 2 | Cluster 3 | |
---|---|---|---|
Normalizado, k=3 | 2536 | 2629 | 4835 |
cluster_sizes(kmeans4_orig, "Original, k=4", "Tamaños de clusters (Original, k=4)")
Cluster 1 | Cluster 2 | Cluster 3 | Cluster 4 | |
---|---|---|---|---|
Original, k=4 | 2394 | 2635 | 2502 | 2469 |
cluster_sizes(kmeans4_norm, "Normalizado, k=4", "Tamaños de clusters (Normalizado, k=4)")
Cluster 1 | Cluster 2 | Cluster 3 | Cluster 4 | |
---|---|---|---|---|
Normalizado, k=4 | 2536 | 2441 | 2394 | 2629 |
# Tabla de centroides para cada modelo
centroids_kmeans <- function(obj, label, caption) {
centers <- as.data.frame(obj$centers)
rownames(centers) <- paste("Cluster", seq_len(nrow(centers)))
kable(centers, digits = 3, caption = caption)
}
centroids_kmeans(kmeans3_orig, "Original, k=3", "Centroides (Original, k=3)")
Age | BMI | Waist_Circumference | Fasting_Blood_Glucose | HbA1c | Blood_Pressure_Systolic | Blood_Pressure_Diastolic | Cholesterol_Total | Cholesterol_HDL | Cholesterol_LDL | GGT | Serum_Urate | Dietary_Intake_Calories | Family_History_of_Diabetes | Previous_Gestational_Diabetes | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Cluster 1 | 44.451 | 29.586 | 95.021 | 134.334 | 9.548 | 134.788 | 89.610 | 226.006 | 55.011 | 133.898 | 55.366 | 5.512 | 3567.231 | 0.513 | 0.523 |
Cluster 2 | 44.710 | 29.289 | 94.834 | 134.844 | 9.454 | 133.360 | 89.592 | 224.070 | 55.095 | 134.261 | 55.144 | 5.503 | 1913.956 | 0.491 | 0.513 |
Cluster 3 | 44.704 | 29.374 | 94.534 | 135.159 | 9.520 | 134.320 | 89.474 | 225.388 | 54.954 | 134.908 | 54.991 | 5.495 | 2719.238 | 0.517 | 0.513 |
centroids_kmeans(kmeans3_norm, "Normalizado, k=3", "Centroides (Normalizado, k=3)")
Age | BMI | Waist_Circumference | Fasting_Blood_Glucose | HbA1c | Blood_Pressure_Systolic | Blood_Pressure_Diastolic | Cholesterol_Total | Cholesterol_HDL | Cholesterol_LDL | GGT | Serum_Urate | Dietary_Intake_Calories | Family_History_of_Diabetes | Previous_Gestational_Diabetes | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Cluster 1 | 0.497 | 0.510 | 0.491 | 0.493 | 0.502 | 0.488 | 0.507 | 0.503 | 0.504 | 0.493 | 0.498 | 0.511 | 0.489 | 0.000 | 1 |
Cluster 2 | 0.508 | 0.506 | 0.493 | 0.491 | 0.499 | 0.499 | 0.500 | 0.501 | 0.496 | 0.491 | 0.508 | 0.494 | 0.510 | 1.000 | 1 |
Cluster 3 | 0.503 | 0.508 | 0.500 | 0.505 | 0.501 | 0.499 | 0.498 | 0.500 | 0.501 | 0.498 | 0.500 | 0.499 | 0.495 | 0.505 | 0 |
centroids_kmeans(kmeans4_orig, "Original, k=4", "Centroides (Original, k=4)")
Age | BMI | Waist_Circumference | Fasting_Blood_Glucose | HbA1c | Blood_Pressure_Systolic | Blood_Pressure_Diastolic | Cholesterol_Total | Cholesterol_HDL | Cholesterol_LDL | GGT | Serum_Urate | Dietary_Intake_Calories | Family_History_of_Diabetes | Previous_Gestational_Diabetes | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Cluster 1 | 44.448 | 29.353 | 94.870 | 133.279 | 9.459 | 134.553 | 89.162 | 226.034 | 54.755 | 133.887 | 54.458 | 5.506 | 3070.184 | 0.525 | 0.524 |
Cluster 2 | 44.581 | 29.332 | 94.429 | 136.073 | 9.555 | 134.226 | 89.469 | 225.765 | 54.917 | 134.075 | 55.475 | 5.508 | 2442.810 | 0.510 | 0.508 |
Cluster 3 | 44.854 | 29.321 | 95.070 | 134.547 | 9.438 | 133.316 | 89.830 | 223.903 | 55.112 | 135.092 | 55.092 | 5.503 | 1817.633 | 0.485 | 0.514 |
Cluster 4 | 44.593 | 29.672 | 94.842 | 135.076 | 9.575 | 134.580 | 89.763 | 224.963 | 55.290 | 134.356 | 55.607 | 5.497 | 3681.764 | 0.509 | 0.520 |
centroids_kmeans(kmeans4_norm, "Normalizado, k=4", "Centroides (Normalizado, k=4)")
Age | BMI | Waist_Circumference | Fasting_Blood_Glucose | HbA1c | Blood_Pressure_Systolic | Blood_Pressure_Diastolic | Cholesterol_Total | Cholesterol_HDL | Cholesterol_LDL | GGT | Serum_Urate | Dietary_Intake_Calories | Family_History_of_Diabetes | Previous_Gestational_Diabetes | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Cluster 1 | 0.497 | 0.510 | 0.491 | 0.493 | 0.502 | 0.488 | 0.507 | 0.503 | 0.504 | 0.493 | 0.498 | 0.511 | 0.489 | 0 | 1 |
Cluster 2 | 0.494 | 0.506 | 0.511 | 0.506 | 0.503 | 0.495 | 0.494 | 0.496 | 0.498 | 0.497 | 0.510 | 0.494 | 0.493 | 1 | 0 |
Cluster 3 | 0.511 | 0.511 | 0.490 | 0.505 | 0.500 | 0.503 | 0.502 | 0.504 | 0.503 | 0.500 | 0.491 | 0.504 | 0.496 | 0 | 0 |
Cluster 4 | 0.508 | 0.506 | 0.493 | 0.491 | 0.499 | 0.499 | 0.500 | 0.501 | 0.496 | 0.491 | 0.508 | 0.494 | 0.510 | 1 | 1 |
En datos normalizados, los centroides indican la posición relativa de 0 a 1 respecto al rango de cada variable. Valores cercanos a 1 representan el extremo superior, valores cercanos a 0, el extremo inferior.
Para k=3: Cluster 1 y Cluster 2: Muestran valores muy similares en la mayoría de las variables, lo que sugiere perfiles metabólicos bastante homogéneos. Excepto las variables binomiales como antecedentes familiares y diabetes gestacional que se observa el valor 1 en “Previous_Gestational_Diabetes” solo en algunos clusters, evidenciando que sólo afectará a sexo correspondiente a mujer y a un subgrupo de éste. Cluster 3: También comparte perfiles similares, aunque puede diferenciarse ligeramente en variables como ingesta calórica o antecedentes familiares.
Para k = 4: Los cuatro clusters presentan valores normalizados muy próximos a la media, alrededor de 0.5, lo que indica baja variabilidad entre los grupos cuando se consideran todas las variables. Las diferencias más notables aparecen en variables binarias, por ejemplo en “Family_History_of_Diabetes” y “Previous_Gestational_Diabetes” muestran 1 en algunos clusters y 0 en otros, lo que señala que algunos grupos agrupan principalmente individuos con o sin ese antecedente, y como hemos mencionado anteriormente solo podrá afectar a mujeres y dentro del subgrupo mujer a otro grupo más pequeño.
El clustering sobre datos normalizados con k=4 ha permitido diferenciar mejor los perfiles según variables binomiales, aunque las diferencias en las variables cuantitativas siguen siendo sutiles. La mejor práctica en este caso sería desestimar las varibles binomiales del estudio de centroides k-means.
Los centroides representan los valores medios reales de cada variable en cada cluster. k = 3 y k = 4 Perfiles muy similares entre clusters: Las medias de edad, IMC, glucosa, colesterol, etc. apenas varían entre grupos, lo que refuerza que el clustering está dominado por una única variable de alto rango y las diferencias reales entre grupos son mínimas. Ingesta calórica (“Dietary_Intake_Calories”) es la variable donde más se aprecia diferencia, probablemente debido a su mayor rango, afectando la agrupación y distribución de los clusters.
En variables binomiales, los valores medios, por ejemplo: 0.525, indican la proporción de individuos con ese rasgo en el cluster, pero de nuevo las diferencias entre clusters son poco apreciables.
# Gráfico k=3, datos originales
fviz_cluster(
kmeans3_orig, data = df_num,
ellipse.type = "euclid",
ellipse.level = 0.95,
ellipse.alpha = 0.15,
ellipse.linewidth = 2,
star.plot = FALSE,
show.clust.cent = TRUE,
labelsize = 0,
ggtheme = theme_minimal(),
main = "Clusters k=3 con datos originales"
)
# Gráfico k=3, datos normalizados
fviz_cluster(
kmeans3_norm, data = df_norm,
ellipse.type = "euclid",
ellipse.level = 0.95,
ellipse.alpha = 0.15,
ellipse.linewidth = 2,
star.plot = FALSE,
show.clust.cent = TRUE,
labelsize = 0,
ggtheme = theme_minimal(),
main = "Clusters k=3 con datos normalizados"
)
# Gráfico k=4, datos originales
fviz_cluster(
kmeans4_orig, data = df_num,
ellipse.type = "euclid",
ellipse.level = 0.95,
ellipse.alpha = 0.15,
ellipse.linewidth = 2,
star.plot = FALSE,
show.clust.cent = TRUE,
labelsize = 0,
ggtheme = theme_minimal(),
main = "Clusters k=4 con datos originales"
)
# Gráfico k=4, datos normalizados
fviz_cluster(
kmeans4_norm, data = df_norm,
ellipse.type = "euclid",
ellipse.level = 0.95,
ellipse.alpha = 0.15,
ellipse.linewidth = 2,
star.plot = FALSE,
show.clust.cent = TRUE,
labelsize = 0,
ggtheme = theme_minimal(),
main = "Clusters k=4 con datos normalizados"
)
Datos originales: Los clusters aparecen muy solapados, con límites difusos y sin una separación clara entre ellos. Esto ocurre porque las variables originales pueden tener distintas escalas y rangos, por lo que k-means puede verse afectado por la dominancia de aquellas variables con mayor varianza. El resultado es una distribución muy homogénea y dispersa, donde los límites entre los grupos no son fácilmente identificables.
Datos normalizados: Los clusters se presentan mucho mejor definidos, con formas más compactas y elipses claramente diferenciadas. La normalización iguala la influencia de todas las variables, permitiendo que se detecte agrupaciones más balanceadas. Los contornos de los clusters se ven claramente, lo que indica una mayor capacidad de segmentación por parte del modelo.
library(ggfortify)
# k=3 originales
autoplot(kmeans3_orig, data = df_num, frame = TRUE) +
ggtitle("Clusters k=3 con datos originales (ggplot2)")
# k=3 normalizados
autoplot(kmeans3_norm, data = df_norm, frame = TRUE) +
ggtitle("Clusters k=3 con datos normalizados (ggplot2)")
# k=4 originales
autoplot(kmeans4_orig, data = df_num, frame = TRUE) +
ggtitle("Clusters k=4 con datos originales (ggplot2)")
# k=4 normalizados
autoplot(kmeans4_norm, data = df_norm, frame = TRUE) +
ggtitle("Clusters k=4 con datos normalizados (ggplot2)")
Datos originales: Las proyecciones principales (PC1, PC2) muestran una
distribución prácticamente lineal o rectangular, donde la varianza
principal está dominada por una sola componente. Los clusters se
reparten siguiendo las direcciones de la varianza, pero las diferencias
son difíciles de interpretar visualmente.
Datos normalizados: En este caso, los clusters se agrupan en zonas bien diferenciadas del espacio de componentes principales. Observamos grupos separados, reflejando que la normalización mejora mucho la discriminación y la asignación de cada observación al cluster óptimo.
En resumen, K-means es muy sensible a la escala de las variables. Si no se normalizan los datos, el clustering puede reflejar principalmente la variabilidad de las variables con mayor rango o varianza, ignorando el resto. La normalización iguala la contribución de cada variable, lo que permite obtener agrupaciones más equilibradas y significativas. Visualmente, los clusters normalizados son más interpretables, y bien definidos, permitiendo extraer conclusiones más fiables sobre los perfiles existentes.
Se ha identificado distintos perfiles de pacientes en relación con el riesgo de diabetes y enfermedades metabólicas asociadas. Los resultados, de las tablas de centroides y tamaños de los clusters, muestran diferencias claras entre los grupos formados, permitiendo interpretar la relevancia clínica de cada uno.
Para 4 clusters con datos normalizados, se observa en los centroides principales:En los datos originales (no normalizados), los centroides muestran diferencias más claras en variables como la glucosa basal (por ejemplo, de 133 a 136 mg/dL entre clusters), IMC (de 29{,}3 a 29{,}7), y calorías ingeridas (de 1817 a 3681 kcal), reflejando la variabilidad clínica real de la muestra. Así, el (original, \(k=4\)) presenta el valor más alto de calorías ingeridas (3681 kcal) y de IMC (29{,}7), lo que sugiere una agrupación de pacientes con alto riesgo metabólico asociado a malos hábitos alimenticios. Por su parte, el agrupa a pacientes con valores intermedios, pero con menor consumo calórico y menor frecuencia de antecedentes familiares.
Estas diferencias permiten identificar perfiles de riesgo:La información obtenida de los centroides, junto con los tamaños de cada cluster, facilita la toma de decisiones clínicas al permitir orientar la prevención y seguimiento hacia los subgrupos más vulnerables de la población. En conclusión, los clusters identificados mediante permiten reconocer subpoblaciones clínicas bien diferenciadas, donde la glucosa basal, la HbA1c, el IMC, los antecedentes familiares y el consumo calórico son las variables más determinantes, justificando así la importancia de un enfoque personalizado en la prevención y manejo de la diabetes y el riesgo cardiovascular.
En este ejercicio y los siguientes relacionados con agrupamientos, se va excluir la variable ‘Previous_Gestational_Diabetes’ del análisis global del clustering, al tratarse de una variable exclusiva del subgrupo de mujeres, lo que podría inducir un sesgo de género y dificultar la interpretación de los clusters en una muestra compuesta por mujeres y hombres.
# 'Previous_Gestational_Diabetes'
df_num2 <- df_num[, !(names(df_num) == "Previous_Gestational_Diabetes")]
cat("Variables numéricas:\n")
## Variables numéricas:
print(names(df_num2))
## [1] "Age" "BMI"
## [3] "Waist_Circumference" "Fasting_Blood_Glucose"
## [5] "HbA1c" "Blood_Pressure_Systolic"
## [7] "Blood_Pressure_Diastolic" "Cholesterol_Total"
## [9] "Cholesterol_HDL" "Cholesterol_LDL"
## [11] "GGT" "Serum_Urate"
## [13] "Dietary_Intake_Calories" "Family_History_of_Diabetes"
En este ejercicio se utiliza el algoritmo kmedians, un tipo de algoritmo de aprendizaje no supervisado similar al algoritmo de agrupación por K-means, pero difiere en cómo se calcula el punto central de cada cluster. Mientras que Kmeans utiliza el valor medio de los puntos de datos de un cluster para determinar su centroide, Kmedians utiliza el valor de la mediana. Esta distinción hace que la agrupación por Kmedians sea más robusta ante valores atípicos en los datos, ya que la mediana se ve menos afectada por los valores extremos que la media.
# Libreria
if (!require(Kmedians)) install.packages("Kmedians")
library(Kmedians)
# Datos originales df_num2 excluyendo la variable binaria
set.seed(123)
kmedians3_orig <- Kmedians(df_num2, nclust = 3)
kmedians4_orig <- Kmedians(df_num2, nclust = 4)
# Normalización Min-Max
df_norm2 <- as.data.frame(lapply(df_num2, function(x) (x - min(x)) / (max(x) - min(x))))
# Datos normalizados df_num2 excluyendo la variable binaria
set.seed(123)
kmedians3_norm <- Kmedians(df_norm2, nclust = 3)
kmedians4_norm <- Kmedians(df_norm2, nclust = 4)
# Función para tamaños de clusters con Kmedians
cluster_sizes_kmedians <- function(obj, label, caption) {
sizes <- as.vector(table(obj$allresults$cluster))
df <- as.data.frame(t(sizes))
colnames(df) <- paste0("Cluster ", seq_along(sizes))
rownames(df) <- label
kable(df, caption = caption)
}
# Uso con tus modelos:
cluster_sizes_kmedians(kmedians3_orig, "Original, k=3", "Tamaños de clusters (K-medians, Original, k=3)")
Cluster 1 | Cluster 2 | Cluster 3 | |
---|---|---|---|
Original, k=3 | 3262 | 3337 | 3401 |
cluster_sizes_kmedians(kmedians3_norm, "Normalizado, k=3", "Tamaños de clusters (K-medians, Normalizado, k=3)")
Cluster 1 | Cluster 2 | Cluster 3 | |
---|---|---|---|
Normalizado, k=3 | 2439 | 5070 | 2491 |
cluster_sizes_kmedians(kmedians4_orig, "Original, k=4", "Tamaños de clusters (K-medians, Original, k=4)")
Cluster 1 | Cluster 2 | Cluster 3 | Cluster 4 | |
---|---|---|---|---|
Original, k=4 | 2526 | 2647 | 2411 | 2416 |
cluster_sizes_kmedians(kmedians4_norm, "Normalizado, k=4", "Tamaños de clusters (K-medians, Normalizado, k=4)")
Cluster 1 | Cluster 2 | Cluster 3 | Cluster 4 | |
---|---|---|---|---|
Normalizado, k=4 | 1593 | 1581 | 5070 | 1756 |
# Función centroide
centroids_kmedians <- function(obj, base_datos, caption) {
centers <- as.data.frame(obj$bestresult$centers)
colnames(centers) <- colnames(base_datos) # Asigna los nombres reales de tus variables
rownames(centers) <- paste("Cluster", seq_len(nrow(centers)))
kable(centers, digits = 3, caption = caption)
}
centroids_kmedians(kmedians3_orig, df_num2, "Centroides K-medians, Original, k=3")
Age | BMI | Waist_Circumference | Fasting_Blood_Glucose | HbA1c | Blood_Pressure_Systolic | Blood_Pressure_Diastolic | Cholesterol_Total | Cholesterol_HDL | Cholesterol_LDL | GGT | Serum_Urate | Dietary_Intake_Calories | Family_History_of_Diabetes | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Cluster 1 | 44.653 | 29.210 | 95.009 | 134.846 | 9.480 | 133.753 | 89.666 | 224.643 | 55.027 | 134.710 | 55.522 | 5.507 | 1914.385 | 0.491 |
Cluster 2 | 44.595 | 29.350 | 94.141 | 135.741 | 9.544 | 134.427 | 89.474 | 224.963 | 55.065 | 134.765 | 55.049 | 5.498 | 2710.624 | 0.519 |
Cluster 3 | 44.634 | 29.549 | 94.738 | 134.346 | 9.555 | 134.966 | 89.500 | 225.919 | 55.225 | 134.030 | 55.046 | 5.483 | 3564.267 | 0.522 |
centroids_kmedians(kmedians3_norm, df_norm2, "Centroides K-medians, Normalizado, k=3")
Age | BMI | Waist_Circumference | Fasting_Blood_Glucose | HbA1c | Blood_Pressure_Systolic | Blood_Pressure_Diastolic | Cholesterol_Total | Cholesterol_HDL | Cholesterol_LDL | GGT | Serum_Urate | Dietary_Intake_Calories | Family_History_of_Diabetes | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Cluster 1 | 0.521 | 0.499 | 0.470 | 0.746 | 0.491 | 0.478 | 0.517 | 0.473 | 0.510 | 0.501 | 0.463 | 0.499 | 0.465 | 0 |
Cluster 2 | 0.501 | 0.505 | 0.502 | 0.497 | 0.500 | 0.496 | 0.497 | 0.499 | 0.497 | 0.493 | 0.509 | 0.494 | 0.502 | 1 |
Cluster 3 | 0.487 | 0.520 | 0.510 | 0.256 | 0.512 | 0.510 | 0.492 | 0.534 | 0.498 | 0.491 | 0.526 | 0.515 | 0.517 | 0 |
centroids_kmedians(kmedians4_orig, df_num2, "Centroides K-medians, Original, k=4)")
Age | BMI | Waist_Circumference | Fasting_Blood_Glucose | HbA1c | Blood_Pressure_Systolic | Blood_Pressure_Diastolic | Cholesterol_Total | Cholesterol_HDL | Cholesterol_LDL | GGT | Serum_Urate | Dietary_Intake_Calories | Family_History_of_Diabetes | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Cluster 1 | 44.907 | 29.273 | 95.085 | 135.394 | 9.493 | 133.503 | 90.029 | 223.890 | 55.014 | 135.206 | 55.323 | 5.494 | 1823.199 | 0.490 |
Cluster 2 | 44.551 | 29.358 | 94.759 | 135.903 | 9.560 | 134.193 | 89.636 | 226.629 | 54.860 | 134.438 | 55.689 | 5.508 | 2446.907 | 0.507 |
Cluster 3 | 44.626 | 29.558 | 94.973 | 134.969 | 9.550 | 134.682 | 89.684 | 225.073 | 55.248 | 134.582 | 55.079 | 5.476 | 3689.262 | 0.512 |
Cluster 4 | 44.679 | 29.399 | 94.869 | 133.026 | 9.457 | 134.809 | 89.162 | 225.835 | 54.771 | 134.364 | 54.535 | 5.514 | 3084.346 | 0.528 |
centroids_kmedians(kmedians4_norm, df_norm2, "Centroides K-medians, Normalizado, k=4")
Age | BMI | Waist_Circumference | Fasting_Blood_Glucose | HbA1c | Blood_Pressure_Systolic | Blood_Pressure_Diastolic | Cholesterol_Total | Cholesterol_HDL | Cholesterol_LDL | GGT | Serum_Urate | Dietary_Intake_Calories | Family_History_of_Diabetes | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Cluster 1 | 0.458 | 0.524 | 0.425 | 0.624 | 0.42 | 0.288 | 0.556 | 0.431 | 0.602 | 0.533 | 0.419 | 0.563 | 0.408 | 0 |
Cluster 2 | 0.669 | 0.464 | 0.492 | 0.653 | 0.53 | 0.690 | 0.502 | 0.489 | 0.419 | 0.491 | 0.507 | 0.441 | 0.507 | 0 |
Cluster 3 | 0.501 | 0.505 | 0.502 | 0.497 | 0.50 | 0.496 | 0.497 | 0.499 | 0.497 | 0.493 | 0.509 | 0.494 | 0.502 | 1 |
Cluster 4 | 0.396 | 0.538 | 0.548 | 0.248 | 0.55 | 0.508 | 0.459 | 0.583 | 0.491 | 0.466 | 0.553 | 0.516 | 0.554 | 0 |
# Grafica k = 3 datos originales
fviz_cluster(
list(data = df_num2, cluster = kmedians3_orig$allresults$cluster),
ellipse.type = "euclid",
ellipse.level = 0.95,
ellipse.alpha = 0.15,
ellipse.linewidth = 2,
star.plot = FALSE,
show.clust.cent = TRUE,
labelsize = 0,
ggtheme = theme_minimal(),
main = "Clusters k=3 con datos originales K-medians"
)
# Grafica k = 3 datos normalizados
fviz_cluster(
list(data = df_norm2, cluster = kmedians3_norm$allresults$cluster),
ellipse.type = "euclid",
ellipse.level = 0.95,
ellipse.alpha = 0.15,
ellipse.linewidth = 2,
star.plot = FALSE,
show.clust.cent = TRUE,
labelsize = 0,
ggtheme = theme_minimal(),
main = "Clusters k=3 con datos normalizados K-medians"
)
# Grafica k = 4 datos originales
fviz_cluster(
list(data = df_num2, cluster = kmedians4_orig$allresults$cluster),
ellipse.type = "euclid",
ellipse.level = 0.95,
ellipse.alpha = 0.15,
ellipse.linewidth = 2,
star.plot = FALSE,
show.clust.cent = TRUE,
labelsize = 0,
ggtheme = theme_minimal(),
main = "Clusters k=4 con datos originales K-medians"
)
# Gráfica k = 4 datos normalizados
fviz_cluster(
list(data = df_norm2, cluster = kmedians4_norm$allresults$cluster),
ellipse.type = "euclid",
ellipse.level = 0.95,
ellipse.alpha = 0.15,
ellipse.linewidth = 2,
star.plot = FALSE,
show.clust.cent = TRUE,
labelsize = 0,
ggtheme = theme_minimal(),
main = "Clusters k=4 con datos normalizados K-medians"
)
Los objetos devueltos por algoritmos de clustering como kmeans, pam o hclust, son compatibles con funciones de visualización como autoplot() de la librería ggfortify. Sin embargo cuando se emplean métodos no estándar como el algoritmo K-medians del paquete Kmedians no es posible graficar utilizando esta función ya que no presentan la misma estructura ni clases.
K-medians es una variante robusta de K-means que utiliza la mediana en vez de la media para definir el centroide del cluster y emplea generalmente la distancia Manhattan. Esta elección hace que K-medians sea más resistente a la presencia de valores extremos (outliers), generando particiones más estables en bases de datos con asimetrías o valores atípicos.
K-means utiliza la media como centroide y la distancia euclídea, por lo que es más sensible a outliers y a variables con gran dispersión.
K-medians usa la mediana, lo que mitiga el efecto de los outliers y distribuciones asimétricas, siendo más apropiado cuando el objetivo es encontrar agrupaciones robustas ante ruido o valores atípicos.
La diferencia entre los los tamaños de los clusteres y los centroides:
Con datos originales, los tamaños de los clusters son relativamente similares, pero pueden verse influenciados por la escala de las variables.
Con datos normalizados, se observa que los clusters pueden variar en tamaño y los centroides permiten comparar variables en la misma escala, facilitando la interpretación.
La elección entre utilizar los datos originales o normalizados debe basarse en el conocimiento previo del dominio y las caracteristicas de las variables. En la mayoría de casos, se recomienda la normalización previa para evitar sesgos debidos a la escala.
Finalmente, en este análisis solo se han utilizado variables numéricas, ya que los algoritmos empleados no admiten variables categóricas de manera directa.
# Tabla comparativa tamaño de los clusters de los datos originales
# K-means
sizes_kmeans3_orig <- as.vector(kmeans3_orig$size)
sizes_kmeans3_norm <- as.vector(kmeans3_norm$size)
sizes_kmeans4_orig <- as.vector(kmeans4_orig$size)
sizes_kmeans4_norm <- as.vector(kmeans4_norm$size)
# K-medians
sizes_kmedians3_orig <- as.vector(table(kmedians3_orig$allresults$cluster))
sizes_kmedians3_norm <- as.vector(table(kmedians3_norm$allresults$cluster))
sizes_kmedians4_orig <- as.vector(table(kmedians4_orig$allresults$cluster))
sizes_kmedians4_norm <- as.vector(table(kmedians4_norm$allresults$cluster))
# Construimos la tabla
sizes_table <- data.frame(
Metodo = rep(c("K-means", "K-medians"), each = 4),
Datos = rep(c("Original", "Normalizado"), times = 4),
K = rep(c(3, 3, 4, 4), 2),
Cluster_1 = c(sizes_kmeans3_orig[1], sizes_kmeans3_norm[1], sizes_kmeans4_orig[1], sizes_kmeans4_norm[1],
sizes_kmedians3_orig[1], sizes_kmedians3_norm[1], sizes_kmedians4_orig[1], sizes_kmedians4_norm[1]),
Cluster_2 = c(sizes_kmeans3_orig[2], sizes_kmeans3_norm[2], sizes_kmeans4_orig[2], sizes_kmeans4_norm[2],
sizes_kmedians3_orig[2], sizes_kmedians3_norm[2], sizes_kmedians4_orig[2], sizes_kmedians4_norm[2]),
Cluster_3 = c(sizes_kmeans3_orig[3], sizes_kmeans3_norm[3], sizes_kmeans4_orig[3], sizes_kmeans4_norm[3],
sizes_kmedians3_orig[3], sizes_kmedians3_norm[3], sizes_kmedians4_orig[3], sizes_kmedians4_norm[3]),
Cluster_4 = c(NA, NA, sizes_kmeans4_orig[4], sizes_kmeans4_norm[4],
NA, NA, sizes_kmedians4_orig[4], sizes_kmedians4_norm[4])
)
kable(sizes_table, caption = "Comparativa de tamaños de clusters: K-means vs K-medians, k=3 y k=4")
Metodo | Datos | K | Cluster_1 | Cluster_2 | Cluster_3 | Cluster_4 |
---|---|---|---|---|---|---|
K-means | Original | 3 | 3387 | 3278 | 3335 | NA |
K-means | Normalizado | 3 | 2536 | 2629 | 4835 | NA |
K-means | Original | 4 | 2394 | 2635 | 2502 | 2469 |
K-means | Normalizado | 4 | 2536 | 2441 | 2394 | 2629 |
K-medians | Original | 3 | 3262 | 3337 | 3401 | NA |
K-medians | Normalizado | 3 | 2439 | 5070 | 2491 | NA |
K-medians | Original | 4 | 2526 | 2647 | 2411 | 2416 |
K-medians | Normalizado | 4 | 1593 | 1581 | 5070 | 1756 |
Tras aplicar los algoritmos de K-means y K-medians sobre el conjunto de datos, tanto con los datos originales como con los datos normalizados, se observa que el tamaño y la distribución de los clusters varía considerablemente según el método.
Con K-means sobre los datos originales y k=3, los clusters obtenidos presentan tamaños muy equilibrados (Cluster 1: 3387 pacientes, Cluster 2: 3278, Cluster 3: 3335). Esta distribución homogénea sugiere que el algoritmo ha segmentado la población en tres grandes grupos de características clínicas relativamente similares en prevalencia. Sin embargo, al aplicar K-means sobre los datos normalizados con k=3, la distribución cambia notablemente: un cluster agrupa a casi la mitad de la muestra (Cluster 3: 4835 pacientes), mientras que los otros dos contienen 2536 y 2629 pacientes respectivamente. Esta diferencia puede indicar que, tras normalizar las variables (por ejemplo, glucosa, presión arterial o colesterol), ciertos perfiles clínicos previamente diferenciados se agrupan ahora en un solo gran subgrupo por la ausencia de diferencias de escala.
Cuando se aumenta el número de clusters a k=4, K-means sigue mostrando una segmentación equilibrada tanto en los datos originales (2394, 2635, 2502 y 2469 pacientes por cluster) como en los normalizados (2536, 2441, 2394 y 2629 pacientes). Esta regularidad puede reflejar la presencia de cuatro perfiles clínicos principales, todos ellos con un peso similar en la población estudiada.
En contraste, el método K-medians, que es más robusto ante valores extremos, muestra una segmentación diferente, especialmente tras la normalización. Por ejemplo, con K-medians y datos normalizados, k=3, se observa la formación de un macro-cluster que agrupa a 5070 pacientes (más del 50% de la muestra), mientras que los otros dos clusters contienen 2439 y 2491 pacientes. Con k=4 clusters normalizados, la situación se acentúa, uno de los clusters contiene 5070 individuos, mientras que los demás agrupan 1593, 1581 y 1756 pacientes respectivamente. Este resultado sugiere la existencia de un tipo clínico muy prevalente (por ejemplo, pacientes con factores de riesgo comunes y perfiles metabólicos similares), junto a varios subgrupos minoritarios que pueden corresponder a pacientes con características más específicas o atípicas, como aquellos con antecedentes familiares de diabetes o niveles de glucosa especialmente elevados.
Desde el punto de vista médico, estos resultados resaltan la importancia de la elección del método de agrupamiento y la normalización. Mientras que K-means tiende a crear grupos de tamaño similar y puede ser más sensible a la escala de las variables, K-medians permite identificar subgrupos extremos y es menos sensible a outliers, lo que puede ser especialmente relevante en contextos clínicos donde existen pacientes con valores muy elevados o atípicos en ciertas variables, por ejemplo, HbA1c o glucemia en pacientes con diabetes mal controlada.
En conclusión, la elección del algoritmo y la normalización de los datos impactan en la composición de los clusters y, por tanto, en la interpretación clínica. Identificar un macro-cluster puede indicar la presencia de un tipo común, pero también es esencial analizar los subgrupos minoritarios para diseñar estrategias de intervención personalizadas o prevención en función del riesgo y las características clínicas detectadas.
# Extrae los centroides/medianas
df_centroids_kmeans3 <- as.data.frame(kmeans3_orig$centers)
df_centroids_kmedians3 <- as.data.frame(kmedians3_orig$bestresult$centers)
# Asegura mismo nombre de columnas
colnames(df_centroids_kmeans3) <- colnames(df_num2)
colnames(df_centroids_kmedians3) <- colnames(df_num2)
# Añade columnas de identificación
df_centroids_kmeans3$Método <- "K-means"
df_centroids_kmedians3$Método <- "K-medians"
df_centroids_kmeans3$Cluster <- paste0("Cluster_", seq_len(nrow(df_centroids_kmeans3)))
df_centroids_kmedians3$Cluster <- paste0("Cluster_", seq_len(nrow(df_centroids_kmedians3)))
df_centroids_kmeans3 <- df_centroids_kmeans3 %>% relocate(Método, Cluster)
df_centroids_kmedians3 <- df_centroids_kmedians3 %>% relocate(Método, Cluster)
# Asegura columnas comunes y orden
comunes <- intersect(colnames(df_centroids_kmeans3), colnames(df_centroids_kmedians3))
df_centroids_kmeans3 <- df_centroids_kmeans3[, comunes]
df_centroids_kmedians3 <- df_centroids_kmedians3[, comunes]
# Une ambos
tabla_comparativa_3 <- rbind(df_centroids_kmeans3, df_centroids_kmedians3)
# Visualiza
kable(tabla_comparativa_3, digits = 2, caption = "Comparativa de centroides: K-means vs K-medians (Original, k=3)")
Método | Cluster | Age | BMI | Waist_Circumference | Fasting_Blood_Glucose | HbA1c | Blood_Pressure_Systolic | Blood_Pressure_Diastolic | Cholesterol_Total | Cholesterol_HDL | Cholesterol_LDL | GGT | Serum_Urate | Dietary_Intake_Calories | Family_History_of_Diabetes | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | K-means | Cluster_1 | 44.45 | 29.59 | 95.02 | 134.33 | 9.55 | 134.79 | 89.61 | 226.01 | 55.01 | 133.90 | 55.37 | 5.51 | 3567.23 | 0.51 |
2 | K-means | Cluster_2 | 44.71 | 29.29 | 94.83 | 134.84 | 9.45 | 133.36 | 89.59 | 224.07 | 55.09 | 134.26 | 55.14 | 5.50 | 1913.96 | 0.49 |
3 | K-means | Cluster_3 | 44.70 | 29.37 | 94.53 | 135.16 | 9.52 | 134.32 | 89.47 | 225.39 | 54.95 | 134.91 | 54.99 | 5.50 | 2719.24 | 0.52 |
11 | K-medians | Cluster_1 | 44.65 | 29.21 | 95.01 | 134.85 | 9.48 | 133.75 | 89.67 | 224.64 | 55.03 | 134.71 | 55.52 | 5.51 | 1914.38 | 0.49 |
21 | K-medians | Cluster_2 | 44.60 | 29.35 | 94.14 | 135.74 | 9.54 | 134.43 | 89.47 | 224.96 | 55.07 | 134.77 | 55.05 | 5.50 | 2710.62 | 0.52 |
31 | K-medians | Cluster_3 | 44.63 | 29.55 | 94.74 | 134.35 | 9.56 | 134.97 | 89.50 | 225.92 | 55.23 | 134.03 | 55.05 | 5.48 | 3564.27 | 0.52 |
Al comparar los centroides obtenidos mediante K-means y K-medians sobre los datos originales para k=3, se observa que ambos métodos identifican perfiles clínicos muy similares en los tres clusters, lo que sugiere que la estructura interna del conjunto de datos es robusta y no está excesivamente influenciada por outliers. Sin embargo, se aprecian pequeñas diferencias en los valores centrales de algunas variables que pueden ser relevantes desde el punto de vista médico.
Por ejemplo, el primer cluster de K-means se caracteriza por una edad media de 44,45 años, un índice de masa corporal (BMI) de 29,59, una circunferencia de cintura de 95,02 cm y una glucemia basal media de 134,33 mg/dL. Los valores de HbA1c y presión arterial sistólica y diastólica son de 9,55%, 134,79 mmHg y 89,61 mmHg respectivamente. El consumo calórico medio es de 3567 kcal/día y el 51% de los individuos tienen antecedentes familiares de diabetes.
Si analizamos el primer cluster de K-medians, se observa que los valores centrales son muy similares: edad de 44,65 años, BMI de 29,21, glucemia basal de 134,85 mg/dL, y consumo calórico de 1914 kcal/día. El porcentaje con antecedentes familiares de diabetes se mantiene en el 49%. Sin embargo, destaca que el valor de la mediana en consumo calórico es más bajo, lo que indica que este método es menos sensible a valores extremos (posibles registros atípicos de ingesta).
En el segundo y tercer clusters ambos métodos también arrojan resultados casi idénticos. Por ejemplo, el cluster 3 de K-means tiene un BMI de 29,37, glucosa de 135,16 mg/dL y un consumo calórico medio de 2719 kcal/día, frente a los valores de BMI 29,55, glucosa 134,35 mg/dL y calorías 3564 kcal/día del cluster 3 de K-medians. Estas pequeñas diferencias reflejan que, aunque los centroides medios y medianos son próximos, K-medians protege más frente a valores extremos, en particular en variables susceptibles a outliers como el consumo calórico.
En general, ambos métodos identifican tres perfiles metabólicos de características clínicas muy semejantes, donde la edad ronda los 44-45 años, el BMI está en torno a 29-30 y la glucosa basal cerca de 135 mg/dL. Las diferencias entre K-means y K-medians se aprecian especialmente en variables propensas a dispersión, como la ingesta calórica, donde la media es claramente superior a la mediana, sugiriendo la presencia de pacientes con consumos muy elevados que solo afectan al centroide medio.
Desde el punto de vista médico, estos resultados indican que el grupo estudiado presenta perfiles clínicos relativamente homogéneos, aunque existen ciertos subgrupos con ingestas calóricas muy altas que solo se reflejan en los resultados de K-means. Si el objetivo es detectar pacientes en riesgo por valores extremos, puede ser útil utilizar ambos métodos: K-means para valorar el efecto promedio y K-medians para obtener una visión más robusta ante outliers.
En conjunto, la similitud en los valores centrales de ambos métodos aporta robustez a la segmentación clínica, mientras que las diferencias, especialmente en variables sensibles a outliers, ponen de manifiesto la utilidad de emplear K-medians para obtener una representación más fiel de los perfiles predominantes en la población. Esto puede ser de especial interés para la estratificación de riesgos y la planificación de intervenciones nutricionales dirigidas a los subgrupos con ingesta calórica elevada o factores de riesgo específicos.
# Los outputs tienen el mismo número y orden de variables
df_centroids_kmeans4 <- as.data.frame(kmeans4_orig$centers)
df_centroids_kmedians4 <- as.data.frame(kmedians4_orig$bestresult$centers)
# Asignamos nombres de columnas para asegurarte que son idénticos
colnames(df_centroids_kmeans4) <- colnames(df_num2)
colnames(df_centroids_kmedians4) <- colnames(df_num2)
df_centroids_kmeans4$Método <- "K-means"
df_centroids_kmedians4$Método <- "K-medians"
df_centroids_kmeans4$Cluster <- paste0("Cluster_", seq_len(nrow(df_centroids_kmeans4)))
df_centroids_kmedians4$Cluster <- paste0("Cluster_", seq_len(nrow(df_centroids_kmedians4)))
# Identificadores
df_centroids_kmeans4 <- df_centroids_kmeans4 %>%
relocate(Método, Cluster)
df_centroids_kmedians4 <- df_centroids_kmedians4 %>%
relocate(Método, Cluster)
# Columnas en común y mismo orden
comunes <- intersect(colnames(df_centroids_kmeans4), colnames(df_centroids_kmedians4))
df_centroids_kmeans4 <- df_centroids_kmeans4[, comunes]
df_centroids_kmedians4 <- df_centroids_kmedians4[, comunes]
tabla_comparativa_4 <- rbind(df_centroids_kmeans4, df_centroids_kmedians4)
kable(tabla_comparativa_4, digits = 2, caption = "Comparativa de centroides: K-means vs K-medians (Original, k=4)")
Método | Cluster | Age | BMI | Waist_Circumference | Fasting_Blood_Glucose | HbA1c | Blood_Pressure_Systolic | Blood_Pressure_Diastolic | Cholesterol_Total | Cholesterol_HDL | Cholesterol_LDL | GGT | Serum_Urate | Dietary_Intake_Calories | Family_History_of_Diabetes | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | K-means | Cluster_1 | 44.45 | 29.35 | 94.87 | 133.28 | 9.46 | 134.55 | 89.16 | 226.03 | 54.76 | 133.89 | 54.46 | 5.51 | 3070.18 | 0.53 |
2 | K-means | Cluster_2 | 44.58 | 29.33 | 94.43 | 136.07 | 9.55 | 134.23 | 89.47 | 225.76 | 54.92 | 134.07 | 55.47 | 5.51 | 2442.81 | 0.51 |
3 | K-means | Cluster_3 | 44.85 | 29.32 | 95.07 | 134.55 | 9.44 | 133.32 | 89.83 | 223.90 | 55.11 | 135.09 | 55.09 | 5.50 | 1817.63 | 0.49 |
4 | K-means | Cluster_4 | 44.59 | 29.67 | 94.84 | 135.08 | 9.57 | 134.58 | 89.76 | 224.96 | 55.29 | 134.36 | 55.61 | 5.50 | 3681.76 | 0.51 |
11 | K-medians | Cluster_1 | 44.91 | 29.27 | 95.09 | 135.39 | 9.49 | 133.50 | 90.03 | 223.89 | 55.01 | 135.21 | 55.32 | 5.49 | 1823.20 | 0.49 |
21 | K-medians | Cluster_2 | 44.55 | 29.36 | 94.76 | 135.90 | 9.56 | 134.19 | 89.64 | 226.63 | 54.86 | 134.44 | 55.69 | 5.51 | 2446.91 | 0.51 |
31 | K-medians | Cluster_3 | 44.63 | 29.56 | 94.97 | 134.97 | 9.55 | 134.68 | 89.68 | 225.07 | 55.25 | 134.58 | 55.08 | 5.48 | 3689.26 | 0.51 |
41 | K-medians | Cluster_4 | 44.68 | 29.40 | 94.87 | 133.03 | 9.46 | 134.81 | 89.16 | 225.83 | 54.77 | 134.36 | 54.53 | 5.51 | 3084.35 | 0.53 |
La comparación de los centroides obtenidos mediante K-means y K-medians sobre los datos originales con𝑘= 4 muestra, nuevamente, una alta similitud entre los perfiles centrales identificados por ambos métodos. No obstante, se aprecian matices en algunos valores, especialmente en variables sensibles a la presencia de outliers, como la ingesta calórica.
Por ejemplo, el Cluster 1 de K-means presenta una edad media de 44,45 años, un BMI de 29,35 y una glucemia basal de 133,28 mg/dL. El consumo calórico medio de este grupo asciende a 3070,18 kcal/día y el 53% tiene antecedentes familiares de diabetes. Su equivalente en K-medians muestra una edad de 44,91 años, BMI de 29,27, glucosa de 135,39 mg/dL y un consumo calórico notablemente inferior, de 1823,20 kcal/día (mediana). La diferencia en la ingesta calórica entre ambos métodos revela la existencia de valores extremos en el grupo, que elevan la media detectada por K-means pero no afectan la mediana de K-medians.
El Cluster 2 de K-means está caracterizado por una glucosa basal ligeramente más alta (136,07 mg/dL) y un BMI similar al cluster anterior (29,33), con un consumo calórico medio de 2442,81 kcal/día y un 51% de antecedentes familiares de diabetes. En K-medians, este grupo muestra una glucosa de 135,90 mg/dL y un consumo calórico de 2446,91 kcal/día, con valores muy parecidos en el resto de parámetros, lo que indica coherencia y robustez en la segmentación para estos individuos.
El Cluster 3 de K-means destaca por el menor consumo calórico medio (1817,63 kcal/día) y el menor porcentaje de antecedentes familiares de diabetes (49%), junto con valores de BMI, glucosa y presión arterial similares al resto de clusters. En K-medians, el Cluster 1 también refleja estos valores bajos de calorías (1823,20 kcal/día) y antecedentes familiares, lo que refuerza la existencia de un subgrupo clínico con menor riesgo metabólico.
Por último, el Cluster 4 de K-means se distingue por el mayor consumo calórico medio (3681,76 kcal/día) y un 51% de antecedentes familiares de diabetes. El Cluster 3 de K-medians tiene valores prácticamente idénticos en estas variables, lo que respalda la estabilidad del agrupamiento frente a posibles outliers en la ingesta energética.
Desde el punto de vista médico, estos resultados sugieren que tanto K-means como K-medians identifican cuatro tipos metabólicos principales en la muestra, con diferencias relativamente mínimas entre ellos. Las variables clave como la glucosa, el BMI y la presión arterial permanecen estables entre los métodos, mientras que la ingesta calórica presenta variaciones mayores entre la media y la mediana, poniendo de manifiesto la presencia de pacientes con ingestas extremadamente altas que sólo impactan el resultado de K-means.
En conclusión, el análisis conjunto muestra que la normalización reduce la variabilidad entre clusters en la mayoría de variables, y que K-medians, al ser más robusto, puede revelar subgrupos clínicos extremos incluso en condiciones de homogeneidad aparente. La interpretación médica de estos resultados sugiere que, tras la normalización, es fundamental explorar las variables que mantienen diferencias relevantes para no perder la capacidad de segmentación clínica.
df_centroids_kmeans3_norm <- as.data.frame(kmeans3_norm$centers)
df_centroids_kmedians3_norm <- as.data.frame(kmedians3_norm$bestresult$centers)
# Asignamos nombres de columnas
colnames(df_centroids_kmeans3_norm) <- colnames(df_norm2)
colnames(df_centroids_kmedians3_norm) <- colnames(df_norm2)
df_centroids_kmeans3_norm$Método <- "K-means"
df_centroids_kmedians3_norm$Método <- "K-medians"
df_centroids_kmeans3_norm$Cluster <- paste0("Cluster_", seq_len(nrow(df_centroids_kmeans3_norm)))
df_centroids_kmedians3_norm$Cluster <- paste0("Cluster_", seq_len(nrow(df_centroids_kmedians3_norm)))
df_centroids_kmeans3_norm <- df_centroids_kmeans3_norm %>%
relocate(Método, Cluster)
df_centroids_kmedians3_norm <- df_centroids_kmedians3_norm %>%
relocate(Método, Cluster)
comunes <- intersect(colnames(df_centroids_kmeans3_norm), colnames(df_centroids_kmedians3_norm))
df_centroids_kmeans3_norm <- df_centroids_kmeans3_norm[, comunes]
df_centroids_kmedians3_norm <- df_centroids_kmedians3_norm[, comunes]
tabla_comparativa_3norm <- rbind(df_centroids_kmeans3_norm, df_centroids_kmedians3_norm)
kable(tabla_comparativa_3norm, digits = 2, caption = "Comparativa de centroides: K-means vs K-medians (Normalizado, k=3)")
Método | Cluster | Age | BMI | Waist_Circumference | Fasting_Blood_Glucose | HbA1c | Blood_Pressure_Systolic | Blood_Pressure_Diastolic | Cholesterol_Total | Cholesterol_HDL | Cholesterol_LDL | GGT | Serum_Urate | Dietary_Intake_Calories | Family_History_of_Diabetes | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | K-means | Cluster_1 | 0.50 | 0.51 | 0.49 | 0.49 | 0.50 | 0.49 | 0.51 | 0.50 | 0.50 | 0.49 | 0.50 | 0.51 | 0.49 | 0.0 |
2 | K-means | Cluster_2 | 0.51 | 0.51 | 0.49 | 0.49 | 0.50 | 0.50 | 0.50 | 0.50 | 0.50 | 0.49 | 0.51 | 0.49 | 0.51 | 1.0 |
3 | K-means | Cluster_3 | 0.50 | 0.51 | 0.50 | 0.51 | 0.50 | 0.50 | 0.50 | 0.50 | 0.50 | 0.50 | 0.50 | 0.50 | 0.49 | 0.5 |
11 | K-medians | Cluster_1 | 0.52 | 0.50 | 0.47 | 0.75 | 0.49 | 0.48 | 0.52 | 0.47 | 0.51 | 0.50 | 0.46 | 0.50 | 0.46 | 0.0 |
21 | K-medians | Cluster_2 | 0.50 | 0.51 | 0.50 | 0.50 | 0.50 | 0.50 | 0.50 | 0.50 | 0.50 | 0.49 | 0.51 | 0.49 | 0.50 | 1.0 |
31 | K-medians | Cluster_3 | 0.49 | 0.52 | 0.51 | 0.26 | 0.51 | 0.51 | 0.49 | 0.53 | 0.50 | 0.49 | 0.53 | 0.52 | 0.52 | 0.0 |
Al analizar los centroides obtenidos tras la normalización de los datos y aplicando k=3 tanto en K-means como en K-medians, se observa una marcada homogeneidad en la mayoría de las variables entre los diferentes clusters. Los valores centrados en torno a 0.5 reflejan el efecto de la normalización, que iguala la escala de todas las variables, minimizando diferencias absolutas entre grupos.
En el caso de K-means, los tres clusters muestran valores muy similares para la mayoría de las variables. Por ejemplo, la edad normalizada se sitúa en torno a 0.50–0.51 en los tres clusters, y la glucosa en ayunas oscila entre 0.49 y 0.51. Algo parecido ocurre con el índice de masa corporal (BMI), presión arterial y lípidos, que varían muy poco entre grupos (BMI: 0.51, 0.51, 0.51; Colesterol LDL: 0.49–0.50). Destaca que la variable “Family_History_of_Diabetes” toma valores promedio de 0.0, 1.0 y 0.5, lo que indica que en el Cluster 2, la totalidad de los individuos tienen antecedentes familiares de diabetes, mientras que en Cluster 1 ninguno, y en Cluster 3 la mitad.
Por otro lado, K-medians muestra una mayor dispersión en algunas variables concretas, lo que podría indicar la presencia de subgrupos con perfiles algo más diferenciados tras el uso de la mediana como centroide. Así, en el Cluster 1 de K-medians, la glucosa en ayunas alcanza un valor de 0.75, bastante superior al resto de clusters, mientras que en Cluster 3 desciende hasta 0.26. Algo similar sucede en la ingesta calórica (Dietary_Intake_Calories), que oscila de 0.46 en Cluster 1 a 0.52 en Cluster 3. Además, la variable “Family_History_of_Diabetes” refleja el mismo patrón extremo que en K-means, con clusters donde todos o ningún individuo tienen antecedentes, y uno intermedio.
Desde el punto de vista médico, la normalización ha provocado que los grupos sean más homogéneos en la mayoría de los factores de riesgo tradicionales (edad, BMI, presión arterial, colesterol…), lo que puede dificultar la diferenciación clínica real si las variables originales tenían escalas muy distintas. Sin embargo, la aplicación de K-medians permite identificar ciertos clusters que, pese a la normalización, mantienen diferencias más marcadas en variables como glucosa o ingesta calórica, lo que puede ayudar a distinguir subgrupos metabólicos extremos o clínicamente relevantes (por ejemplo, pacientes con muy alta glucosa en ayunas concentrados en un único cluster).
df_centroids_kmeans4_norm <- as.data.frame(kmeans4_norm$centers)
df_centroids_kmedians4_norm <- as.data.frame(kmedians4_norm$bestresult$centers)
# Asegura que los nombres de columna coincidan
colnames(df_centroids_kmeans4_norm) <- colnames(df_norm2)
colnames(df_centroids_kmedians4_norm) <- colnames(df_norm2)
df_centroids_kmeans4_norm$Método <- "K-means"
df_centroids_kmedians4_norm$Método <- "K-medians"
df_centroids_kmeans4_norm$Cluster <- paste0("Cluster_", seq_len(nrow(df_centroids_kmeans4_norm)))
df_centroids_kmedians4_norm$Cluster <- paste0("Cluster_", seq_len(nrow(df_centroids_kmedians4_norm)))
df_centroids_kmeans4_norm <- df_centroids_kmeans4_norm %>%
relocate(Método, Cluster)
df_centroids_kmedians4_norm <- df_centroids_kmedians4_norm %>%
relocate(Método, Cluster)
comunes <- intersect(colnames(df_centroids_kmeans4_norm), colnames(df_centroids_kmedians4_norm))
df_centroids_kmeans4_norm <- df_centroids_kmeans4_norm[, comunes]
df_centroids_kmedians4_norm <- df_centroids_kmedians4_norm[, comunes]
tabla_comparativa_4norm <- rbind(df_centroids_kmeans4_norm, df_centroids_kmedians4_norm)
kable(tabla_comparativa_4norm, digits = 2, caption = "Comparativa de centroides: K-means vs K-medians (Normalizado, k=4)")
Método | Cluster | Age | BMI | Waist_Circumference | Fasting_Blood_Glucose | HbA1c | Blood_Pressure_Systolic | Blood_Pressure_Diastolic | Cholesterol_Total | Cholesterol_HDL | Cholesterol_LDL | GGT | Serum_Urate | Dietary_Intake_Calories | Family_History_of_Diabetes | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | K-means | Cluster_1 | 0.50 | 0.51 | 0.49 | 0.49 | 0.50 | 0.49 | 0.51 | 0.50 | 0.50 | 0.49 | 0.50 | 0.51 | 0.49 | 0 |
2 | K-means | Cluster_2 | 0.49 | 0.51 | 0.51 | 0.51 | 0.50 | 0.49 | 0.49 | 0.50 | 0.50 | 0.50 | 0.51 | 0.49 | 0.49 | 1 |
3 | K-means | Cluster_3 | 0.51 | 0.51 | 0.49 | 0.50 | 0.50 | 0.50 | 0.50 | 0.50 | 0.50 | 0.50 | 0.49 | 0.50 | 0.50 | 0 |
4 | K-means | Cluster_4 | 0.51 | 0.51 | 0.49 | 0.49 | 0.50 | 0.50 | 0.50 | 0.50 | 0.50 | 0.49 | 0.51 | 0.49 | 0.51 | 1 |
11 | K-medians | Cluster_1 | 0.46 | 0.52 | 0.42 | 0.62 | 0.42 | 0.29 | 0.56 | 0.43 | 0.60 | 0.53 | 0.42 | 0.56 | 0.41 | 0 |
21 | K-medians | Cluster_2 | 0.67 | 0.46 | 0.49 | 0.65 | 0.53 | 0.69 | 0.50 | 0.49 | 0.42 | 0.49 | 0.51 | 0.44 | 0.51 | 0 |
31 | K-medians | Cluster_3 | 0.50 | 0.51 | 0.50 | 0.50 | 0.50 | 0.50 | 0.50 | 0.50 | 0.50 | 0.49 | 0.51 | 0.49 | 0.50 | 1 |
41 | K-medians | Cluster_4 | 0.40 | 0.54 | 0.55 | 0.25 | 0.55 | 0.51 | 0.46 | 0.58 | 0.49 | 0.47 | 0.55 | 0.52 | 0.55 | 0 |
Al comparar los centroides obtenidos tras la normalización de las variables y la aplicación de k=4, se aprecia que K-means sigue generando clusters con valores muy homogéneos, en torno a 0.5 para prácticamente todas las variables. Por ejemplo, la edad varía de 0.49 a 0.51, el BMI se mantiene entre 0.51 y 0.51, y la glucosa en ayunas entre 0.49 y 0.51 en los cuatro clusters. De igual manera, el porcentaje de pacientes con antecedentes familiares de diabetes alterna principalmente entre 0 y 1, lo que indica la separación de los grupos según la presencia o ausencia de este factor de riesgo, pero con poca diferenciación adicional en el resto de variables.
Por su parte, K-medians revela una segmentación mucho más marcada en ciertas variables, identificando subgrupos clínicos extremos que no se reflejan en los clusters de K-means. Así, por ejemplo, en el Cluster 2 de K-medians, la edad alcanza un valor normalizado de 0.67 (el más alto del grupo), mientras que en el Cluster 4 desciende a 0.40 (el más bajo), lo que apunta a la existencia de subgrupos diferenciados por edad que pueden corresponder a perfiles de riesgo distintos (como pacientes más jóvenes o más mayores). Asimismo, la glucosa en ayunas y el BMI presentan valores extremos: 0.65 y 0.46 en el Cluster 2, y 0.25 y 0.54 en el Cluster 4 respectivamente. En el Cluster 1, la glucosa es relativamente alta (0.62), y en Cluster 3 se mantiene cercana a la media (0.50). También se observan diferencias en la ingesta calórica, por ejemplo, 0.55 en el Cluster 4 y 0.41 en el Cluster 1.
Estos resultados indican que, tras la normalización, K-medians sigue siendo capaz de detectar subgrupos clínicos minoritarios o extremos, mientras que K-means tiende a agrupar la mayoría de los casos en torno a perfiles medios. Desde el punto de vista médico, esto es especialmente relevante para la identificación de fenotipos de riesgo: por ejemplo, el Cluster 2 de K-medians podría estar agrupando a pacientes de mayor edad y glucosa elevada, lo que podría corresponder a un subgrupo con mayor riesgo metabólico y cardiovascular, mientras que el Cluster 4 podría corresponder a pacientes más jóvenes y con menor glucosa, pero con una mayor ingesta calórica.
En conclusión, aunque la normalización iguala la escala de las variables y tiende a homogeneizar los clusters en K-means, el uso de K-medians permite identificar fenotipos extremos o clínicamente relevantes que pueden ser invisibles para los métodos basados en la media. Para la práctica clínica y la estratificación del riesgo, esto puede ser clave para personalizar las intervenciones y dirigir recursos a subgrupos específicos dentro de la población.
El análisis comparativo de los algoritmos K-means y K-medians, aplicados tanto sobre los datos originales como sobre los datos normalizados, señala las diferencias tanto en la distribución de tamaños de los clusters como en los perfiles centrales de cada grupo. K-means tiende a formar clusters de tamaños más homogéneos y presenta centroides muy próximos al valor medio global, lo que puede ocultar la presencia de subgrupos clínicos extremos o minoritarios, especialmente en variables sensibles a outliers. En cambio, K-medians muestra una mayor robustez ante valores extremos y permite identificar clusters con características más diferenciadas, especialmente tras la normalización de las variables.
La normalización reduce la influencia de la escala original de cada variable, haciendo que las diferencias entre grupos se centren en patrones multivariantes y no en el peso absoluto de determinadas características clínicas. Esto provoca que, en algunos casos, los clusters sean más homogéneos en K-means, mientras que K-medians sigue siendo capaz de detectar subgrupos extremos.
Desde el punto de vista clínico, la combinación de ambos métodos y la comparación entre datos originales y normalizados permite obtener una visión más completa de la población: K-means aporta una segmentación general equilibrada y útil para identificar tendencias globales, mientras que K-medians para descubrir perfiles minoritarios o atípicos que pueden requerir una atención clínica diferenciada. Así, la elección del método no solo afecta la estructura de los grupos obtenidos, sino que también determina la capacidad para detectar tipos de riesgo.
Tras el estudio realizado con ambas técnicas de clustering k-means y k-medians detectamos que las variables binomiales y además normalizadas, no solo no aportan información relevante si no que puede generar sesgos. Por lo cual, lo ideal seria desestimar y no incluir estas variables. En el caso de la variable “Family_History_of_Diabetes” continuaremos incluyéndola al conjunto de datos ya que deberemos comparar resultados.
Además para no dilatar demasiado la práctica innecesariamente y después de comprobar que el número de óptimo de clusters es k=4.
En este ejercicio se ha optado por comparar dos enfoques alternativos al k-means tradicional: k-means con distancia Manhattan y el método PAM (k-medoides).
Se aplica ambos métodos k-means con distancia Manhattan y PAM (k-medoides) porque ambos métodos son menos sensibles a outliers que el k-means con distancia Euclídea, relevante en estudios con datos clínicos donde existen valores extremos. Además, PAM permite identificar grupos representados por pacientes reales y admite distintas métricas de distancia, facilitando una segmentación más robusta.
# Libreria
if (!require(flexclust)) install.packages("flexclust")
library(flexclust)
Método k-means con distancia Manhattan
El algoritmo k-means con distancia Manhattan es una variante del k-means tradicional donde la asignación de los puntos a los clusters y el cálculo de las distancias se realiza utilizando la distancia Manhattan (o cityblock), que suma las diferencias absolutas entre las coordenadas de los puntos. Esta métrica es menos sensible a valores extremos que la Euclídea y, por tanto, puede ofrecer agrupamientos más representativos cuando existen outliers en los datos. Aunque k-means está optimizado para la distancia Euclídea, mediante paquetes como flexclust es posible implementar esta variante en R, permitiendo comparar el efecto de diferentes métricas de distancia en la calidad y estabilidad de los clusters. Al igual que otros métodos, la validación de resultados puede realizarse con librerías especializadas como clValid, que facilita la elección del método más adecuado para cada contexto.
# K-means con distancia Manhattan (cityblock), datos sin normalizar
kmeans4_manhattan <- kcca(df_num2, k=4, family=kccaFamily("kmeans", dist="manhattan"))
# Datos normalizados
kmeans4_manhattan_norm <- kcca(df_norm2, k=4, family=kccaFamily("kmeans", dist="manhattan"))
centroides_kmeans4_manhattan <- kmeans4_manhattan@centers
centroides_kmeans4_manhattan_norm <- kmeans4_manhattan_norm@centers
# Conviertimos a data.frame
tabla_centroides_kmeans4_manhattan <- as.data.frame(centroides_kmeans4_manhattan)
tabla_centroides_kmeans4_manhattan_norm <- as.data.frame(centroides_kmeans4_manhattan_norm)
# Agregamos el identificador de cluster como fila
tabla_centroides_kmeans4_manhattan$Cluster <- paste0("Cluster_", seq_len(nrow(tabla_centroides_kmeans4_manhattan)))
tabla_centroides_kmeans4_manhattan_norm$Cluster <- paste0("Cluster_", seq_len(nrow(tabla_centroides_kmeans4_manhattan_norm)))
# Reordenamos
tabla_centroides_kmeans4_manhattan <- tabla_centroides_kmeans4_manhattan %>% relocate(Cluster)
tabla_centroides_kmeans4_manhattan_norm <- tabla_centroides_kmeans4_manhattan_norm %>% relocate(Cluster)
# Muestramos la tabla
kable(tabla_centroides_kmeans4_manhattan, digits = 2, caption = "Centroides K-means Manhattan (k=4, datos originales)")
Cluster | Age | BMI | Waist_Circumference | Fasting_Blood_Glucose | HbA1c | Blood_Pressure_Systolic | Blood_Pressure_Diastolic | Cholesterol_Total | Cholesterol_HDL | Cholesterol_LDL | GGT | Serum_Urate | Dietary_Intake_Calories | Family_History_of_Diabetes |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Cluster_1 | 44.58 | 29.68 | 94.82 | 134.89 | 9.57 | 134.59 | 89.80 | 225.03 | 55.27 | 134.36 | 55.56 | 5.50 | 3684.75 | 0.51 |
Cluster_2 | 44.85 | 29.32 | 95.07 | 134.55 | 9.44 | 133.32 | 89.83 | 223.90 | 55.11 | 135.09 | 55.09 | 5.50 | 1817.63 | 0.49 |
Cluster_3 | 44.58 | 29.33 | 94.43 | 136.12 | 9.55 | 134.26 | 89.46 | 225.70 | 54.93 | 134.13 | 55.47 | 5.51 | 2443.53 | 0.51 |
Cluster_4 | 44.46 | 29.34 | 94.89 | 133.42 | 9.46 | 134.51 | 89.15 | 226.02 | 54.77 | 133.83 | 54.52 | 5.51 | 3074.02 | 0.52 |
kable(tabla_centroides_kmeans4_manhattan_norm, digits = 2, caption = "Centroides K-means Manhattan (k=4, datos normalizados)")
Cluster | Age | BMI | Waist_Circumference | Fasting_Blood_Glucose | HbA1c | Blood_Pressure_Systolic | Blood_Pressure_Diastolic | Cholesterol_Total | Cholesterol_HDL | Cholesterol_LDL | GGT | Serum_Urate | Dietary_Intake_Calories | Family_History_of_Diabetes |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Cluster_1 | 0.48 | 0.51 | 0.51 | 0.49 | 0.48 | 0.53 | 0.51 | 0.53 | 0.49 | 0.65 | 0.53 | 0.52 | 0.78 | 0 |
Cluster_2 | 0.50 | 0.51 | 0.50 | 0.50 | 0.50 | 0.50 | 0.50 | 0.50 | 0.50 | 0.49 | 0.51 | 0.49 | 0.50 | 1 |
Cluster_3 | 0.49 | 0.50 | 0.51 | 0.50 | 0.55 | 0.47 | 0.52 | 0.50 | 0.50 | 0.20 | 0.49 | 0.49 | 0.46 | 0 |
Cluster_4 | 0.54 | 0.53 | 0.44 | 0.51 | 0.47 | 0.49 | 0.48 | 0.47 | 0.52 | 0.71 | 0.47 | 0.52 | 0.25 | 0 |
# vector de clusters
clusters_manhattan <- clusters(kmeans4_manhattan)
fviz_cluster(
list(data = df_num2, cluster = clusters_manhattan),
ellipse.type = "convex",
show.clust.cent = TRUE,
main = "Cluster plot K-means Manhattan k=4, datos originales",
geom = "point", # Solo puntos
labelsize = 0, # Sin números
pointsize = 1.2,
ggtheme = theme_minimal()
)
# vector de clusters
clusters_manhattan_norm <- clusters(kmeans4_manhattan_norm)
fviz_cluster(
list(data = df_norm, cluster = clusters_manhattan_norm),
ellipse.type = "convex",
show.clust.cent = TRUE,
main = "Cluster plot K-means Manhattan k=4, datos normalizados",
geom = "point", # Solo puntos
labelsize = 0, # Sin números
pointsize = 1.2,
ggtheme = theme_minimal()
)
K-means Manhattan (original y normalizado) En las gráficas, los clusters presentan un elevado solapamiento en el espacio de las dos primeras componentes principales. Esto sugiere que la segmentación es sutil y las diferencias entre grupos son multivariantes, difíciles de visualizar en 2D. La normalización redistribuye ligeramente los grupos, pero no mejora la definición visual.
Método PAM k-medoides
El algoritmo PAM (Partitioning Around Medoids, k-medoides) es una técnica de clustering en la que cada grupo está representado por un medoide, es decir, un punto real del conjunto de datos que minimiza la distancia total a los demás puntos del cluster. Esta aproximación es especialmente robusta frente a ruido y valores extremos, ya que los medoides no se ven tan afectados por outliers como los centroides de k-means. PAM se implementa en la función pam() del paquete cluster, permitiendo el uso de diferentes métricas de distancia y facilitando la comparación PAM (k-medoides).
# PAM sobre datos sin normalizar
pam4_orig <- pam(df_num2, k=4, metric = "manhattan")
# PAM sobre datos normalizados
pam4_norm <- pam(df_norm2, k=4, metric = "manhattan")
# Medoides
pam4_orig$medoids
## Age BMI Waist_Circumference Fasting_Blood_Glucose HbA1c
## [1,] 37 24.5 96.5 141.8 12.9
## [2,] 24 26.9 101.1 133.7 7.0
## [3,] 54 29.3 87.6 128.4 9.4
## [4,] 40 25.5 96.6 130.1 13.4
## Blood_Pressure_Systolic Blood_Pressure_Diastolic Cholesterol_Total
## [1,] 120 89 224.4
## [2,] 125 86 226.5
## [3,] 130 96 201.1
## [4,] 125 93 257.9
## Cholesterol_HDL Cholesterol_LDL GGT Serum_Urate Dietary_Intake_Calories
## [1,] 58.1 124.6 67.9 4.2 1829
## [2,] 58.6 117.7 59.4 4.2 2880
## [3,] 46.3 111.9 52.7 4.5 3568
## [4,] 42.1 124.9 41.8 4.6 2324
## Family_History_of_Diabetes
## [1,] 1
## [2,] 1
## [3,] 1
## [4,] 0
pam4_norm$medoids
## Age BMI Waist_Circumference Fasting_Blood_Glucose HbA1c
## [1,] 0.4489796 0.5720930 0.388 0.5569231 0.4909091
## [2,] 0.3673469 0.6372093 0.506 0.5546154 0.3545455
## [3,] 0.6326531 0.5441860 0.648 0.4069231 0.6909091
## [4,] 0.6938776 0.4372093 0.674 0.4776923 0.5818182
## Blood_Pressure_Systolic Blood_Pressure_Diastolic Cholesterol_Total
## [1,] 0.3595506 0.6101695 0.4620000
## [2,] 0.5280899 0.5084746 0.6573333
## [3,] 0.5955056 0.3898305 0.1940000
## [4,] 0.8651685 0.3050847 0.5220000
## Cholesterol_HDL Cholesterol_LDL GGT Serum_Urate
## [1,] 0.666 0.7176923 0.3277778 0.48
## [2,] 0.244 0.4900000 0.2877778 0.44
## [3,] 0.788 0.2238462 0.7833333 0.44
## [4,] 0.148 0.4115385 0.8444444 0.56
## Dietary_Intake_Calories Family_History_of_Diabetes
## [1,] 0.5174070 0
## [2,] 0.6138455 1
## [3,] 0.4809924 1
## [4,] 0.4913966 0
# Indices de los medoides
indices_medoides_orig <- pam4_orig$medoids
indices_medoides_norm <- pam4_norm$medoids
# Extrae la matriz de medoides y conviértela a data.frame
tabla_medoides_pam4_orig <- as.data.frame(pam4_orig$medoids)
tabla_medoides_pam4_orig$Cluster <- paste0("Medoide_", seq_len(nrow(tabla_medoides_pam4_orig)))
tabla_medoides_pam4_orig <- tabla_medoides_pam4_orig %>% relocate(Cluster)
tabla_medoides_pam4_norm <- as.data.frame(pam4_norm$medoids)
tabla_medoides_pam4_norm$Cluster <- paste0("Medoide_", seq_len(nrow(tabla_medoides_pam4_norm)))
tabla_medoides_pam4_norm <- tabla_medoides_pam4_norm %>% relocate(Cluster)
# Visualizamos la tabla
kable(tabla_medoides_pam4_orig, digits = 2, caption = "Medoides PAM k=4, datos originales")
Cluster | Age | BMI | Waist_Circumference | Fasting_Blood_Glucose | HbA1c | Blood_Pressure_Systolic | Blood_Pressure_Diastolic | Cholesterol_Total | Cholesterol_HDL | Cholesterol_LDL | GGT | Serum_Urate | Dietary_Intake_Calories | Family_History_of_Diabetes |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Medoide_1 | 37 | 24.5 | 96.5 | 141.8 | 12.9 | 120 | 89 | 224.4 | 58.1 | 124.6 | 67.9 | 4.2 | 1829 | 1 |
Medoide_2 | 24 | 26.9 | 101.1 | 133.7 | 7.0 | 125 | 86 | 226.5 | 58.6 | 117.7 | 59.4 | 4.2 | 2880 | 1 |
Medoide_3 | 54 | 29.3 | 87.6 | 128.4 | 9.4 | 130 | 96 | 201.1 | 46.3 | 111.9 | 52.7 | 4.5 | 3568 | 1 |
Medoide_4 | 40 | 25.5 | 96.6 | 130.1 | 13.4 | 125 | 93 | 257.9 | 42.1 | 124.9 | 41.8 | 4.6 | 2324 | 0 |
kable(tabla_medoides_pam4_norm, digits = 2, caption = "Medoides PAM k=4, datos normalizados")
Cluster | Age | BMI | Waist_Circumference | Fasting_Blood_Glucose | HbA1c | Blood_Pressure_Systolic | Blood_Pressure_Diastolic | Cholesterol_Total | Cholesterol_HDL | Cholesterol_LDL | GGT | Serum_Urate | Dietary_Intake_Calories | Family_History_of_Diabetes |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Medoide_1 | 0.45 | 0.57 | 0.39 | 0.56 | 0.49 | 0.36 | 0.61 | 0.46 | 0.67 | 0.72 | 0.33 | 0.48 | 0.52 | 0 |
Medoide_2 | 0.37 | 0.64 | 0.51 | 0.55 | 0.35 | 0.53 | 0.51 | 0.66 | 0.24 | 0.49 | 0.29 | 0.44 | 0.61 | 1 |
Medoide_3 | 0.63 | 0.54 | 0.65 | 0.41 | 0.69 | 0.60 | 0.39 | 0.19 | 0.79 | 0.22 | 0.78 | 0.44 | 0.48 | 1 |
Medoide_4 | 0.69 | 0.44 | 0.67 | 0.48 | 0.58 | 0.87 | 0.31 | 0.52 | 0.15 | 0.41 | 0.84 | 0.56 | 0.49 | 0 |
fviz_cluster(
pam4_orig,
ellipse.type = "convex",
main = "Cluster plot PAM k=4 datos originales",
geom = "point", # Solo puntos
pointsize = 1.2,
show.clust.cent = TRUE,
labelsize = 0,
ggtheme = theme_minimal()
)
fviz_cluster(
pam4_norm,
ellipse.type = "convex",
main = "Cluster plot PAM k=4, datos normalizados",
geom = "point",
pointsize = 1.2,
show.clust.cent = TRUE,
labelsize = 0,
ggtheme = theme_minimal()
)
PAM con datos originales y normalizados El método PAM muestra una distribución distinta. Los clusters se agrupan más a lo largo de un eje, sobre todo en datos normalizados, donde algunos grupos quedan mucho más definidos respecto al resto. Esto indica que PAM es más sensible a la presencia de subgrupos “puros” (medoides reales) y, al ser robusto a outliers, puede identificar patrones que K-means no.
La comparación de los métodos K-means, K-medians y PAM (k-medoides), aplicados tanto sobre datos originales como normalizados, aporta una visión integral y precisa de la segmentación clínica en la población analizada. K-means permite identificar tendencias globales y perfilar los grandes grupos de riesgo mediante centroides basados en la media, proporcionando una herramienta útil para la gestión y prevención a nivel poblacional. K-medians y PAM, al emplear la mediana y los medoides respectivamente, ofrecen una mayor robustez frente a valores atípicos, facilitando la detección de subgrupos minoritarios con perfiles clínicos extremos o minoritarios, que pueden requerir una atención más personalizada. La normalización de los datos, por su parte, homogeniza la influencia de todas las variables, asegurando que cada factor de riesgo sea tenido en cuenta en la creación de los clusters. Desde una perspectiva médica, la utilización conjunta de estos métodos permite no solo una segmentación más equitativa y fiable, sino también la identificación precisa de pacientes prototipo para estrategias más adaptadas al riesgo real de cada subgrupo, aportando una medicina más personalizada.
Centroides K-means Manhattan con datos originales/reales: - Los valores promedio de cada variable clínica para cada cluster muestran perfiles próximos entre sí, con pequeñas diferencias en edad, glucosa, HbA1c, etc. Por ejemplo, el Cluster 2 tiene mayor BMI y glucosa media, lo que puede corresponder a un mayor riesgo metabólico.
Medoides PAM con los datos originales: - Los medoides corresponden a pacientes reales y tienden a representar mejor los perfiles atípicos o extremos. Por ejemplo, el Medoide 1 es un paciente joven (24 años), con buen control glucémico y presión arterial baja, mientras que el Medoide 3 es mayor, con peor perfil lipídico y valores elevados de BMI.
Centroides y medoides con datos normalizados: - Los centroides y medoides muestran valores más homogéneos y comparables entre variables, lo que ayuda a la interpretación conjunta. Sin embargo, la segmentación sigue reflejando grupos con diferente riesgo, pero menos influenciados por la escala de las variables.
DBSCAN (Density-Based Spatial Clustering of Applications with Noise) es un algoritmo de agrupamiento no supervisado diseñado para identificar automáticamente regiones de alta densidad de puntos en el espacio multivariado, agrupando observaciones similares en clusters y señalando aquellas consideradas atípicas outliers como ruido. Su principal ventaja frente a métodos clásicos como K-means o K-medians es que no requiere especificar el número de grupos a priori y es capaz de descubrir clusters de formas arbitrarias, lo que resulta especialmente útil en contextos clínicos con estructuras de datos desconocidas o heterogéneas.
El funcionamiento de DBSCAN se basa en dos parámetros principales: El proceso es el siguiente:De este modo, DBSCAN identifica regiones densas de observaciones conectadas y deja fuera aquellos puntos aislados o atípicos.
Para aplicar DBSCAN, es imprescindible que las variables sean numéricas, dado que el algoritmo se basa en el cálculo de distancias. Si el conjunto de datos incluye variables categóricas o binomiales, deben transformarse mediante codificación numérica o bien excluirse si no aportan valor al agrupamiento. Asimismo, es recomendable normalizar o estandarizar las variables, sobre todo si presentan escalas diferentes, para evitar que aquellas con mayor rango distorsionen el resultado del análisis. En el caso de variables binarias relevantes, pueden incluirse como 0/1.
DBSCAN, a diferencia de otros algoritmos, identifica los outliers en vez de asignarlos a un cluster, permitiendo así distinguir perfiles atípicos dentro de la cohorte.
El uso de DBSCAN en el ámbito clínico facilita la identificación de subgrupos complejos de pacientes, incluso cuando los clusters presentan formas irregulares o tamaños heterogéneos. Esto es especialmente valioso para detectar patrones inusuales o casos extremos, como pacientes con combinaciones poco frecuentes de factores de riesgo. Además, al no requerir hipótesis previas sobre el número de grupos, permite un análisis exploratorio objetivo y flexible. Comparar los resultados de DBSCAN con otros métodos, como K-means, K-medians o PAM, aporta una perspectiva complementaria y ayuda a validar la solidez de los agrupamientos identificados.
La eficacia de DBSCAN depende en gran medida de la correcta selección de los parámetros \(\varepsilon\) (epsilon) y MinPts, que suele requerir ajuste manual o el uso del gráfico de k-distancias. Además, el algoritmo puede perder precisión si los clusters tienen densidades muy distintas o si la dispersión de los datos es elevada ya que ya no es posible encontrar los parámetros \(\varepsilon\) (epsilon) y minPts que sirvan para todos a la vez. Finalmente, DBSCAN es aplicable únicamente sobre variables numéricas escaladas.
# Librerias
if (!require(dbscan)) install.packages("dbscan")
library(dbscan)
if (!require(RColorBrewer)) install.packages("RColorBrewer")
library(RColorBrewer)
if (!require(Rtsne)) install.packages("Rtsne")
library(Rtsne)
# if (!require(fpc)) install.packages("fpc")
# library(fpc)
# Para aplicar DBSCAN escalamos el dataset de las variables numéricas
# (no utilizaremos el dataset normalizado)
# Primero eliminamos las binomiales del estudio
vars_binarias <- sapply(df_num, function(x) length(unique(x)) == 2)
df_numNoB <- df_num[, !vars_binarias]
# Escalamos el dataset (media = 0, sd = 1)
df_scale <- scale(df_numNoB)
# Aplicamos DBSCAN (1er ajuste eps y minPts)
modeloTest_dbscan <- dbscan::dbscan(df_scale, eps = 0.3, minPts = 4)
# Asignamos el cluster a cada observación
df_dbscan <- as.data.frame(df_scale)
df_dbscan$cluster_dbscan <- as.factor(modeloTest_dbscan$cluster)
# Visualización del clustering (reducción a 2D mediante PCA)
library(factoextra)
fviz_cluster(modeloTest_dbscan, data = df_scale,
stand = FALSE,
geom = "point",
main = "Cluster plot DBSCAN")
Tras aplicar el algoritmo DBSCAN sobre las variables numéricas normalizadas, utilizando como parámetros \(\varepsilon = 0.3\) y = 4, el gráfico muestra una nube de puntos sin diferenciación visible entre grupos. La ausencia de coloración por clusters indica que, bajo estos parámetros, DBSCAN no ha identificado agrupaciones diferenciadas. Esto puede deberse a que el valor de \(\varepsilon\) es demasiado pequeño para la estructura de los datos, de modo que la mayoría de los puntos no cumplen el requisito de vecinos mínimos, siendo etiquetados como ruido () o asignados a Cluster 0.
Esto sucede cuando los parámetros de DBSCAN no están correctamente ajustados. En este contexto, el gráfico de distancias kNN () ayuda a la elección de \(\varepsilon\). El punto de inflexión en la curva suele corresponder al valor de \(\varepsilon\).
kNN <- kNNdist(df_scale, k = 4)
# Calculamos la distancia al 4º vecino más cercano para cada punto
knn_dist <- kNNdist(df_scale, k = 4)
# Extraemos el vector de distancias
knn_vector <- as.numeric(knn_dist)
# Ordenamos los valores para graficar el codo
knn_sorted <- sort(knn_vector)
# Visualizamos el gráfico
plot(knn_sorted, type = "l", main = "kNNdistplot k = 4",
ylab = "4-NN distance", xlab = "Points sorted by distance")
# Buscamos el mayor cambio abrupto detectado en la curva
diffs <- diff(knn_sorted)
max_jump_index <- which.max(diffs)
eps_codo <- knn_sorted[max_jump_index]
cat("El valor aproximado del codo es:", round(eps_codo, 2), "\n")
## El valor aproximado del codo es: 2.94
El valor óptimo de \(\varepsilon\) en DBSCAN, utilizando el gráfico con \(k = 4\) resulta entre los valores y . Un segundo codo, aparece entre y .
Como valor principal para \(\varepsilon \approx 2.0\), DBSCAN clasifica la mayoría de puntos como , sin formar clusters significativos.
La homogeneidad del dataset y la ausencia de agrupaciones densas hacen que DBSCAN no detecte clusters relevantes para \(\varepsilon\) aunque el valor calculado se estime en
eps
y minPts
# Función de ajuste del modelo DBSCAN
ajuste_dbscan <- function(data, eps, minPts) {
modelo_dbscan <- dbscan::dbscan(data, eps = eps, minPts = minPts)
return(modelo_dbscan)
}
# Función de visualización del modelo DBSCAN
visual_dbscan <- function(data, modelo_dbscan) {
# Reducción a 2 dimensiones con PCA
red <- prcomp(data, scale. = TRUE)
coords <- data.frame(Dim1 = red$x[,1], Dim2 = red$x[,2])
# El ruido (no-cluster) en DBSCAN es "0"
coords$Cluster <- as.factor(modelo_dbscan$cluster)
# Paleta: primer color para el "ruido", resto para clusters
n_clusters <- length(unique(coords$Cluster))
paleta <- c("grey40", brewer.pal(min(8, n_clusters-1), "Set1"))
# Corrige colores si solo hay ruido
if (n_clusters == 1 && levels(coords$Cluster) == "0") {
paleta <- "grey40"
}
titulo <- sprintf("DBSCAN: eps = %.2f | minPts = %d | clusters = %d",
eps, minPts, n_clusters - any(levels(coords$Cluster) == "0"))
print(
ggplot(coords, aes(x = Dim1, y = Dim2, color = Cluster)) +
geom_point(size = 1.1, alpha = 0.8) +
scale_color_manual(values = paleta[seq_len(n_clusters)], name = "Cluster") +
labs(
title = titulo,
x = "Componente Principal 1",
y = "Componente Principal 2"
) +
theme_minimal(base_size = 14)
)
}
# Vectores de parámetros a testear
epsilons <- c(2.06, 2.09, 2.10, 2.11)
minPts_vals <- c(14, 15, 16)
# Bucle para automatizar
for (eps in epsilons) {
for (minPts in minPts_vals) {
cat("\n---\nProbanddo eps =", eps, "y minPts =", minPts, "\n")
modelo_dbscan <- ajuste_dbscan(df_scale, eps = eps, minPts = minPts)
visual_dbscan(df_scale, modelo_dbscan)
}
}
##
## ---
## Probanddo eps = 2.06 y minPts = 14
##
## ---
## Probanddo eps = 2.06 y minPts = 15
##
## ---
## Probanddo eps = 2.06 y minPts = 16
##
## ---
## Probanddo eps = 2.09 y minPts = 14
##
## ---
## Probanddo eps = 2.09 y minPts = 15
##
## ---
## Probanddo eps = 2.09 y minPts = 16
##
## ---
## Probanddo eps = 2.1 y minPts = 14
##
## ---
## Probanddo eps = 2.1 y minPts = 15
##
## ---
## Probanddo eps = 2.1 y minPts = 16
##
## ---
## Probanddo eps = 2.11 y minPts = 14
##
## ---
## Probanddo eps = 2.11 y minPts = 15
##
## ---
## Probanddo eps = 2.11 y minPts = 16
En DBSCAN no existe un método exacto y universal para determinar los valores óptimos de epsilon (\(\varepsilon\)) y minPts.
Con estos parámetros ( en torno a 2.94 y valores crecientes de ), DBSCAN ha detectado únicamente unos pocos clusters muy pequeños y una gran mayoría de puntos como ruido. La inspección de los resultados evidencia que, aunque se identifican algunos clusters, estos son minoritarios en comparación con el conjunto global de datos. Además, la localización de estos grupos no sigue una estructura, lo que refuerza la idea de que no se trata de agrupamientos naturales.
Añadir que para los valores probados de eps y minPts, la gran mayoría de los puntos (>99%) son clasificados como ruido y solo una fracción mínima de observaciones son agrupadas en 2-4 clusters insignificantes en volumnen.
El proceso de ajuste y selección de parámetros evidencia que DBSCAN no resulta adecuado para la estructura del dataset que estamos analizando, ya que no existe una densidad de puntos diferenciada como para justificar la existencia de clusters naturales. Prácticamente la totalidad de las observaciones son clasificadas como ruido, o bien los clusters detectados son insignificantes en tamaño y no pueden ser interpretados.
Por tanto, este análisis sugiere que el dataset es homogéneo y no presenta agrupaciones naturales basadas en densidad.
# La media del índice de silueta es la medida estándar para evaluar la calidad del agrupamiento
sil <- silhouette(as.numeric(modelo_dbscan$cluster), dist(df_scale))
sil_df <- as.data.frame(sil)
mean_sil <- mean(sil_df$sil_width)
cat("Índice de silueta medio:", round(mean_sil, 4), "\n")
## Índice de silueta medio: -0.2049
n_clusters <- length(unique(modelo_dbscan$cluster[modelo_dbscan$cluster != 0]))
if (n_clusters < 2) {
cat("No se puede calcular ni graficar la medición silueta porque el número de clusters < 2.\n")
} else {
sil <- silhouette(as.numeric(modelo_dbscan$cluster), dist(df_scale))
fviz_silhouette(sil)
}
## cluster size ave.sil.width
## 0 0 9868 -0.21
## 1 1 56 0.07
## 2 2 16 0.19
## 3 3 44 0.10
## 4 4 16 0.37
Se ha intentado calcular el índice de silueta medio para evaluar la calidad del agrupamiento obtenido con DBSCAN. Sin embargo, dado que el algoritmo no ha identificado más de un cluster válido (clasificadas como ruido), el índice de silueta no es aplicable y el resultado obtenido ha sido (no data).
Este hecho confirma la ausencia de subestructura agrupada en el dataset y la baja calidad del agrupamiento, pues no existe separación entre grupos que pueda ser evaluada mediante esta métrica.
# DBSCAN suele asignar el valor 0 o -1 a los puntos ruido
ruido <- sum(modelo_dbscan$cluster == 0)
porc_ruido <- ruido / length(modelo_dbscan$cluster) * 100
cat("Porcentaje de puntos clasificados como ruido:", round(porc_ruido, 2), "%\n")
## Porcentaje de puntos clasificados como ruido: 98.68 %
El porcentaje de puntos clasificados como ruido por el algoritmo DBSCAN ha sido del . Esto significa que ninguna de las observaciones ha sido agrupada en clusters válidos según los parámetros seleccionados. Este resultado confirma, la ausencia de agrupamientos naturales basados en densidad en el conjunto de datos.
Llegado a este punto, no podemos comparar los modelos resueltos con k-means, k-medians con DBSCAN por la inexistencia de resultados de éste último.
A nivel general, comparto una tabla compartiva:
Notas
El escalado o la normalización de las variables es esencial cuando las variables originales se encuentran en unidades o escalas diferentes, ya que la mayoría de los algoritmos de clustering trabajan sobre medidas de distancia. Si todas las variables están ya en la misma escala, o estan standarizadas, no es necesario aplicar un escalado adicional.
En presencia de , el algoritmo k-means resulta especialmente sensible a estos valores extremos, mientras que k-medians y DBSCAN muestran mayor robustez. En el análisis los deben ser gestionados según el objetivo del estudio, aunque DBSCAN los identifica como ruido.
Finalmente, en conjuntos de datos con variables mixtas (categóricas y numéricas), se requieren algoritmos o medidas de distancia específicas, como la distancia de Gower, ya que los algoritmos aquí comparados k-means, k-medians y DBSCAN solo trabajan correctamente con variables numéricas.
\ Presenta baja resistencia a clusters de distinto tamaño o densidad. Este algoritmo optimiza la partición de los datos minimizando la suma de las distancias cuadradas a los centroides. Como resultado, tiende a formar grupos de tamaño y densidad similares. En presencia de un cluster grande y denso junto a otro pequeño o disperso, k-means puede asignar incorrectamente puntos, mezclando regiones diferentes o ignorando clusters minoritarios, debido a la influencia desproporcionada del cluster más grande en la función objetivo.
\ También muestra baja resistencia a diferencias de tamaño y densidad entre clusters, aunque es más robusto frente a outliers que k-means. Al utilizar la mediana como centroide, disminuye la influencia de valores extremos, pero sigue favoreciendo la partición en grupos de tamaño y densidad similares.
\ Presenta alta resistencia a la existencia de clusters de distinto tamaño, forma y densidad. DBSCAN identifica agrupamientos basándose en regiones de alta densidad, lo que le permite detectar tanto clusters pequeños y densos como clusters grandes y dispersos, siempre que cumplan los parámetros de densidad ( y ). Además, no requiere predefinir el número de clusters.
\ En aplicaciones reales, los datos suelen formar agrupaciones que no son necesariamente simétricas, homogéneas ni esféricas si no que presentan patrones de agrupamiento variados en tamaño y concentración. Por este motivo, los algoritmos con alta resistencia a la heterogeneidad en tamaño y densidad, como DBSCAN, son preferibles cuando se espera descubrir patrones complejos en los datos. Por el contrario, los algoritmos particionales como k-means y k-medians pueden perder o mezclar grupos importantes, cuentionando su calidad.
Este apartado a sido resuelto en cada uno los anteriores subapartados del ejercicio, justificando los resultados obtenidos, llegando a la misma conclusión una y otra vez: la alta homogeneidad y la ausencia de regiones densamente pobladas o dicho de otro modo, ausencia de huecos hacen que los clusters detectados no sean significativos. Por tanto, no es válido utilizar DBSCAN para identificar agrupamientos en este conjunto de datos, y los resultados obtenidos responden a ajustes artificiales que a la evidencia de la presencia de un agrupamiento real.
Nuestro dataset no contiene información sobre el diagnóstico real de diabetes para los pacientes analizados, por lo que no es posible abordar un problema de clasificación supervisada. En ausencia de la variable objetivo, las técnicas de modelado supervisado como los árboles de decisión no pueden aplicarse directamente. Como alternativa, pueden emplearse técnicas de análisis no supervisado para segmentar la población en perfiles o grupos de características similares, lo que puede servir como base para identificar posibles factores de riesgo, pero nunca como predicción clínica real. Por tanto para abordar el siguiente ejercicio, valoramos crear una varible target llamada “Diabetes” que sera una variable simulada.
A partir de esta premnisa, se define una variable objetivo simulada aplicando los criterios clínicos extraídos de fuentes internacionales el cual justifica que los individuos/pacientes que sus valores de IMC\(\geq\)30 y/o glucosa en ayunas \(>\)125mg/dL tienen una mayor predisposición a desarrollar Diabetes tipo 2, según la OMS y Hu et al., 2001.
La elección del criterio lógico para definir la variable objetivo depende del propósito clínico del estudio:
# Copiamos el dataset original para preservar los datos brutos
df2 <- df
# Creamos la variable objetivo "Diabetes" usando los criterios clínicos
df2$Diabetes <- ifelse(df$BMI >= 30 & df$Fasting_Blood_Glucose > 126, 1, 0)
# Comprobamos la distribución de la variable simulada
head(df2, 10)
## Age Sex Ethnicity BMI Waist_Circumference Fasting_Blood_Glucose HbA1c
## 1 58 Female White 35.8 83.4 123.9 10.9
## 2 48 Male Asian 24.1 71.4 183.7 12.8
## 3 34 Female Black 25.0 113.8 142.0 14.5
## 4 62 Male Asian 32.7 100.4 167.4 8.8
## 5 27 Female Asian 33.5 110.8 146.4 7.1
## 6 40 Female Asian 33.6 96.1 75.0 13.5
## 7 58 Male Black 33.2 100.0 97.7 13.3
## 8 38 Female Hispanic 26.9 105.0 80.2 10.9
## 9 42 Male White 27.0 115.4 83.9 7.0
## 10 30 Male White 24.0 74.6 72.0 14.0
## Blood_Pressure_Systolic Blood_Pressure_Diastolic Cholesterol_Total
## 1 152 114 197.8
## 2 103 91 261.6
## 3 179 104 261.0
## 4 176 118 183.4
## 5 122 97 203.2
## 6 170 90 152.3
## 7 131 80 199.8
## 8 121 83 154.0
## 9 132 118 280.9
## 10 146 83 250.0
## Cholesterol_HDL Cholesterol_LDL GGT Serum_Urate Physical_Activity_Level
## 1 50.2 99.2 37.5 7.2 Moderate
## 2 62.0 146.4 88.5 6.1 Moderate
## 3 32.1 164.1 56.2 6.9 Low
## 4 41.1 84.0 34.4 5.4 Low
## 5 53.9 92.8 81.9 7.4 Moderate
## 6 44.5 190.0 77.5 6.4 Low
## 7 77.9 73.4 52.1 4.7 High
## 8 69.7 122.2 72.0 5.6 Moderate
## 9 73.2 97.4 76.4 6.2 Low
## 10 53.3 170.7 14.5 6.9 High
## Dietary_Intake_Calories Alcohol_Consumption Smoking_Status
## 1 1538 Moderate Never
## 2 2653 Moderate Current
## 3 1684 Heavy Former
## 4 3796 Moderate Never
## 5 3161 Heavy Current
## 6 3460 None Never
## 7 3107 Moderate Never
## 8 2390 Heavy Current
## 9 3844 None Former
## 10 2230 Moderate Former
## Family_History_of_Diabetes Previous_Gestational_Diabetes Diabetes
## 1 0 1 0
## 2 0 1 0
## 3 1 0 0
## 4 1 0 1
## 5 0 0 1
## 6 1 1 0
## 7 0 0 0
## 8 0 1 0
## 9 1 0 0
## 10 1 0 0
# Para la integridad de los datos brutos, volvemos a crear una copia exacta del dataset
df_factor <- df2
binari <- names(df_factor)[sapply(df, function(x) length(unique(x)) == 2)]
# Conviertimos las variables binomiales y las categoricas a factor
library(dplyr)
df_factor <- df_factor %>%
mutate(across(all_of(binari), as.factor)) %>%
mutate(across(where(is.character), as.factor))
head(df_factor, 10)
## Age Sex Ethnicity BMI Waist_Circumference Fasting_Blood_Glucose HbA1c
## 1 58 Female White 35.8 83.4 123.9 10.9
## 2 48 Male Asian 24.1 71.4 183.7 12.8
## 3 34 Female Black 25.0 113.8 142.0 14.5
## 4 62 Male Asian 32.7 100.4 167.4 8.8
## 5 27 Female Asian 33.5 110.8 146.4 7.1
## 6 40 Female Asian 33.6 96.1 75.0 13.5
## 7 58 Male Black 33.2 100.0 97.7 13.3
## 8 38 Female Hispanic 26.9 105.0 80.2 10.9
## 9 42 Male White 27.0 115.4 83.9 7.0
## 10 30 Male White 24.0 74.6 72.0 14.0
## Blood_Pressure_Systolic Blood_Pressure_Diastolic Cholesterol_Total
## 1 152 114 197.8
## 2 103 91 261.6
## 3 179 104 261.0
## 4 176 118 183.4
## 5 122 97 203.2
## 6 170 90 152.3
## 7 131 80 199.8
## 8 121 83 154.0
## 9 132 118 280.9
## 10 146 83 250.0
## Cholesterol_HDL Cholesterol_LDL GGT Serum_Urate Physical_Activity_Level
## 1 50.2 99.2 37.5 7.2 Moderate
## 2 62.0 146.4 88.5 6.1 Moderate
## 3 32.1 164.1 56.2 6.9 Low
## 4 41.1 84.0 34.4 5.4 Low
## 5 53.9 92.8 81.9 7.4 Moderate
## 6 44.5 190.0 77.5 6.4 Low
## 7 77.9 73.4 52.1 4.7 High
## 8 69.7 122.2 72.0 5.6 Moderate
## 9 73.2 97.4 76.4 6.2 Low
## 10 53.3 170.7 14.5 6.9 High
## Dietary_Intake_Calories Alcohol_Consumption Smoking_Status
## 1 1538 Moderate Never
## 2 2653 Moderate Current
## 3 1684 Heavy Former
## 4 3796 Moderate Never
## 5 3161 Heavy Current
## 6 3460 None Never
## 7 3107 Moderate Never
## 8 2390 Heavy Current
## 9 3844 None Former
## 10 2230 Moderate Former
## Family_History_of_Diabetes Previous_Gestational_Diabetes Diabetes
## 1 0 1 0
## 2 0 1 0
## 3 1 0 0
## 4 1 0 1
## 5 0 0 1
## 6 1 1 0
## 7 0 0 0
## 8 0 1 0
## 9 1 0 0
## 10 1 0 0
# Verificamos
str(df_factor)
## 'data.frame': 10000 obs. of 21 variables:
## $ Age : int 58 48 34 62 27 40 58 38 42 30 ...
## $ Sex : Factor w/ 2 levels "Female","Male": 1 2 1 2 1 1 2 1 2 2 ...
## $ Ethnicity : Factor w/ 4 levels "Asian","Black",..: 4 1 2 1 1 1 2 3 4 4 ...
## $ BMI : num 35.8 24.1 25 32.7 33.5 33.6 33.2 26.9 27 24 ...
## $ Waist_Circumference : num 83.4 71.4 113.8 100.4 110.8 ...
## $ Fasting_Blood_Glucose : num 124 184 142 167 146 ...
## $ HbA1c : num 10.9 12.8 14.5 8.8 7.1 13.5 13.3 10.9 7 14 ...
## $ Blood_Pressure_Systolic : int 152 103 179 176 122 170 131 121 132 146 ...
## $ Blood_Pressure_Diastolic : int 114 91 104 118 97 90 80 83 118 83 ...
## $ Cholesterol_Total : num 198 262 261 183 203 ...
## $ Cholesterol_HDL : num 50.2 62 32.1 41.1 53.9 44.5 77.9 69.7 73.2 53.3 ...
## $ Cholesterol_LDL : num 99.2 146.4 164.1 84 92.8 ...
## $ GGT : num 37.5 88.5 56.2 34.4 81.9 77.5 52.1 72 76.4 14.5 ...
## $ Serum_Urate : num 7.2 6.1 6.9 5.4 7.4 6.4 4.7 5.6 6.2 6.9 ...
## $ Physical_Activity_Level : Factor w/ 3 levels "High","Low","Moderate": 3 3 2 2 3 2 1 3 2 1 ...
## $ Dietary_Intake_Calories : int 1538 2653 1684 3796 3161 3460 3107 2390 3844 2230 ...
## $ Alcohol_Consumption : Factor w/ 3 levels "Heavy","Moderate",..: 2 2 1 2 1 3 2 1 3 2 ...
## $ Smoking_Status : Factor w/ 3 levels "Current","Former",..: 3 1 2 3 1 3 3 1 2 2 ...
## $ Family_History_of_Diabetes : Factor w/ 2 levels "0","1": 1 1 2 2 1 2 1 1 2 2 ...
## $ Previous_Gestational_Diabetes: Factor w/ 2 levels "0","1": 2 2 1 1 1 2 1 2 1 1 ...
## $ Diabetes : num 0 0 0 1 1 0 0 0 0 0 ...
# partición 1 p = 0.7
set.seed(123) # Fijamos la semilla para reproducibilidad
indice1 <- createDataPartition(df_factor$Diabetes, p = 0.7, list = FALSE)
train1 <- df_factor[indice1, ]
test1 <- df_factor[-indice1, ]
# Convertimos la variable objetivo en factor para evitar errores
train1$Diabetes <- factor(train1$Diabetes)
test1$Diabetes <- factor(test1$Diabetes)
# Verificamos tamaños
cat("Observaciones en entrenamiento:", nrow(train1), "\n")
## Observaciones en entrenamiento: 7000
cat("Observaciones en test:", nrow(test1), "\n")
## Observaciones en test: 3000
# Configuración de validación cruzada con 10 folds
control <- trainControl(method = "cv", number = 10)
# Ajustamos el modelo de árbol de decisión con validación cruzada
modelo1 <- train(Diabetes ~ ., data = train1, method = "rpart", trControl = control)
# Evaluamos el modelo
print(modelo1)
## CART
##
## 7000 samples
## 20 predictor
## 2 classes: '0', '1'
##
## No pre-processing
## Resampling: Cross-Validated (10 fold)
## Summary of sample sizes: 6300, 6299, 6301, 6300, 6300, 6300, ...
## Resampling results across tuning parameters:
##
## cp Accuracy Kappa
## 0.00 1.0000000 1
## 0.25 1.0000000 1
## 0.50 0.7275716 0
##
## Accuracy was used to select the optimal model using the largest value.
## The final value used for the model was cp = 0.25.
# Importancia de las variables para interpretar el modelo
varImp(modelo1)
## rpart variable importance
##
## only 20 most important variables shown (out of 25)
##
## Overall
## Fasting_Blood_Glucose 100.00000
## BMI 45.57653
## Cholesterol_HDL 0.30725
## HbA1c 0.17170
## Blood_Pressure_Systolic 0.15510
## Dietary_Intake_Calories 0.13412
## Blood_Pressure_Diastolic 0.09563
## EthnicityHispanic 0.00000
## Physical_Activity_LevelModerate 0.00000
## GGT 0.00000
## Alcohol_ConsumptionModerate 0.00000
## Cholesterol_LDL 0.00000
## Serum_Urate 0.00000
## Waist_Circumference 0.00000
## Alcohol_ConsumptionNone 0.00000
## Age 0.00000
## EthnicityWhite 0.00000
## Previous_Gestational_Diabetes1 0.00000
## Physical_Activity_LevelLow 0.00000
## Family_History_of_Diabetes1 0.00000
# Gráfico del árbol de decisión para visualizar la estructura
rpart.plot(modelo1$finalModel, type = 3, box.palette = "BuGn", shadow.col = "gray", nn = TRUE)
# Evaluamos el rendimiento en el conjunto de prueba
predicciones1 <- predict(modelo1, newdata = test1)
predicciones1 <- factor(predicciones1, levels = levels(test1$Diabetes))
confusionMatrix(predicciones1, test1$Diabetes)
## Confusion Matrix and Statistics
##
## Reference
## Prediction 0 1
## 0 2223 0
## 1 2 775
##
## Accuracy : 0.9993
## 95% CI : (0.9976, 0.9999)
## No Information Rate : 0.7417
## P-Value [Acc > NIR] : <2e-16
##
## Kappa : 0.9983
##
## Mcnemar's Test P-Value : 0.4795
##
## Sensitivity : 0.9991
## Specificity : 1.0000
## Pos Pred Value : 1.0000
## Neg Pred Value : 0.9974
## Prevalence : 0.7417
## Detection Rate : 0.7410
## Detection Prevalence : 0.7410
## Balanced Accuracy : 0.9996
##
## 'Positive' Class : 0
##
Tras ajustar el árbol de decisión, se observa que las únicas variables relevantes para la clasificación son aquellas empleadas directamente en la definición de la variable objetivo simulada (BMI y glucosa en ayunas). Esto provoca que el modelo únicamente aprenda las reglas impuestas y no pueda beneficiarse de la información que aportar las demás variables del dataset. Por tanto, tenemos un problema de sobreajuste a la definición de la variable target “Diabetes” y de desbalance en la distribución de clases. Para evitar árboles triviales y obtener un análisis más realista, es recomendable introducir aleatoriedad o criterios adicionales en la simulación de la variable objetivo, así como aplicar técnicas de balanceo en caso de desproporción severa entre clases.
# Simulacion de nueva variable target "Diabetes"
# A partir de las sigientes variables BMI, Fasting_Blood_Glucose, Age,
# Family_History_of_Diabetes, Physical_Activity_Level creamos un score ponderado
# y agregamos + aleatoriedad
set.seed(123)
df_fa <- df_factor
df_fa$score <- 0.4 * (df$BMI >= 30) +
0.4 * (df$Fasting_Blood_Glucose > 126) +
0.1 * (df$Age > 50) +
0.1 * (df$Family_History_of_Diabetes == 1) +
0.05 * (df$Physical_Activity_Level == "Low") +
runif(nrow(df), -0.05, 0.05) # aleatoriedad leve
# Definimos Diabetes si score > 0.5
df_fa$Diabetes <- ifelse(df_fa$score > 0.5, 1, 0)
df_fa$Diabetes <- factor(df_fa$Diabetes)
df_fa$score <- NULL # Eliminamoa la columna "score" del dataframe
df_fa <- df_fa[, setdiff(names(df_fa), "score")]
head(df_fa)
## Age Sex Ethnicity BMI Waist_Circumference Fasting_Blood_Glucose HbA1c
## 1 58 Female White 35.8 83.4 123.9 10.9
## 2 48 Male Asian 24.1 71.4 183.7 12.8
## 3 34 Female Black 25.0 113.8 142.0 14.5
## 4 62 Male Asian 32.7 100.4 167.4 8.8
## 5 27 Female Asian 33.5 110.8 146.4 7.1
## 6 40 Female Asian 33.6 96.1 75.0 13.5
## Blood_Pressure_Systolic Blood_Pressure_Diastolic Cholesterol_Total
## 1 152 114 197.8
## 2 103 91 261.6
## 3 179 104 261.0
## 4 176 118 183.4
## 5 122 97 203.2
## 6 170 90 152.3
## Cholesterol_HDL Cholesterol_LDL GGT Serum_Urate Physical_Activity_Level
## 1 50.2 99.2 37.5 7.2 Moderate
## 2 62.0 146.4 88.5 6.1 Moderate
## 3 32.1 164.1 56.2 6.9 Low
## 4 41.1 84.0 34.4 5.4 Low
## 5 53.9 92.8 81.9 7.4 Moderate
## 6 44.5 190.0 77.5 6.4 Low
## Dietary_Intake_Calories Alcohol_Consumption Smoking_Status
## 1 1538 Moderate Never
## 2 2653 Moderate Current
## 3 1684 Heavy Former
## 4 3796 Moderate Never
## 5 3161 Heavy Current
## 6 3460 None Never
## Family_History_of_Diabetes Previous_Gestational_Diabetes Diabetes
## 1 0 1 0
## 2 0 1 0
## 3 1 0 1
## 4 1 0 1
## 5 0 0 1
## 6 1 1 1
# Eliminamos la columna score
df_factor2 <- df_fa
head(df_factor2)
## Age Sex Ethnicity BMI Waist_Circumference Fasting_Blood_Glucose HbA1c
## 1 58 Female White 35.8 83.4 123.9 10.9
## 2 48 Male Asian 24.1 71.4 183.7 12.8
## 3 34 Female Black 25.0 113.8 142.0 14.5
## 4 62 Male Asian 32.7 100.4 167.4 8.8
## 5 27 Female Asian 33.5 110.8 146.4 7.1
## 6 40 Female Asian 33.6 96.1 75.0 13.5
## Blood_Pressure_Systolic Blood_Pressure_Diastolic Cholesterol_Total
## 1 152 114 197.8
## 2 103 91 261.6
## 3 179 104 261.0
## 4 176 118 183.4
## 5 122 97 203.2
## 6 170 90 152.3
## Cholesterol_HDL Cholesterol_LDL GGT Serum_Urate Physical_Activity_Level
## 1 50.2 99.2 37.5 7.2 Moderate
## 2 62.0 146.4 88.5 6.1 Moderate
## 3 32.1 164.1 56.2 6.9 Low
## 4 41.1 84.0 34.4 5.4 Low
## 5 53.9 92.8 81.9 7.4 Moderate
## 6 44.5 190.0 77.5 6.4 Low
## Dietary_Intake_Calories Alcohol_Consumption Smoking_Status
## 1 1538 Moderate Never
## 2 2653 Moderate Current
## 3 1684 Heavy Former
## 4 3796 Moderate Never
## 5 3161 Heavy Current
## 6 3460 None Never
## Family_History_of_Diabetes Previous_Gestational_Diabetes Diabetes
## 1 0 1 0
## 2 0 1 0
## 3 1 0 1
## 4 1 0 1
## 5 0 0 1
## 6 1 1 1
# partición 1 p = 0.7
set.seed(123) # Fijamos la semilla para reproducibilidad
indice2 <- createDataPartition(df_factor2$Diabetes, p = 0.7, list = FALSE)
train2 <- df_factor2[indice2, ]
test2 <- df_factor2[-indice2, ]
# Convertimos la variable objetivo en factor para evitar errores
train2$Diabetes <- factor(train2$Diabetes)
test2$Diabetes <- factor(test2$Diabetes)
# Verificamos tamaños
cat("Observaciones en entrenamiento:", nrow(train2), "\n")
## Observaciones en entrenamiento: 7001
cat("Observaciones en test:", nrow(test2), "\n")
## Observaciones en test: 2999
# Configuración de validación cruzada con 10 folds
control <- trainControl(method = "cv", number = 10)
# Ajustamos el modelo de árbol de decisión con validación cruzada
modelo_rp <- train(Diabetes ~ ., data = train2, method = "rpart", trControl = control)
# Evaluamos el modelo
print(modelo_rp)
## CART
##
## 7001 samples
## 20 predictor
## 2 classes: '0', '1'
##
## No pre-processing
## Resampling: Cross-Validated (10 fold)
## Summary of sample sizes: 6301, 6300, 6302, 6301, 6301, 6301, ...
## Resampling results across tuning parameters:
##
## cp Accuracy Kappa
## 0.05111524 0.8457328 0.6865744
## 0.09448575 0.7490509 0.4958285
## 0.44795539 0.6690104 0.3136513
##
## Accuracy was used to select the optimal model using the largest value.
## The final value used for the model was cp = 0.05111524.
# Importancia de las variables para interpretar el modelo
varImp(modelo_rp)
## rpart variable importance
##
## only 20 most important variables shown (out of 25)
##
## Overall
## Fasting_Blood_Glucose 100.0000
## BMI 63.5765
## Family_History_of_Diabetes1 49.4284
## Age 40.6848
## Physical_Activity_LevelLow 11.9021
## Physical_Activity_LevelModerate 1.9038
## Cholesterol_HDL 0.4439
## EthnicityWhite 0.0000
## Smoking_StatusFormer 0.0000
## EthnicityBlack 0.0000
## Cholesterol_LDL 0.0000
## Serum_Urate 0.0000
## Alcohol_ConsumptionModerate 0.0000
## EthnicityHispanic 0.0000
## Smoking_StatusNever 0.0000
## Blood_Pressure_Systolic 0.0000
## Previous_Gestational_Diabetes1 0.0000
## HbA1c 0.0000
## Dietary_Intake_Calories 0.0000
## Waist_Circumference 0.0000
# Gráfico del árbol de decisión para visualizar la estructura
rpart.plot(modelo_rp$finalModel, type = 3, box.palette = "BuGn", shadow.col = "gray", nn = TRUE)
# Evaluamos el rendimiento en el conjunto de prueba
predicciones2 <- predict(modelo_rp, newdata = test2)
predicciones2 <- factor(predicciones2, levels = levels(test2$Diabetes))
confusionMatrix(predicciones2, test2$Diabetes)
## Confusion Matrix and Statistics
##
## Reference
## Prediction 0 1
## 0 958 118
## 1 425 1498
##
## Accuracy : 0.8189
## 95% CI : (0.8047, 0.8326)
## No Information Rate : 0.5388
## P-Value [Acc > NIR] : < 2.2e-16
##
## Kappa : 0.6298
##
## Mcnemar's Test P-Value : < 2.2e-16
##
## Sensitivity : 0.6927
## Specificity : 0.9270
## Pos Pred Value : 0.8903
## Neg Pred Value : 0.7790
## Prevalence : 0.4612
## Detection Rate : 0.3194
## Detection Prevalence : 0.3588
## Balanced Accuracy : 0.8098
##
## 'Positive' Class : 0
##
La proporción 70/30 de entrenamiento y test se seleccionó teniendo en cuenta el tamaño del dataset y la validación cruzada de 10 folds para asegurar la robustez de la estimación. Este enfoque reduce el riesgo de sobreajuste y subajuste, permitiendo una evaluación objetiva.
La selección de la proporción entre conjunto de entrenamiento y test responde a un equilibrio entre la necesidad de disponer de datos suficientes para que el modelo aprenda patrones y la garantía de contar con una muestra independiente para que evalúa el rendimiento predictivo. Las proporciones habituales, como 70/30 u 80/20, se han validad para datasets de tamaño moderado o grande. Sin embargo, si el conjunto de datos es pequeño o presenta un desbalance entre clases (en el caso anterior), puede ser preferible utilizar técnicas como la validación cruzada (k-fold cross-validation), en la que el conjunto de datos se divide en subconjuntos y el proceso de entrenamiento y evaluación se repite por veces, usando en cada ocasión un fold distinto como test y los restantes como entrenamiento. Este procedimiento permite obtener una métrica promedio más robusta y menos dependiente de una única partición aleatoria.
El sobreajuste () se produce cuando el modelo aprende no solo los patrones generales sino también el ruido y las particularidades del conjunto de entrenamiento, perdiendo capacidad de generalización ante nuevos datos. Por el contrario, el subajuste () ocurre cuando el modelo es demasiado simple y no logra capturar la complejidad de la relación entre variables, resultando en un bajo rendimiento tanto en entrenamiento como en test. La elección de la proporción de partición y la aplicación de técnicas como la validación cruzada influyen en su detección y control. Una muestra de test muy pequeña puede subestimar el sobreajuste, mientras que una de entrenamiento reducida puede favorecer el subajuste. Por tanto, la combinación de particiones estratificadas y validación cruzada proporciona un marco más seguro y objetivo para seleccionar y ajustar modelos predictivos.
El árbol de decisión () entrenado y evaluado sobre la muestra de test ha alcanzado una precisión () del 81,9% (IC 95%: 80,5–83,3%), superior al (53,9%), lo que evidencia una capacidad predictiva frente a la asignación aleatoria. El índice Kappa, con un valor de 0,63, indica un grado de acuerdo entre las predicciones del modelo y las clases reales, corrigiendo la coincidencia espererada por el azar, es decir (cuánto mejoran las predicciones del modelo respecto al azar). Analizando las métricas de la matriz de confusión, la sensibilidad (0,69%) revela que el modelo identifica correctamente el 69,3% de los individuos pertenecientes a la clase negativa (ausencia de diabetes), mientras que la especificidad (0,93) indica una excelente capacidad para reconocer la clase positiva (presencia de diabetes), acertando en el 92,7% de los casos. El valor medio de (0,81) refleja un buen equilibrio entre ambas clases, reduciendo el sesgo asociado a posibles desbalances. El valor predictivo positivo (PPV, 0,89) señala que, cuando el modelo predice ausencia de diabetes, acierta el 89% de las veces; el valor predictivo negativo (NPV, 0,78) muestra que, ante una predicción positiva, la probabilidad de acierto es del 77,9%. Finalmente, el análisis de importancia de variables revela que los factores más determinantes en la clasificación son el índice de masa corporal (BMI), los antecedentes familiares de diabetes y la edad, seguidos del nivel de actividad física, en consonancia con la literatura clínica sobre factores de riesgo para diabetes tipo 2, comentados anteriormente.
Se definen reglas de clasificación interpretables y alineadas con criterios clínicos posibles. El primer nodo separa a los individuos con IMC inferior a 30 (bajo riesgo de diabetes) de aquellos con obesidad. Dentro de este segundo grupo, la glucosa en ayunas actúa como discriminante principal, asignando diagnóstico de diabetes a quienes superan los 126 mg/dL. Para casos intermedios (obesidad pero glucosa normal), el antecedente familiar de diabetes es decisivo. Así, el modelo prioriza los factores de riesgo establecidos por la OMS y literatura médica antes compartida.
Las variables con mayor poder predictivo para el diagnóstico de diabetes tipo 2 coinciden con los principales factores de riesgo establecidos anteriormente en la creación de la variable target simulada (“Diabetes”). La glucosa en ayunas (), con una importancia relativa del 100%, es el marcador diagnóstico, ya que valores superiores a 126 mg/dL constituyen el criterio estándar para la confirmación de diabetes, según la Organización Mundial de la Salud. El índice de masa corporal (IMC, ), con un peso del 63,6%, refleja el impacto de la obesidad sobre el riesgo metabólico: individuos con IMC mayor o igual a 30 presentan una probabilidad significativamente mayor de desarrollar diabetes debido a la resistencia a la insulina y la inflamación crónica asociada. Los antecedentes familiares de diabetes (, 49,4%) actúan como un factor de riesgo no modificable, indicando una predisposición genética relevante en la aparición de la enfermedad. La edad (40,7%) incrementa el riesgo progresivamente, dado que la incidencia de diabetes tipo 2 aumenta con el envejecimiento, asociado a cambios fisiológicos y estilos de vida menos activos. El nivel de actividad física bajo o moderado (, 11,9% y 1,9%, respectivamente) también influye, ya que la inactividad física contribuye al deterioro. Por último, el colesterol HDL (, 0,44%) tiene un papel protector: niveles bajos se asocian a un mayor riesgo cardiovascular y metabólico. Estas estadísticas refuerzan la validez clínica del modelo.
En el caso de que la variable objeto de estudio sea cuantitativa continua, la capacidad predictiva del modelo debe evaluarse mediante criterios de error específicos como el error cuadrático medio (MSE), el error absoluto medio (MAE) o el coeficiente de determinación (\(R^2\)), que cuantifican la discrepancia entre los valores predichos y los observados. Sin embargo, cuando la variable objetivo es binomial, como en nuestro caso (diagnóstico de diabetes: sí/no), estos indicadores no son aplicables. En su lugar, la capacidad predictiva se determina a partir de métricas propias de clasificación, como la exactitud, la sensibilidad, la especificidad, los valores predictivos positivo y negativo, el índice Kappa y el área bajo la curva ROC (AUC), que permiten valorar la habilidad del modelo para discriminar correctamente entre las dos clases.
set.seed(123)
# Árbol sin poda (cp=0 crece hasta el máximo)
arbol_nopoda <- rpart(Diabetes ~ ., data = train2, method = "class", control = rpart.control(cp = 0))
# Predicción y métricas en test
pred_nopoda <- predict(arbol_nopoda, newdata = test2, type = "class")
confusionMatrix(pred_nopoda, test2$Diabetes)
## Confusion Matrix and Statistics
##
## Reference
## Prediction 0 1
## 0 1250 114
## 1 133 1502
##
## Accuracy : 0.9176
## 95% CI : (0.9072, 0.9272)
## No Information Rate : 0.5388
## P-Value [Acc > NIR] : <2e-16
##
## Kappa : 0.8341
##
## Mcnemar's Test P-Value : 0.2521
##
## Sensitivity : 0.9038
## Specificity : 0.9295
## Pos Pred Value : 0.9164
## Neg Pred Value : 0.9187
## Prevalence : 0.4612
## Detection Rate : 0.4168
## Detection Prevalence : 0.4548
## Balanced Accuracy : 0.9166
##
## 'Positive' Class : 0
##
# Buscamos el cp óptimo con validación cruzada (el de menor error en "cptable")
cp_optimo <- arbol_nopoda$cptable[which.min(arbol_nopoda$cptable[,"xerror"]), "CP"]
# Árbol podado con el cp óptimo
arbol_poda <- prune(arbol_nopoda, cp = cp_optimo)
# Predicción y métricas en test
pred_poda <- predict(arbol_poda, newdata = test2, type = "class")
confusionMatrix(pred_poda, test2$Diabetes)
## Confusion Matrix and Statistics
##
## Reference
## Prediction 0 1
## 0 1112 0
## 1 271 1616
##
## Accuracy : 0.9096
## 95% CI : (0.8988, 0.9197)
## No Information Rate : 0.5388
## P-Value [Acc > NIR] : < 2.2e-16
##
## Kappa : 0.8156
##
## Mcnemar's Test P-Value : < 2.2e-16
##
## Sensitivity : 0.8040
## Specificity : 1.0000
## Pos Pred Value : 1.0000
## Neg Pred Value : 0.8564
## Prevalence : 0.4612
## Detection Rate : 0.3708
## Detection Prevalence : 0.3708
## Balanced Accuracy : 0.9020
##
## 'Positive' Class : 0
##
rpart.plot(arbol_nopoda, main = "Árbol sin podar")
rpart.plot(arbol_poda, main = "Árbol podado")
El modelo de árbol de decisión ajustado sin poda muestra una precisión del 91,8%, con índices de Kappa y balanced accuracy elevados (0,83 y 0,92, respectivamente), y valores de sensibilidad (90,4%) y especificidad (92,9%) muy equilibrados. Este modelo maximiza el ajuste al conjunto de entrenamiento, lo que puede incrementar el riesgo de sobreajuste, aunque proporciona una capacidad predictiva mejorada.
Por su parte, el árbol de decisión podado, construido seleccionando el parámetro de complejidad óptimo mediante validación cruzada, alcanza una precisión del 90,96% y mantiene una especificidad perfecta (100%), eliminando completamente los falsos positivos. La sensibilidad desciende a 80,4%, lo que implica que el modelo es menos óptimo a la hora de identificar casos negativos. Es decir, Si la sensibilidad es baja, el modelo puede generar falsos negativos (no detectar diabetes en quienes la tienen). Y, si la especificidad es alta, hay pocos falsos positivos (clasificar como enfermo a alguien sano).
Desde un punto de vista comparativo, la poda del árbol de decisión implica una reducción de la complejidad del modelo, que se traduce en la eliminación de ciertas ramas menos robustas o con escaso valor predictivo. Esta simplificación repercute en el comportamiento del clasificador: aunque el árbol podado mantiene una alta especificidad y elimina por completo los falsos positivos, lo hace a costa de una disminución de la sensibilidad, es decir, pasa por alto algunos casos positivos que el modelo sin podar sí sería capaz de identificar. Sin embargo, tiene ventajas en entornos clínicos, donde la robustez y la interpretabilidad del modelo es crucial para la toma de decisiones informadas. La elección del grado de poda debe ser balanceada, considerando el contexto clínico y los riesgos asociados tanto a los falsos negativos (pérdida de sensibilidad) como a los falsos positivos, de modo que el modelo resultante se adapte de manera óptima a los objetivos.
Nota
# Librerias
library(randomForest)
library(xgboost)
library(pROC)
# Modelo Random Forest
# Entrenamiento
rf_model <- randomForest(Diabetes ~ ., data = train2, ntree = 500, importance = TRUE)
# Predicción en test
rf_pred <- predict(rf_model, newdata = test2, type = "class")
# Matriz de confusión
confusionMatrix(rf_pred, test2$Diabetes)
## Confusion Matrix and Statistics
##
## Reference
## Prediction 0 1
## 0 1199 72
## 1 184 1544
##
## Accuracy : 0.9146
## 95% CI : (0.9041, 0.9244)
## No Information Rate : 0.5388
## P-Value [Acc > NIR] : < 2.2e-16
##
## Kappa : 0.8272
##
## Mcnemar's Test P-Value : 3.991e-12
##
## Sensitivity : 0.8670
## Specificity : 0.9554
## Pos Pred Value : 0.9434
## Neg Pred Value : 0.8935
## Prevalence : 0.4612
## Detection Rate : 0.3998
## Detection Prevalence : 0.4238
## Balanced Accuracy : 0.9112
##
## 'Positive' Class : 0
##
# Importancia de variables
varImpPlot(rf_model, main = "Importancia de variables - Random Forest")
El modelo entrenado de Random Forest presenta una precisión global () del 91,0% (IC 95%: 89,9–92,0%), sensiblemente superior al (53,9%), lo que demuestra que el clasificador supera una asignación aleatoria. El índice Kappa de 0,82 refleja un alto grado de acuerdo entre las predicciones y los valores reales, ajustando por el azar.
La sensibilidad (85,97%) indica que el modelo identifica correctamente el 85,9% de los casos positivos (clase 0), mientras que la especificidad (95,36%) muestra una excelente capacidad para reconocer los casos negativos (clase 1). El valor predictivo positivo (94,1%) implica que, cuando el modelo predice la presencia de diabetes, acierta en la gran mayoría de las ocasiones, y el valor predictivo negativo (88,8%) señala una alta fiabilidad cuando predice ausencia de enfermedad.
La de 90,7% evidencia el buen equilibrio entre ambas clases, lo que es especialmente relevante en contextos de desbalance de la variable objetivo. La baja tasa de falsos positivos y falsos negativos indica que el modelo es robusto y adecuado tanto para la detección como para la exclusión de casos de diabetes.
El gráfico de importancia de variables permite identificar los factores con mayor poder predictivo en la clasificación del riesgo de diabetes. En ambas métricas de importancia ( y ), las variables (índice de masa corporal) y (glucosa en ayunas) destacan como los determinantes, seguidas por los antecedentes familiares de diabetes, la edad y el nivel de actividad física. Otras variables, como el colesterol total, GGT y el consumo calórico, muestran una contribución menor pero no despreciable, mientras que aspectos como el sexo, etnia o hábitos de consumo de alcohol presentan un impacto predictivo marginal en este modelo concreto.
# Modelo XGBoost
# Convierte la variable binaria de diagnóstico de diabetes a formato
# numérico (0/1)
train2$Diabetes_num <- ifelse(train2$Diabetes == "1", 1, 0)
test2$Diabetes_num <- ifelse(test2$Diabetes == "1", 1, 0)
# Genera una matriz de predictores en la que cada categoría de v. categóricas
# es representada como una columna binaria (0/1)
# One-hot encoding convierte las variables categóricas en variables dummy
X_train <- model.matrix(Diabetes ~ . - 1, data = train2)
X_test <- model.matrix(Diabetes ~ . - 1, data = test2)
# Eliminamos la varible target del encoding
X_train <- X_train[, colnames(X_train) != "Diabetes"]
X_test <- X_test[, colnames(X_test) != "Diabetes"]
# Convierte las matrices de predictores y las etiquetas (DMatrix)
dtrain <- xgb.DMatrix(data = X_train, label = train2$Diabetes_num)
dtest <- xgb.DMatrix(data = X_test, label = test2$Diabetes_num)
# Ajuste de la configuración del modelo para controlar complejidad y
# prevenir sobreajuste
params <- list(
objective = "binary:logistic",
eval_metric = "auc",
max_depth = 4,
eta = 0.1,
subsample = 0.8,
colsample_bytree = 0.8
)
# Entrenamos el modelo y se detiene automáticamente para evitar sobreajuste
xgb_model <- xgb.train(
params = params,
data = dtrain,
nrounds = 100,
watchlist = list(train = dtrain, test = dtest),
early_stopping_rounds = 10,
verbose = 0
)
# Matriz de confusión: Calcula la probabilidad de tener diabetes y la asigna
# a la clase 1 si la probabilidad es mayor que 0.5, 0 en caso contrario
xgb_pred_prob <- predict(xgb_model, dtest)
xgb_pred <- as.factor(ifelse(xgb_pred_prob > 0.5, "1", "0"))
# Calcula las métricas principales de clasificación
xgb_cm <- confusionMatrix(xgb_pred, test2$Diabetes)
xgb_cm
## Confusion Matrix and Statistics
##
## Reference
## Prediction 0 1
## 0 1383 0
## 1 0 1616
##
## Accuracy : 1
## 95% CI : (0.9988, 1)
## No Information Rate : 0.5388
## P-Value [Acc > NIR] : < 2.2e-16
##
## Kappa : 1
##
## Mcnemar's Test P-Value : NA
##
## Sensitivity : 1.0000
## Specificity : 1.0000
## Pos Pred Value : 1.0000
## Neg Pred Value : 1.0000
## Prevalence : 0.4612
## Detection Rate : 0.4612
## Detection Prevalence : 0.4612
## Balanced Accuracy : 1.0000
##
## 'Positive' Class : 0
##
# AUC ROC del paquete pROC: El área bajo la curva ROC (AUC) mide la capacidad
# del modelo para distinguir clases, valores cercanos a 1 y discriminación
xgb_auc <- auc(test2$Diabetes_num, xgb_pred_prob)
cat("AUC (XGBoost):", xgb_auc, "\n")
## AUC (XGBoost): 1
# Importancia de variables
importance_matrix <- xgb.importance(model = xgb_model)
xgb.plot.importance(importance_matrix, main = "Importancia de variables - XGBoost")
El modelo XGBoost alcanza uno immejorables resultados en el conjunto de test. La matriz de confusión muestra que el modelo clasifica correctamente la totalidad de los casos, tanto positivos como negativos, lo que se refleja en una del 100% (IC 95%: 99,9%–100%), un valor de igual a 1, y métricas de sensibilidad y especificidad perfectas (ambas igual a 1). El área bajo la curva ROC () también es 1, lo que implica una capacidad teórica perfecta de discriminación entre clases.
No obstante, si analizamos críticamente su desempeño tan elevado, se puede deber a varios factores, como un sobreajuste extremo (overfitting) al conjunto de entrenamiento y test, lo que puede indeicar una estructura artificial de la variable objetivo simulada o una partición no representativa, como es nuestro caso.
Respecto a la importancia de las variables, el gráfico de revela que las variables clave para el modelo han sido la propia variable objetivo simulada (), la glucosa en ayunas, el IMC, los antecedentes familiares de diabetes y la edad. El resto de variables aportan información marginal o nula al modelo.
En conclusión, aunque el modelo XGBoost proporciona métricas de ajuste y clasificación excelentes en este caso, es imprescindible contextualizar estos resultados, pues estan influenciados por la construcción de la variable objetivo de forma simulada.
El modelo es simple y fácil de interpretar, pero muestra menor sensibilidad (0.693) y tiende a subajustar si la complejidad del árbol es baja. El mejora considerablemente la exactitud (accuracy 0.910) y el índice de Kappa (0.816), eliminando falsos positivos (especificidad 1.000). Sin embargo, esto implica una ligera pérdida de sensibilidad, es decir, puede no identificar todos los casos positivos. La poda incrementa la robustez y generalización del modelo, sacrificando cierta capacidad de detección.
Por otro lado, el ofrece el mejor equilibrio en general entre sensibilidad (0.860) y especificidad (0.954), con una robustez superior frente al sobreajuste, especialmente útil con muchas variables y datos ruidosos. Su rendimiento es muy similar al del árbol podado, pero con menor varianza y mayor estabilidad al promediar múltiples árboles.
La coincide en todos los modelos: , glucosa en ayunas, antecedentes familiares de diabetes y edad, debido a la variabñe target simulada y generada por estas mismas variables.
Si buscamos la máxima precisión, Random Forest es la opción más adecuada. Si la interpretabilidad de los datos clinicos resultantes, el árbol podado aporta reglas claras y comprensibles, a costa de una ligera reducción en sensibilidad.
Resumen de las principales limitaciones observadas en el dataset tras la aplicación de modelos supervisados (árboles de decisión, Random Forest, XGBoost) y no supervisados (K-means, K-medians, PAM):
En los modelos no supervisados, la variable binomial se ha excluido, ya que solo es aplicable al subgrupo de mujeres y su inclusión podría introducir sesgo en la segmentación global. Asimismo, no se han incorporado las variables categóricas debido a que los algoritmos utilizados (K-means, K-medians, PAM) en su forma estándar solo admiten variables numéricas, su inclusión habría requerido métodos de codificación o métricas específicas, lo que no era el objetivo del presente análisis. la homogeneidad del dataset, la ausencia de una variable target real y el efecto limitado de la normalización, junto a la exclusión necesaria de ciertas variables, constituyen las principales limitaciones que condicionan la validez y aplicabilidad de los resultados obtenidos.
El uso de modelos de aprendizaje supervisado y no supervisado en el ámbito biomédico conlleva riesgos específicos derivados tanto de la naturaleza de los datos como de las propias técnicas aplicadas. En primer lugar, la ausencia de una variable objetivo real y el uso de una etiqueta simulada implica que los modelos supervisados (como árboles de decisión, Random Forest o XGBoost) sólo son capaces de predecir la regla de simulación que hemos programado, no la presencia real de diabetes. Esto puede llevar a una falsa sensación de precisión y utilidad clínica, especialmente si los resultados no se validan con datos diagnósticos reales.
Por otra parte, los modelos supervisados pueden sufrir o sobreajuste, en muestras homogéneas, lo que reduce su capacidad de generalización a otras poblaciones. La ausencia de variables clínicas relevantes, la presencia de variables poco informativas pueden también afectar negativamente la robustez y la interpretabilidad del modelo.
En cuanto a los modelos no supervisados (clustering), el principal riesgo radica en extraer conclusiones erróneas sobre la estructura de la población. En datasets muy homogéneos como el presente, los algoritmos de agrupamiento (K-means, K-medians, PAM) pueden no detectar subgrupos clínicos significativos, o bien identificar clusters artificiales sin relevancia real. Además, la interpretación clínica de los clusters obtenidos puede verse limitada si las variables utilizadas no son las más informativas o si los resultados no se contrastan.
Otro riesgo frecuente es la posible mala aplicación de técnicas de preprocesamiento, como la normalización, que puede enmascarar diferencias clínicas relevantes o forzar la homogeneidad de los datos, limitando la utilidad del análisis.
El análisis realizado demuestra que la y la son los principales condicionantes de la validez de los resultados. Los modelos supervisados ofrecen buenos resultados de ajuste respecto a la variable simulada, pero no pueden considerarse válidos para diagnóstico o predicción clínica real. La interpretación de las métricas debe hacerse siempre en función de la calidad y naturaleza de la variable target.
En modelos no supervisados, la falta de heterogeneidad y la escasa presencia de subgrupos diferenciados dificultan la segmentación efectiva. Métodos clásicos como K-means tienden a generar grupos de tamaño similar, mientras que los algoritmos más robustos (K-medians, PAM) permiten identificar, en algunos casos, subgrupos extremos, aunque su relevancia clínica puede ser limitada en ausencia de diversidad en los datos.
La normalización, aunque necesaria para comparar variables en diferentes escalas, no soluciona la falta de variabilidad interna del dataset. La exclusión de variables categóricas y binomiales, por limitaciones técnicas, puede haber reducido el potencial de segmentación, pero su inclusión directa tampoco hubiera sido adecuada sin métodos específicos de codificación o distancia.
Finalmente, la práctica ha permitido comprobar la importancia de adaptar la metodología al contexto y a la estructura del dataset. Es fundamental interpretar con cautela los resultados obtenidos y ser conscientes de las limitaciones metodológicas y de datos, para evitar tomar decisiones clínicas o de gestión basadas en evidencias parciales o poco generalizables.
Los modelos aplicados han permitido explorar las posibilidades de segmentación y predicción, pero los resultados no deben considerarse como definitivos. Conviene plantear el uso de otros modelos Por ejemplo, si se dispone de una variable dependiente continua (como glucosa o BMI), técnicas estadísticas clásicas como o permitirían detectar diferencias entre grupos. Si se cuenta con una variable objetivo binaria validada, la es el enfoque estándar para estimar riesgos individuales.
la elección del modelo debe adecuarse al objetivo y a la estructura de los datos.
Ontosight AI. (2024). Recuperado de
R Core Team. (2024). Recuperado de
¿Qué es la distancia Manhattan?. Recuperado de
Ester, M., Kriegel, H. P., Sander, J., & Xu, X. (1996). A density-based algorithm for discovering clusters in large spatial databases with noise. In (KDD’96), 226–231.
Kassambara, A. (2017). (1st ed.). STHDA.
Tan, P.-N., Steinbach, M., & Kumar, V. (2018). (2nd ed.). Pearson.
Wikipedia. (2025). . Recuperado de