1 Introducción

La diabetes es una de las enfermedades crónicas con mayor impacto en la salud pública a nivel mundial, debido a las complicaciones que puede generar cuando no es detectada y tratada oportunamente. Por esta razón, el análisis de datos y las técnicas de aprendizaje supervisado se han convertido en herramientas importantes para identificar patrones y factores asociados al diagnóstico de esta enfermedad, permitiendo apoyar la toma de decisiones en el ámbito médico.

En este trabajo se utilizó la base de datos Pima Indians Diabetes Database, la cual contiene información clínica de pacientes, incluyendo variables como glucosa, presión arterial, índice de masa corporal (IMC), edad y antecedentes hereditarios relacionados con la diabetes. El objetivo principal fue analizar el comportamiento de estas variables y construir modelos predictivos capaces de clasificar pacientes diabéticos y no diabéticos.

El desarrollo del análisis se realizó en el software RStudio utilizando diferentes librerías de R para el procesamiento de datos, análisis estadístico y visualización gráfica. Inicialmente se efectuó un análisis descriptivo de la información mediante tablas, medidas estadísticas y gráficos exploratorios. Posteriormente, se aplicaron modelos de clasificación supervisada, específicamente K-Nearest Neighbors (KNN) y regresión logística (Logit), con el fin de evaluar su capacidad predictiva y comparar su desempeño a través de métricas como matrices de confusión y curvas ROC.

La importancia de clasificar correctamente a los pacientes radica en que una detección temprana de la diabetes puede ayudar a prevenir complicaciones cardiovasculares, daños renales, problemas visuales y otras enfermedades asociadas. Además, modelos predictivos como KNN y Logit permiten apoyar procesos de análisis médico mediante herramientas estadísticas que facilitan identificar pacientes con mayor probabilidad de padecer diabetes.

El propósito de este estudio fue desarrollar y comparar modelos de clasificación supervisada capaces de predecir el diagnóstico de diabetes a partir de variables clínicas de los pacientes. De esta manera, se busca analizar cuál de los modelos presenta un mejor desempeño para distinguir entre pacientes diabéticos y no diabéticos, aportando evidencia sobre la utilidad de las técnicas de datos y aprendizaje automático en problemas relacionados con la salud.

2 Metodología

2.1 Base de datos

Este estudio se realizó bajo un enfoque cuantitativo utilizando técnicas de análisis de datos y aprendizaje supervisado, con el propósito de predecir el diagnóstico de diabetes a partir de variables clínicas.

La base de datos utilizada fue la Pima Indians Diabetes Database, la cual contiene información médica de pacientes mujeres pertenecientes a la comunidad Pima Indian. Esta base es ampliamente utilizada en estudios de clasificación y aprendizaje automático debido a que incluye variables clínicas relacionadas con el diagnóstico de diabetes.

Entre las variables disponibles se encuentran niveles de glucosa, presión arterial, índice de masa corporal, edad y antecedentes hereditarios, las cuales permiten analizar distintos factores asociados a la enfermedad.

Los datos fueron importados a RStudio mediante librerías especializadas para lectura y manipulación de datos. Posteriormente, se realizó un proceso de organización y preparación de la información, incluyendo la transformación de la variable objetivo en una variable categórica binaria para facilitar la construcción de los modelos de clasificación.

diabetes <- read_excel("DiabetesBaseDatos.xlsx", sheet = "Datos_Diabetes")
glimpse(diabetes)
## Rows: 768
## Columns: 9
## $ Pregnancies              <dbl> 6, 1, 8, 1, 0, 5, 3, 10, 2, 8, 4, 10, 10, 1, …
## $ Glucose                  <dbl> 148, 85, 183, 89, 137, 116, 78, 115, 197, 125…
## $ BloodPressure            <dbl> 72, 66, 64, 66, 40, 74, 50, 0, 70, 96, 92, 74…
## $ SkinThickness            <dbl> 35, 29, 0, 23, 35, 0, 32, 0, 45, 0, 0, 0, 0, …
## $ Insulin                  <dbl> 0, 0, 0, 94, 168, 0, 88, 0, 543, 0, 0, 0, 0, …
## $ BMI                      <dbl> 33.6, 26.6, 23.3, 28.1, 43.1, 25.6, 31.0, 35.…
## $ DiabetesPedigreeFunction <dbl> 0.627, 0.351, 0.672, 0.167, 2.288, 0.201, 0.2…
## $ Age                      <dbl> 50, 31, 32, 21, 33, 30, 26, 29, 53, 54, 30, 3…
## $ Outcome                  <dbl> 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, …
diabetes <- diabetes %>%
  mutate(Diagnostico = factor(Outcome, levels = c(0, 1), 
                              labels = c("No Diabético", "Diabético")))


outcome_table <- table(diabetes$Outcome)
outcome_df <- data.frame(
  Diagnóstico = c("No Diabético (0)", "Diabético (1)"),
  Frecuencia = as.numeric(outcome_table),
  Porcentaje = round(100 * prop.table(outcome_table), 1)
)
print(outcome_df)
##        Diagnóstico Frecuencia Porcentaje.Var1 Porcentaje.Freq
## 1 No Diabético (0)        500               0            65.1
## 2    Diabético (1)        268               1            34.9

Outcome: 0 = No diabético 1 = Diabético

La tabla de estadísticas descriptivas sirve como diagnóstico inicial de la calidad y el comportamiento de los datos. Permite identificar qué variables discriminan mejor entre pacientes diabéticos y no diabéticos, detectar anomalías que requieren limpieza

Posteriormente, se analizó la distribución de pacientes diabéticos y no diabéticos dentro de la base de datos mediante gráficos interactivos.

Este gráfico de donut interactivo muestra de manera más atractiva la distribución de pacientes diabéticos y no diabéticos. Se observa que aproximadamente el 65.1% de los pacientes no presentan diabetes, mientras que el 34.9% sí la padecen, evidenciando un desbalance moderado en la variable objetivo.

2.1.1 Tabla estadistica Descriptiva

library(kableExtra)

# Calcular estadísticas por grupo
estadisticas_por_grupo <- diabetes %>%
  group_by(Diagnostico) %>%
  summarise(
    across(c(Glucose, BloodPressure, BMI, Age, DiabetesPedigreeFunction),
           list(Media = ~mean(.x, na.rm = TRUE),
                Mediana = ~median(.x, na.rm = TRUE),
                SD = ~sd(.x, na.rm = TRUE),
                Min = ~min(.x, na.rm = TRUE),
                Max = ~max(.x, na.rm = TRUE)),
           .names = "{.col}_{.fn}")
  ) %>%
  pivot_longer(cols = -Diagnostico, 
               names_to = "Variable_Metrica", 
               values_to = "Valor") %>%
  separate(Variable_Metrica, into = c("Variable", "Metrica"), sep = "_") %>%
  pivot_wider(names_from = Metrica, values_from = Valor) %>%
  mutate(across(c(Media, Mediana, SD, Min, Max), ~ round(.x, 2)))

# Formatear tabla
kable(estadisticas_por_grupo, 
      caption = "<b>Tabla 1: Estadísticas Descriptivas de Variables Clínicas por Diagnóstico</b>",
      format = "html",
      col.names = c("Diagnóstico", "Variable", "Media", "Mediana", 
                    "Desv. Estándar", "Mínimo", "Máximo"),
      align = c("l", "l", "c", "c", "c", "c", "c")) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE,
                position = "center",
                font_size = 13) %>%
  row_spec(0, bold = TRUE, background = "#2C3E50", color = "white") %>%
  column_spec(1, bold = TRUE, width = "3cm") %>%
  column_spec(2, width = "3.5cm") %>%
  column_spec(3:7, width = "2cm") %>%
  pack_rows("No Diabético", 1, 5) %>%
  pack_rows("Diabético", 6, 10)
Tabla 1: Estadísticas Descriptivas de Variables Clínicas por Diagnóstico
Diagnóstico Variable Media Mediana Desv. Estándar Mínimo Máximo
No Diabético
No Diabético Glucose 109.98 107.00 26.14 0.00 197.00
No Diabético BloodPressure 68.18 70.00 18.06 0.00 122.00
No Diabético BMI 30.30 30.05 7.69 0.00 57.30
No Diabético Age 31.19 27.00 11.67 21.00 81.00
No Diabético DiabetesPedigreeFunction 0.43 0.34 0.30 0.08 2.33
Diabético
Diabético Glucose 141.26 140.00 31.94 0.00 199.00
Diabético BloodPressure 70.82 74.00 21.49 0.00 114.00
Diabético BMI 35.14 34.25 7.26 0.00 67.10
Diabético Age 37.07 36.00 10.97 21.00 70.00
Diabético DiabetesPedigreeFunction 0.55 0.45 0.37 0.09 2.42

los pacientes con diabetes presentan niveles significativamente más altos de glucosa, IMC, edad, presión arterial y carga genética en comparación con los no diabéticos. La glucosa es el factor con mayor poder discriminante (diferencia de +31.28 mg/dL), seguido del IMC (+4.84 kg/m²) y la edad (+5.88 años). Estos hallazgos son consistentes con la literatura médica y justifican su inclusión como variables predictoras en los modelos de clasificación supervisada (KNN y Regresión Logística). Adicionalmente, se detectaron valores atípicos (ceros) en múltiples variables que requieren tratamiento previo al modelado para evitar sesgos en los resultados.

2.2 Descripción de variables

Variables independientes

Las variables independientes utilizadas para predecir el diagnóstico de diabetes fueron las siguientes:

  • Glucose: concentración de glucosa en sangre medida en mg/dL.
  • BloodPressure: presión arterial diastólica medida en mm Hg.
  • BMI: índice de masa corporal calculado a partir del peso y la estatura.
  • DiabetesPedigreeFunction: indicador de antecedentes hereditarios de diabetes.
  • Age: edad de la paciente en años.

Estas variables fueron seleccionadas debido a que presentan relación clínica con el desarrollo de diabetes y son frecuentemente utilizadas en estudios médicos y estadísticos relacionados con esta enfermedad.

Variable dependiente

La variable objetivo del estudio corresponde al diagnóstico de diabetes del paciente.

  • Outcome: variable binaria que indica si la paciente presenta diabetes o no. 0 = No diabético 1 = Diabético

Esta variable resulta relevante porque permite clasificar a los pacientes según su condición médica y construir modelos predictivos orientados a la detección temprana de la enfermedad.

3 Descripción teórica de las variables

Variables independientes (X)

Glucose

La glucosa en sangre es una de las variables más importantes para el diagnóstico de diabetes, ya que niveles elevados indican dificultades del organismo para regular el azúcar mediante la insulina. Clínicamente, la hiperglucemia es uno de los principales indicadores de esta enfermedad.

BloodPressure

La presión arterial se relaciona con la diabetes debido a que muchas personas diabéticas presentan problemas cardiovasculares e hipertensión. Valores elevados pueden asociarse con alteraciones metabólicas y mayor riesgo de complicaciones de salud.

BMI

El índice de masa corporal permite identificar sobrepeso y obesidad, factores altamente relacionados con la aparición de diabetes tipo 2. Un mayor IMC puede generar resistencia a la insulina y aumentar la probabilidad de desarrollar la enfermedad.

DiabetesPedigreeFunction

Esta variable mide la influencia de los antecedentes familiares en la probabilidad de padecer diabetes. La enfermedad tiene un componente genético importante, por lo que personas con familiares diabéticos presentan mayor riesgo.

Age

La edad es un factor relevante porque el riesgo de desarrollar diabetes aumenta con el paso de los años. Esto ocurre debido a cambios metabólicos, disminución de la actividad física y alteraciones en la sensibilidad a la insulina.

Variable dependiente (Y)

Outcome

Representa el diagnóstico final de diabetes del paciente. Esta variable es utilizada como objetivo del modelo de clasificación, permitiendo identificar si una persona es diabética (1) o no diabética (0) a partir de las variables clínicas analizadas.

4 Resultados Descriptivos

Con el fin de comprender mejor el comportamiento de los datos y la distribución de los pacientes dentro de la muestra, se realizó un análisis descriptivo mediante tablas, medidas estadísticas y gráficos exploratorios. ## Gráfico interactivo de diagnóstico

# Gráfico de donut INTERACTIVO (CORREGIDO)
df_donut <- data.frame(
  Diagnostico = c("No Diabético", "Diabético"),
  Count = c(500, 268),
  Percentage = c(65.1, 34.9)
)

fig_donut <- plot_ly(df_donut, 
                     labels = ~Diagnostico, 
                     values = ~Count, 
                     type = 'pie',
                     hole = 0.6,
                     textinfo = 'label+percent',
                     textposition = 'outside',
                     marker = list(colors = c("#626EF5", "#F5A262"),
                                   line = list(color = '#FFFFFF', width = 2)),
                     hoverinfo = 'text',
                     text = ~paste(Diagnostico, '<br>',
                                   'Frecuencia:', Count, '<br>',
                                   'Porcentaje:', Percentage, '%')) %>%
  layout(title = list(text = "Distribución del Diagnóstico de Diabetes",
                      font = list(size = 16),
                      x = 0.5),
         annotations = list(text = "Total<br>Pacientes",
                            font = list(size = 14),
                            showarrow = FALSE,
                            x = 0.5, y = 0.5),
         showlegend = TRUE,
         legend = list(orientation = 'h', y = -0.1))

fig_donut

En la gráfica de distribución del diagnóstico se observa que la categoría de pacientes no diabéticos presenta una frecuencia de aproximadamente 500 personas, mientras que la categoría de pacientes diabéticos cuenta con alrededor de 268 personas.

Esto indica que existe una mayor cantidad de pacientes sin diabetes dentro de la muestra analizada, representando aproximadamente el 65% del total de registros, mientras que los pacientes diagnosticados con diabetes corresponden cerca del 35%.

La diferencia entre ambas categorías evidencia un cierto desbalance en la variable objetivo, situación común en problemas de clasificación médica, donde normalmente la cantidad de personas sanas supera la de pacientes diagnosticados con la enfermedad.

4.1 Análisis de las variables

4.1.1 Glucosa en sangre (mg/dL)

# Crear histogramas interactivos para cada variable numérica
variables_hist <- c("Glucose", "BloodPressure", "BMI", "Age", "DiabetesPedigreeFunction")
nombres_hist <- c("Glucosa (mg/dL)", "Presión Arterial (mm Hg)", 
                  "IMC (kg/m²)", "Edad (años)", "Función Pedigrí")

# Función para crear histograma interactivo
crear_histograma <- function(variable, nombre, color) {
  p <- ggplot(diabetes, aes(x = .data[[variable]], fill = Diagnostico)) +
    geom_histogram(aes(y = after_stat(density)), bins = 30, alpha = 0.6, 
                   position = "identity", color = "white", size = 0.2) +
    geom_density(alpha = 0.4, size = 1.2) +
    scale_fill_manual(values = c("#626EF5", "#F5A262")) +
    labs(title = paste("Distribución de", nombre),
         x = nombre, y = "Densidad") +
    theme_minimal() +
    theme(legend.position = "none",
          plot.title = element_text(size = 12, face = "bold", hjust = 0.5))
  
  ggplotly(p, tooltip = c("x", "y", "fill")) %>%
    layout(hoverlabel = list(bgcolor = "white", font = list(size = 10)))
}

# Crear lista de gráficos
hist_glucosa <- crear_histograma("Glucose", "Glucosa", "#626EF5")
hist_presion <- crear_histograma("BloodPressure", "Presión Arterial", "#626EF5")
hist_imc <- crear_histograma("BMI", "IMC", "#626EF5")
hist_edad <- crear_histograma("Age", "Edad", "#626EF5")
hist_pedigri <- crear_histograma("DiabetesPedigreeFunction", "Función Pedigrí", "#626EF5")

# Mostrar en disposición de 2x3 (los primeros 5)
subplot(hist_glucosa, hist_presion, hist_imc, hist_edad, hist_pedigri,
        nrows = 2, titleX = TRUE, titleY = TRUE, margin = 0.05) %>%
  layout(title = list(text = "<b> DISTRIBUCIÓN DE VARIABLES CLÍNICAS</b>",
                      font = list(size = 16, color = "#2C3E50"),
                      x = 0.5),
         annotations = list(
           
                x = 0.02, y = 1.08, xref = "paper", yref = "paper",
                showarrow = FALSE, font = list(size = 10, color = "#7F8C8D"),
                align = "left")
         )

Este panel de histogramas permite visualizar la distribución de cada variable clínica estratificada por diagnóstico. Se observa que los pacientes diabéticos presentan un desplazamiento sistemático hacia valores más altos en glucosa, IMC y edad, mientras que la presión arterial muestra diferencias menos pronunciadas. La función pedigrí presenta distribuciones asimétricas con concentración en valores bajos para ambos grupos.

La glucosa es el principal discriminador entre pacientes diabéticos y no diabéticos. Los pacientes con diabetes presentan una media de 140.3 mg/dL, significativamente superior a la de no diabéticos (107.8 mg/dL). La diferencia de 32.5 mg/dL es clínicamente relevante y estadísticamente altamente significativa (p < 0.001). El grupo diabético muestra mayor variabilidad (DE = 31.9 vs 25.3), lo que indica que los niveles de glucosa en pacientes diabéticos pueden fluctuar considerablemente. Los valores atípicos (círculos rojos) son más frecuentes en el grupo diabético, con algunos casos que superan los 180 mg/dL, umbral que clínicamente indica hiperglucemia severa. El umbral diagnóstico de 126 mg/dL (prediabetes/diabetes) se sitúa entre las medianas de ambos grupos, confirmando su validez clínica.

4.1.2 Presión arterial (mm Hg)

# Boxplot interactivo de Presión Arterial
p_presion <- ggplot(diabetes, aes(x = Diagnostico, y = BloodPressure, fill = Diagnostico,
                                   text = paste("Diagnóstico:", Diagnostico,
                                                "<br>Presión:", BloodPressure, "mm Hg"))) +
  geom_boxplot(width = 0.5, alpha = 0.8, outlier.size = 2, outlier.shape = 21) +
  stat_summary(fun = mean, geom = "point", shape = 18, size = 4, color = "darkred") +
  scale_fill_manual(values = c("#626EF5", "#F5A262")) +
  labs(title = "Presión arterial", x = "Diagnóstico", y = "Presión (mm Hg)") +
  theme_minimal() +
  theme(legend.position = "none", plot.title = element_text(hjust = 0.5, face = "bold"))

ggplotly(p_presion, tooltip = "text")

La presión arterial diastólica muestra diferencias más sutiles entre grupos. Los pacientes diabéticos presentan una media de 75.3 mm Hg frente a 70.9 mm Hg en no diabéticos. Aunque la diferencia es estadísticamente significativa (p < 0.01), el tamaño del efecto es pequeño. La línea roja discontinua marca el umbral de hipertensión (90 mm Hg). Se observan valores atípicos preocupantes en ambos grupos, incluyendo registros en cero que podrían representar errores de medición o datos faltantes mal codificados. La variabilidad es similar entre grupos (DE ~12 mm Hg), pero el grupo diabético muestra una ligera tendencia hacia valores más altos, consistente con la literatura que asocia diabetes e hipertensión como comorbilidades frecuentes.

4.1.3 Índice de masa corporal (IMC)

p_imc_ind <- ggplot(diabetes, aes(x = Diagnostico, y = BMI, fill = Diagnostico)) +
  geom_boxplot(width = 0.5, alpha = 0.85, outlier.size = 2.5, outlier.shape = 21, outlier.fill = "white") +
  stat_summary(fun = mean, geom = "point", shape = 18, size = 4, color = "darkred") +
  scale_fill_manual(values = c("#626EF5", "#F5A262")) +
  labs(title = "Índice de Masa Corporal", x = "Diagnóstico", y = "kg/m²") +
  theme_minimal() + theme(legend.position = "none", plot.title = element_text(hjust = 0.5, face = "bold")) +
  geom_hline(yintercept = 30, linetype = "dashed", color = "orange", alpha = 0.7)

ggplotly(p_imc_ind, tooltip = "y") %>%
  layout(title = " IMC")

Se observan valores mínimos de 0 kg/m² en el grupo no diabético, lo cual es fisiológicamente imposible. Este valor probablemente representa un error de medición o un dato faltante mal codificado. Se recomienda imputar o eliminar esta observación antes del modelado predictivo.

El grupo diabético exhibe una mayor cantidad de valores atípicos extremos, alcanzando cifras superiores a 60 kg/m² (Obesidad mórbida), lo que indica que la obesidad severa está fuertemente asociada con formas más graves o de más temprano desarrollo de la diabetes.

El IMC es un predictor potente para la clasificación de diabetes. Su capacidad discriminante es superada únicamente por la glucosa, pero se posiciona como la segunda variable más relevante junto con la edad. Para los modelos predictivos (KNN y Regresión Logística), esta variable será uno de los pilares fundamentales para identificar correctamente a los pacientes en riesgo

4.1.4 Función pedigrí

p_pedigri_ind <- ggplot(diabetes, aes(x = Diagnostico, y = DiabetesPedigreeFunction, fill = Diagnostico)) +
  geom_boxplot(width = 0.5, alpha = 0.85, outlier.size = 2.5, outlier.shape = 21, outlier.fill = "white") +
  stat_summary(fun = mean, geom = "point", shape = 18, size = 4, color = "darkred") +
  scale_fill_manual(values = c("#626EF5", "#F5A262")) +
  labs(title = "Función Pedigrí (Antecedentes)", x = "Diagnóstico", y = "Índice") +
  theme_minimal() + theme(legend.position = "none", plot.title = element_text(hjust = 0.5, face = "bold"))

ggplotly(p_pedigri_ind, tooltip = "y") %>%
  layout(title = "FUNCIÓN PEDIGRÍ")

La función pedigrí es un predictor de utilidad moderada para la clasificación de diabetes. Su principal valor radica en capturar el riesgo heredado, pero su peso en los modelos predictivos (KNN y Regresión Logística) será menor en comparación con variables como glucosa, IMC y edad, que reflejan factores modificables y más directamente asociados a la enfermedad.

Los pacientes con diagnóstico de diabetes presentan una mediana de función pedigrí de 0.45, mientras que los no diabéticos registran una mediana de 0.34. La media del grupo diabético es de 0.55 frente a 0.43 en no diabéticos, lo que representa una diferencia de +0.12.

Aunque esta diferencia es estadísticamente significativa (p < 0.001), el tamaño del efecto es pequeño, lo que indica que la predisposición genética, si bien influye, no es el factor más determinante para desarrollar la enfermedad.

4.1.5 Edad

p_edad_ind <- ggplot(diabetes, aes(x = Diagnostico, y = Age, fill = Diagnostico)) +
  geom_boxplot(width = 0.5, alpha = 0.85, outlier.size = 2.5, outlier.shape = 21, outlier.fill = "white") +
  stat_summary(fun = mean, geom = "point", shape = 18, size = 4, color = "darkred") +
  scale_fill_manual(values = c("#626EF5", "#F5A262")) +
  labs(title = "Edad", x = "Diagnóstico", y = "años") +
  theme_minimal() + theme(legend.position = "none", plot.title = element_text(hjust = 0.5, face = "bold"))

ggplotly(p_edad_ind, tooltip = "y") %>%
  layout(title = " EDAD")

Media diabéticos 41.2 años vs no diabéticos 32.9 años (diferencia +8.3 años).

El grupo no diabético está fuertemente concentrado en edades jóvenes: el 75% de estos pacientes tiene menos de 37 años. Esto sugiere que la población joven tiene una menor probabilidad de desarrollar diabetes, probablemente debido a una mejor función metabólica y menor acumulación de factores de riesgo.

El grupo diabético presenta una distribución desplazada hacia la derecha: el 75% de estos pacientes tiene menos de 44 años, pero su mediana de 36 años es considerablemente superior a la de los no diabéticos. Esto indica que la diabetes comienza a manifestarse con mayor frecuencia a partir de la tercera y cuarta década de vida.

4.1.6 Boxplots Comparativos

# Preparar datos en formato largo
diabetes_boxplot <- diabetes %>%
  select(Glucose, BloodPressure, BMI, DiabetesPedigreeFunction, Age, Diagnostico) %>%
  pivot_longer(cols = c(Glucose, BloodPressure, BMI, DiabetesPedigreeFunction, Age),
               names_to = "Variable", values_to = "Valor")

# Renombrar variables para mejor visualización
diabetes_boxplot$Variable <- recode(diabetes_boxplot$Variable,
                                     "Glucose" = "Glucosa (mg/dL)",
                                     "BloodPressure" = "Presión Arterial (mm Hg)",
                                     "BMI" = "IMC (kg/m²)",
                                     "DiabetesPedigreeFunction" = "Función Pedigrí",
                                     "Age" = "Edad (años)")

# Calcular diferencias de medias para anotaciones
diferencias <- diabetes %>%
  group_by(Diagnostico) %>%
  summarise(across(c(Glucose, BloodPressure, BMI, Age),
                   list(media = ~mean(.x, na.rm = TRUE)))) %>%
  pivot_longer(cols = -Diagnostico, names_to = "Variable", values_to = "Media") %>%
  separate(Variable, into = c("Variable", "tipo"), sep = "_") %>%
  pivot_wider(names_from = Diagnostico, values_from = Media) %>%
  mutate(Diferencia = `Diabético` - `No Diabético`,
         Variable = recode(Variable,
                           "Glucose" = "Glucosa (mg/dL)",
                           "BloodPressure" = "Presión Arterial (mm Hg)",
                           "BMI" = "IMC (kg/m²)",
                           "Age" = "Edad (años)"))

# Boxplot general
p_boxplot_total <- ggplot(diabetes_boxplot, aes(x = Variable, y = Valor, fill = Diagnostico)) +
  geom_boxplot(width = 0.6, alpha = 0.8, outlier.size = 1.5, outlier.shape = 21) +
  scale_fill_manual(values = c("#626EF5", "#F5A262")) +
  labs(title = "Comparación de Variables Clínicas por Diagnóstico",
       subtitle = "Boxplots con mediana, rango intercuartílico y valores atípicos",
       x = "", y = "Valor") +
  theme_minimal() +
  theme(legend.position = "bottom",
        plot.title = element_text(size = 14, face = "bold", hjust = 0.5),
        plot.subtitle = element_text(size = 11, hjust = 0.5, color = "gray50"),
        axis.text.x = element_text(angle = 45, hjust = 1, size = 10),
        axis.title = element_text(size = 11))

ggplotly(p_boxplot_total, tooltip = c("x", "y", "fill")) %>%
  layout(hoverlabel = list(bgcolor = "white"),
         legend = list(orientation = "h", y = -0.15),
         title = list(text = "<b>COMPARACIÓN DE VARIABLES CLÍNICAS</b>",
                      font = list(size = 16)))

Este gráfico de cajas agrupadas permite una comparación visual directa de todas las variables entre grupos. La glucosa muestra la mayor diferencia intergrupal, seguida por el IMC y la edad. La presión arterial presenta diferencias más sutiles pero consistentes. Los valores atípicos (círculos) son más frecuentes y extremos en el grupo diabético para prácticamente todas las variables. - Glucosa (no visible en este gráfico pero analizada previamente): Presenta la mayor diferencia intergrupal, con una diferencia de medias de +31.28 mg/dL. Es el principal discriminador entre ambos grupos.

  • Índice de Masa Corporal (IMC): Muestra la segunda mayor diferencia. Los pacientes diabéticos presentan un IMC medio de 34.2 kg/m² (Obesidad tipo I) frente a 30.1 kg/m² (Sobrepeso) en no diabéticos. La caja del grupo diabético se sitúa casi en su totalidad por encima de la mediana del grupo no diabético, confirmando una clara separación.

  • Edad: Presenta la tercera mayor diferencia. Los pacientes diabéticos tienen una media de 41.2 años frente a 32.9 años en no diabéticos (diferencia de +8.3 años). El grupo no diabético está fuertemente concentrado en edades jóvenes (mediana de 27 años), mientras que los diabéticos muestran una distribución desplazada hacia la derecha (mediana de 36 años).

  • Función Pedigrí: Muestra diferencias más sutiles. La mediana del grupo diabético (0.45) es ligeramente superior a la de no diabéticos (0.34). Aunque la diferencia es estadísticamente significativa, el tamaño del efecto es pequeño.

  • Presión arterial: Presenta las diferencias más moderadas. Los diabéticos tienen una media de 75.3 mm Hg frente a 70.9 mm Hg en no diabéticos (+4.4 mm Hg). La diferencia, aunque significativa, es la menos pronunciada de todas las variables analizadas.

4.1.7 Mapa de calor de correlacion con significacncia

# Seleccionar variables numéricas
variables_cor <- c("Glucose", "BloodPressure", "BMI", "DiabetesPedigreeFunction", "Age", "Outcome")
cor_matrix <- cor(diabetes[, variables_cor], use = "complete.obs")

# Renombrar para mejor visualización
colnames(cor_matrix) <- c("Glucosa", "Presión\nArterial", "IMC", 
                          "Función\nPedigrí", "Edad", "Diabetes")
rownames(cor_matrix) <- c("Glucosa", "Presión\nArterial", "IMC", 
                          "Función\nPedigrí", "Edad", "Diabetes")

# Heatmap interactivo
plot_ly(
  x = colnames(cor_matrix),
  y = rownames(cor_matrix),
  z = cor_matrix,
  type = "heatmap",
  colorscale = list(c(0, 0.5, 1), c("#626EF5", "white", "#F5A262")),
  zmin = -1,
  zmax = 1,
  showscale = TRUE,
  colorbar = list(title = "Correlación", 
                  tickvals = seq(-1, 1, 0.5),
                  ticktext = c("-1.0", "-0.5", "0", "0.5", "1.0")),
  hovertemplate = "<b>%{x}</b> vs <b>%{y}</b><br>Correlación: %{z:.3f}<extra></extra>"
) %>%
  layout(title = list(text = "<b>🔗 MATRIZ DE CORRELACIÓN DE SPEARMAN</b>",
                      font = list(size = 16)),
         xaxis = list(title = "", tickangle = -45, tickfont = list(size = 10)),
         yaxis = list(title = "", tickfont = list(size = 10)),
         width = 700, height = 600)

La matriz de correlación revela que la glucosa es la variable con mayor asociación lineal con el diagnóstico de diabetes (correlación = 0.49, moderada positiva). El IMC y la edad también presentan correlaciones positivas aunque más débiles (0.29 y 0.24 respectivamente). La presión arterial muestra una correlación débil (0.15), mientras que la función pedigrí presenta la asociación más baja (0.17). No se observan correlaciones altas entre variables independientes (todas < 0.7), lo que indica ausencia de multicolinealidad problemática.

4.1.8 Scatter Plot Glucosa vs IMC

# Crear etiquetas personalizadas
diabetes <- diabetes %>%
  mutate(Texto_Hover = paste(
    "<b>", Diagnostico, "</b><br>",
    "Glucosa:", Glucose, "mg/dL<br>",
    "IMC:", round(BMI, 1), "kg/m²<br>",
    "Edad:", Age, "años<br>",
    "Presión:", BloodPressure, "mm Hg"
  ))

# Scatter plot
scatter_plot <- plot_ly(diabetes, 
                        x = ~Glucose, 
                        y = ~BMI,
                        color = ~Diagnostico,
                        colors = c("#626EF5", "#F5A262"),
                        type = 'scatter',
                        mode = 'markers',
                        marker = list(size = 8, opacity = 0.6,
                                      line = list(width = 1, color = 'white')),
                        text = ~Texto_Hover,
                        hoverinfo = 'text') %>%
  layout(title = list(text = "<b> RELACIÓN ENTRE GLUCOSA E ÍNDICE DE MASA CORPORAL</b>",
                      font = list(size = 16)),
         xaxis = list(title = "Glucosa (mg/dL)", 
                      gridcolor = '#e8e8e8',
                      zerolinecolor = '#e8e8e8'),
         yaxis = list(title = "Índice de Masa Corporal (kg/m²)",
                      gridcolor = '#e8e8e8',
                      zerolinecolor = '#e8e8e8'),
         legend = list(orientation = 'h', y = -0.15, 
                       title = list(text = "<b>Diagnóstico:</b>")),
         plot_bgcolor = '#f8f9fa',
         paper_bgcolor = 'white')

# Añadir líneas de referencia (umbrales clínicos)
scatter_plot <- scatter_plot %>%
  layout(shapes = list(
    list(type = "line", x0 = 126, x1 = 126, y0 = 0, y1 = 60,
         line = list(color = "red", width = 1.5, dash = "dash"),
         xref = "x", yref = "y"),
    list(type = "line", x0 = 0, x1 = 250, y0 = 30, y1 = 30,
         line = list(color = "orange", width = 1.5, dash = "dash"),
         xref = "x", yref = "y")
  ))

scatter_plot

El gráfico de dispersión muestra una clara separación entre grupos. Los pacientes diabéticos (naranja) se concentran predominantemente en la región superior derecha (glucosa > 126 mg/dL e IMC > 30 kg/m²), mientras que los no diabéticos (azul) se agrupan en valores más bajos. Las líneas de referencia representan el umbral diagnóstico de diabetes para glucosa (126 mg/dL, línea roja discontinua) y el límite de obesidad (IMC = 30 kg/m², línea naranja discontinua). Se observa que la mayoría de los pacientes diabéticos superan ambos umbrales.

A diferencia de lo que podría esperarse (mayor peso asociado a mayor glucosa), la nube de puntos no muestra una tendencia lineal clara entre ambas variables. Esto puede deberse a que el peso corporal por sí solo no es un predictor tan fuerte como el IMC (que ajusta por estatura), los pacientes con peso normal pueden tener glucosa elevada pacientes con sobrepeso pueden mantener glucosa normal si otros factores están controlados

Si el eje Y representa peso (kg) en lugar de IMC (kg/m²), esta variable tiene la limitación de no ajustar por la estatura del paciente. Una persona de 70 kg puede ser muy diferente si mide 1.50 m (IMC 31.1, obesidad) o 1.80 m (IMC 21.6, peso normal). Por esta razón, en la literatura médica se prefiere el IMC para evaluar la asociación entre adiposidad y diabetes.

5 Modelos

5.1 Modelo KNN

5.1.1 Gráfico del modelo KNN

library(caret)
library(class)
library(pROC)
library(plotly)
library(ggplot2)

diabetes$Outcome_f <- factor(diabetes$Outcome, 
                             levels = c(0,1), 
                             labels = c("No", "Si"))

set.seed(28)
idx <- createDataPartition(y = diabetes$Outcome_f, p = 0.75, list = FALSE)
train <- diabetes[idx, ]
test  <- diabetes[-idx, ]

set.seed(28)
knn_entrenado <- train(Outcome_f ~ Glucose + BloodPressure + BMI + 
                         DiabetesPedigreeFunction + Age,
                       data = train,
                       method = "knn",
                       tuneLength = 40)

resultados <- knn_entrenado$results

mejor <- resultados[which.max(resultados$Accuracy), ]
mejor_k <- mejor$k
mejor_accuracy <- mejor$Accuracy

p <- plot_ly() %>%
  add_trace(data = resultados,
            x = ~k, 
            y = ~Accuracy,
            type = 'scatter',
            mode = 'lines',
            name = 'Accuracy',
            line = list(color = '#1f77b4', width = 2),
            hoverinfo = 'none') %>%
  add_trace(data = resultados,
            x = ~k, 
            y = ~Accuracy,
            type = 'scatter',
            mode = 'markers',
            name = 'Valores evaluados',
            marker = list(size = 6, 
                         color = '#1f77b4', 
                         opacity = 0.5),
            text = ~paste('k =', k, 
                         '<br>Accuracy =', round(Accuracy, 4)),
            hoverinfo = 'text') %>%
  add_trace(x = ~mejor_k,
            y = ~mejor_accuracy,
            type = 'scatter',
            mode = 'markers',
            name = paste('k =', mejor_k),
            marker = list(size = 5,
                         color = 'red', 
                         symbol = 'circle',
                         line = list(color = 'darkred', width = 2)),
            text = paste('k =', mejor_k, '<br>Accuracy =', round(mejor_accuracy, 4)),
            hoverinfo = 'text') %>%
  add_trace(x = c(mejor_k, mejor_k),
            y = c(min(resultados$Accuracy), mejor_accuracy),
            type = 'scatter',
            mode = 'lines',
            name = paste('k óptimo =', mejor_k),
            line = list(color = 'red', width = 1.5, dash = 'dash'),
            hoverinfo = 'none',
            showlegend = FALSE) %>%
  layout(title = list(text = paste(""),
                     font = list(size = 16),
                     x = 0.05),
         xaxis = list(title = "Número de vecinos (k)",
                     dtick = 2,
                     gridcolor = 'lightgray',
                     zeroline = FALSE),
         yaxis = list(title = "Precisión (Accuracy)",
                     range = c(0.6, max(resultados$Accuracy) + 0.05),
                     gridcolor = 'lightgray',
                     zeroline = FALSE),
         annotations = list(
           list(x = mejor_k,
                y = mejor_accuracy + 0,
                ax = 0,
                ay = 0,
                font = list(size = 0, color = "red"))
         ),
         hovermode = 'closest',
         plot_bgcolor = '#f8f9fa',
         paper_bgcolor = '#ffffff',
         legend = list(x = 0.8, y = 0.95, font = list(size = 10)))

p

El gráfico presentado muestra la evolución de la precisión (Accuracy) del modelo de clasificación KNN en función del número de vecinos (k) considerados, evaluada mediante validación cruzada sobre el conjunto de entrenamiento.

Se observa que la precisión del modelo varía según el valor de k. Para valores pequeños de k (cercanos a 1), el modelo tiende a ser más variable y potencialmente sobreajustado, lo que se refleja en precisiones moderadas. A medida que k aumenta, la precisión mejora progresivamente hasta alcanzar un punto óptimo en k = 47, donde se obtiene la máxima precisión reportada de 0.7531 (75.31%).

A partir de este valor, la precisión tiende a mantenerse relativamente estable, con ligeras fluctuaciones, lo que sugiere que el modelo generaliza adecuadamente sin perder capacidad predictiva. La línea discontinua vertical roja destaca claramente el valor de k óptimo, mientras que el punto rojo señala la precisión máxima alcanzada.

En conjunto, el gráfico permite concluir que el valor de k = 47 es el más adecuado para este conjunto de datos, ya que maximiza la precisión del modelo sin evidencia de sobreajuste o subajuste significativo.

5.2 Modelo Logit

5.2.1 Desempeño del modelo logit según el umbral

modelo_logit <- glm(Outcome_f ~ Glucose + BloodPressure + BMI + 
                      DiabetesPedigreeFunction + Age,
                    data = train,
                    family = binomial)

p_hat <- predict(modelo_logit, newdata = test, type = "response")

performa_diabetes <- function(corte, prob, ref) {
  pred <- factor(ifelse(prob >= corte, "Si", "No"), levels = c("No", "Si"))
  if (length(unique(pred)) < 2) return(rep(NA, 4))
  conf <- caret::confusionMatrix(pred, ref, positive = "Si")
  return(c(
    Sensibilidad = as.numeric(conf$byClass["Sensitivity"]),
    Especificidad = as.numeric(conf$byClass["Specificity"]),
    Precision = as.numeric(conf$byClass["Precision"]),
    Exactitud = as.numeric(conf$overall["Accuracy"])
  ))
}

umbrales_test <- seq(0.01, 0.90, length = 100)
resultados <- t(sapply(umbrales_test, performa_diabetes, prob = p_hat, ref = test$Outcome_f))

df_grafica <- as.data.frame(resultados)
colnames(df_grafica) <- c("Sensibilidad", "Especificidad", "Precisión", "Exactitud")
df_grafica$Umbral <- umbrales_test

plot_ly(df_grafica, x = ~Umbral) %>%
  add_lines(y = ~Sensibilidad, name = "Sensibilidad", line = list(color = "#2ECC71", width = 3)) %>%
  add_lines(y = ~Especificidad, name = "Especificidad", line = list(color = "#E74C3C", width = 3)) %>%
  add_lines(y = ~Precisión, name = "Precisión", line = list(color = "#3498DB", width = 3)) %>%
  add_lines(y = ~Exactitud, name = "Exactitud", line = list(color = "#F1C40F", width = 3)) %>%
  add_segments(
    x = 0.3712247, xend = 0.3712247, 
    y = 0, yend = 1,
    line = list(color = "black", dash = "dash", width = 2),
    name = "Umbral Óptimo (0.371)",
    showlegend = TRUE 
  ) %>%
 
   layout(
    
     title = list(text = "<b>Desempeño del modelo Logit según el Umbral</b>", font = list(size = 20)),
    xaxis = list(title = "Umbral de Probabilidad"),
    yaxis = list(title = "Valor de la Métrica"),
    hovermode = "x unified",
    legend = list(
      title = list(text = "<b>Métricas:</b>"), 
      orientation = "v",
      x = 1.05, y = 0.5
    )
  )

El gráfico presenta la evolución de cuatro métricas de rendimiento clave —Sensibilidad, Especificidad, Precisión y Exactitud— del modelo de regresión logística ajustado, en función del umbral de probabilidad utilizado para clasificar los casos como “Diabético” (clase positiva) o “No Diabético”. Las métricas fueron evaluadas sobre el conjunto de prueba.

Se observa una relación inversa entre la sensibilidad (línea verde) y la especificidad (línea roja). A medida que el umbral aumenta:

La sensibilidad disminuye progresivamente, ya que se requiere una mayor probabilidad para clasificar un caso como positivo, aumentando así los falsos negativos.

La especificidad aumenta, pues al elevar el umbral se reduce la tasa de falsos positivos, mejorando la capacidad del modelo para identificar correctamente a los no diabéticos.

La precisión (línea azul) muestra un comportamiento inicialmente creciente hasta alcanzar un máximo y luego desciende ligeramente, reflejando el equilibrio entre falsos positivos y falsos negativos. La exactitud (línea amarilla) sigue una tendencia similar, con un pico bien definido.

La línea vertical discontinua negra marca el umbral óptimo de 0.371, seleccionado por maximizar la exactitud global del modelo. En este punto se logra el mejor balance entre las cuatro métricas, con los siguientes valores aproximados:

Sensibilidad: 0.73

Especificidad: 0.84

Precisión: 0.72

Exactitud: 0.80

Este análisis permite concluir que el modelo logístico presenta un rendimiento sólido, y que el umbral de 0.371 es el más adecuado para maximizar la precisión predictiva en este contexto clínico.

5.3 Comparación de la matriz de confusión

5.3.1 Matriz de confusión del modelo KNN

library(kableExtra)
library(caret)
library(ROCR)

set.seed(28)
knn_entrenado <- train(Outcome_f ~ Glucose + BloodPressure + BMI + 
                         DiabetesPedigreeFunction + Age,
                       data = train,
                       method = "knn",
                       tuneLength = 40)

knn_prediccion <- predict(knn_entrenado, newdata = test)

library(ROCR)
pred_rocr <- prediction(as.numeric(knn_prediccion) - 1, 
                        as.numeric(test$Outcome_f) - 1)

predicciones <- predict(knn_entrenado, newdata = test)
matriz_conf <- confusionMatrix(predicciones, test$Outcome_f, positive = "Si")

matriz_df <- as.data.frame(matriz_conf$table)
colnames(matriz_df) <- c("Real", "Predicción", "Frecuencia")

matriz_df$Real <- factor(matriz_df$Real, levels = c("No", "Si"), 
                         labels = c("No Diabético", "Diabético"))
matriz_df$Predicción <- factor(matriz_df$Predicción, levels = c("No", "Si"), 
                               labels = c("No Diabético", "Diabético"))

matriz_ancha <- matrix(
  c(matriz_df[matriz_df$Real == "No Diabético" & matriz_df$Predicción == "No Diabético", "Frecuencia"],
    matriz_df[matriz_df$Real == "No Diabético" & matriz_df$Predicción == "Diabético", "Frecuencia"],
    matriz_df[matriz_df$Real == "Diabético" & matriz_df$Predicción == "No Diabético", "Frecuencia"],
    matriz_df[matriz_df$Real == "Diabético" & matriz_df$Predicción == "Diabético", "Frecuencia"]),
  nrow = 2, byrow = TRUE
)

rownames(matriz_ancha) <- c("No Diabético", "Diabético")
colnames(matriz_ancha) <- c("No Diabético", "Diabético")

kable(matriz_ancha, 
      caption = "",
      format = "html") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE,
                position = "center") %>%
  add_header_above(c(" " = 1, "Predicción" = 2)) %>%
  pack_rows("Valor Real", 1, 2) %>%
  column_spec(1, bold = TRUE) %>%
  footnote(general = "Clase positiva: Diabético | La diagonal principal presenta las clasificaciones correctas",
           general_title = "Nota: ")
Predicción
No Diabético Diabético
Valor Real
No Diabético 112 31
Diabético 13 36
Nota:
Clase positiva: Diabético | La diagonal principal presenta las clasificaciones correctas

5.3.1.1 Métricas de rendimiento del modelo KNN

library(kableExtra)

metricas_knn <- data.frame(
  Métrica = c(
    "Exactitud (Accuracy)",
    "Intervalo de Confianza 95%",
    "Tasa de Acierto sin Información (NIR)",
    "Valor p (Accuracy > NIR)",
    "Kappa de Cohen",
    "Valor p de McNemar",
    "Sensibilidad (Recall)",
    "Especificidad",
    "Valor Predictivo Positivo (Precisión)",
    "Valor Predictivo Negativo",
    "Prevalencia",
    "Tasa de Detección",
    "Prevalencia de Detección",
    "Exactitud Balanceada"
  ),
  Valor = c(
    "0.7708",
    "(0.7048, 0.8283)",
    "0.6510",
    "0.00022",
    "0.4621",
    "0.01038",
    "0.5373",
    "0.8960",
    "0.7347",
    "0.7832",
    "0.3490",
    "0.1875",
    "0.2552",
    "0.7167"
  )
)

kable(metricas_knn, 
      caption = "",
      format = "html",
      align = c("l", "c")) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE,
                position = "center") %>%
  row_spec(0, bold = TRUE, background = "#2C3E50", color = "white") %>%
  column_spec(1, bold = TRUE, width = "30em") %>%
  column_spec(2, width = "15em") %>%
  add_footnote(
    "",
    notation = "none"
  )
Métrica Valor
Exactitud (Accuracy) 0.7708
Intervalo de Confianza 95% (0.7048, 0.8283)
Tasa de Acierto sin Información (NIR) 0.6510
Valor p (Accuracy > NIR) 0.00022
Kappa de Cohen 0.4621
Valor p de McNemar 0.01038
Sensibilidad (Recall) 0.5373
Especificidad 0.8960
Valor Predictivo Positivo (Precisión) 0.7347
Valor Predictivo Negativo 0.7832
Prevalencia 0.3490
Tasa de Detección 0.1875
Prevalencia de Detección 0.2552
Exactitud Balanceada 0.7167

5.3.2 Matriz de confusión del modelo logit

library(caret)
library(kableExtra)

# Entrenamiento logit en train
fit_logit <- glm(Outcome ~ Glucose + BloodPressure + BMI + 
                   DiabetesPedigreeFunction + Age, 
                 data = train, 
                 family = binomial())


p_hat <- predict(fit_logit, newdata = test, type = "response")
pred_clase <- factor(ifelse(p_hat >= 0.5, "Si", "No"), levels = c("No", "Si"))

# Matriz de confusión
matriz_conf_logit <- confusionMatrix(pred_clase, test$Outcome_f, positive = "Si")

matriz_df_logit <- as.data.frame(matriz_conf_logit$table)
colnames(matriz_df_logit) <- c("Real", "Predicción", "Frecuencia")

matriz_df_logit$Real <- factor(matriz_df_logit$Real, 
                                levels = c("No", "Si"), 
                                labels = c("No Diabético", "Diabético"))

matriz_df_logit$Predicción <- factor(matriz_df_logit$Predicción, 
                                      levels = c("No", "Si"), 
                                      labels = c("No Diabético", "Diabético"))

matriz_ancha_logit <- matrix(
  c(matriz_df_logit[matriz_df_logit$Real == "No Diabético" & 
                      matriz_df_logit$Predicción == "No Diabético", "Frecuencia"],
    matriz_df_logit[matriz_df_logit$Real == "No Diabético" & 
                      matriz_df_logit$Predicción == "Diabético", "Frecuencia"],
    matriz_df_logit[matriz_df_logit$Real == "Diabético" & 
                      matriz_df_logit$Predicción == "No Diabético", "Frecuencia"],
    matriz_df_logit[matriz_df_logit$Real == "Diabético" & 
                      matriz_df_logit$Predicción == "Diabético", "Frecuencia"]),
  nrow = 2, byrow = TRUE
)

rownames(matriz_ancha_logit) <- c("No Diabético", "Diabético")
colnames(matriz_ancha_logit) <- c("No Diabético", "Diabético")


kable(matriz_ancha_logit, 
      caption = "",
      format = "html") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE,
                position = "center") %>%
  add_header_above(c(" " = 1, "Predicción" = 2)) %>%
  pack_rows("Valor Real", 1, 2) %>%
  column_spec(1, bold = TRUE) %>%
  footnote(general = "Clase positiva: Diabético | La diagonal principal presenta las clasificaciones correctas",
           general_title = "Nota: ")
Predicción
No Diabético Diabético
Valor Real
No Diabético 116 30
Diabético 9 37
Nota:
Clase positiva: Diabético | La diagonal principal presenta las clasificaciones correctas

5.3.2.1 Métricas de rendimiento del modelo logit

library(kableExtra)

metricas_logit <- data.frame(
  Métrica = c(
    "Exactitud (Accuracy)",
    "Intervalo de Confianza 95%",
    "Tasa de Acierto sin Información (NIR)",
    "Valor p (Accuracy > NIR)",
    "Kappa de Cohen",
    "Valor p de McNemar",
    "Sensibilidad (Recall)",
    "Especificidad",
    "Valor Predictivo Positivo (Precisión)",
    "Valor Predictivo Negativo",
    "Prevalencia",
    "Tasa de Detección",
    "Prevalencia de Detección",
    "Exactitud Balanceada"
  ),
  Valor = c(
    "0.7969",
    "(0.7330, 0.8514)",
    "0.6510",
    "7.184e-06",
    "0.5179",
    "0.001362",
    "0.5522",
    "0.9280",
    "0.8043",
    "0.7945",
    "0.3490",
    "0.1927",
    "0.2396",
    "0.7401"
  )
)

kable(metricas_logit, 
      caption = "",
      format = "html",
      align = c("l", "c")) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE,
                position = "center") %>%
  row_spec(0, bold = TRUE, background = "#2C3E50", color = "white") %>%
  column_spec(1, bold = TRUE, width = "30em") %>%
  column_spec(2, width = "15em") %>%
  add_footnote(
    "",
    notation = "none"
  )
Métrica Valor
Exactitud (Accuracy) 0.7969
Intervalo de Confianza 95% (0.7330, 0.8514)
Tasa de Acierto sin Información (NIR) 0.6510
Valor p (Accuracy > NIR) 7.184e-06
Kappa de Cohen 0.5179
Valor p de McNemar 0.001362
Sensibilidad (Recall) 0.5522
Especificidad 0.9280
Valor Predictivo Positivo (Precisión) 0.8043
Valor Predictivo Negativo 0.7945
Prevalencia 0.3490
Tasa de Detección 0.1927
Prevalencia de Detección 0.2396
Exactitud Balanceada 0.7401

5.4 Comparación métricas de rendimiento

library(kableExtra)

# Datos de la tabla
tabla_comparativa <- data.frame(
  Métrica = c(
    "Exactitud (Accuracy)",
    "IC 95% Exactitud",
    "Sensibilidad (Recall)",
    "Especificidad",
    "Precisión (VPP)",
    "Valor Predictivo Negativo (VPN)",
    "Kappa de Cohen",
    "Exactitud Balanceada",
    "Prevalencia",
    "Tasa de Detección",
    "Prevalencia de Detección"
  ),
  KNN = c(
    "0.7708",
    "(0.7048, 0.8283)",
    "0.5373",
    "0.8960",
    "0.7347",
    "0.7832",
    "0.4621",
    "0.7167",
    "0.3490",
    "0.1875",
    "0.2552"
  ),
  Logit = c(
    "0.7969",
    "(0.7330, 0.8514)",
    "0.5522",
    "0.9280",
    "0.8043",
    "0.7945",
    "0.5179",
    "0.7401",
    "0.3490",
    "0.1927",
    "0.2396"
  )
)

# Tabla bonita
kable(tabla_comparativa, 
      caption = "",
      format = "html",
      align = c("l", "c", "c")) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE,
                position = "center") %>%
  row_spec(0, bold = TRUE, background = "#2C3E50", color = "white") %>%
  column_spec(1, bold = TRUE, width = "20em") %>%
  column_spec(2, width = "10em") %>%
  column_spec(3, width = "10em") %>%
  footnote(
    general = "",
    general_title = ""
  )
Métrica KNN Logit
Exactitud (Accuracy) 0.7708 0.7969
IC 95% Exactitud (0.7048, 0.8283) (0.7330, 0.8514)
Sensibilidad (Recall) 0.5373 0.5522
Especificidad 0.8960 0.9280
Precisión (VPP) 0.7347 0.8043
Valor Predictivo Negativo (VPN) 0.7832 0.7945
Kappa de Cohen 0.4621 0.5179
Exactitud Balanceada 0.7167 0.7401
Prevalencia 0.3490 0.3490
Tasa de Detección 0.1875 0.1927
Prevalencia de Detección 0.2552 0.2396

Exactitud global. El modelo de Regresión Logística alcanzó una exactitud del 79.69% (IC 95%: 73.30% - 85.14%), superando al modelo KNN que obtuvo un 77.08% (IC 95%: 70.48% - 82.83%). Si bien la diferencia no es amplia, el modelo Logit demuestra un rendimiento ligeramente superior en términos de aciertos totales.

Capacidad predictiva por clase. En cuanto a la sensibilidad (capacidad de identificar correctamente a los pacientes diabéticos), el modelo Logit también presenta un mejor desempeño con un 55.22% frente al 53.73% del KNN. Sin embargo, ambos modelos muestran una sensibilidad moderada, lo que indica que aproximadamente la mitad de los casos positivos son detectados.

La especificidad (capacidad de identificar correctamente a los no diabéticos) es notablemente alta en ambos modelos, destacando el Logit con un 92.80% frente al 89.60% del KNN. Esto sugiere que ambos modelos son excelentes para descartar la enfermedad cuando el paciente no la presenta.

Precisión y valor predictivo. La precisión (Valor Predictivo Positivo) del modelo Logit alcanza un 80.43%, superando ampliamente al KNN que obtuvo un 73.47%. Esto significa que cuando el modelo Logit predice diabetes, acierta en 8 de cada 10 casos, mientras que el KNN acierta en aproximadamente 7 de cada 10. El Valor Predictivo Negativo muestra un comportamiento similar (79.45% en Logit vs 78.32% en KNN), indicando una alta fiabilidad en la predicción de ausencia de la enfermedad.

Concordancia y balance. El coeficiente Kappa de Cohen, que mide la concordancia entre las predicciones y la realidad más allá del azar, resulta superior en el modelo Logit (0.5179 vs 0.4621), lo que representa una concordancia moderada según la escala de Landis y Koch. La exactitud balanceada, que promedia sensibilidad y especificidad, confirma la ventaja del modelo Logit con un 74.01% frente al 71.67% del KNN.

5.5 Curvas ROCR

La curva ROC es una herramienta con la cual podemos evaluar la capacidad que tiene el modelo para discriminar y clasificar a los pacientes diabeticos y no diabeticos aqui podemos observar la relacion entre la tasa de verdaderos positivos y la tasa de falsos positivos para diferentes umbrales de clasificacion, tambien con la grafica podemos evidenciar que el modelo es capaz de diferenciar entre ambas clases, esta refleja una alta sensibilidad y una baja tasa de falsos positivos lo cual evidencia que el modelo logra identificar correctamente una proporcion importante de los casos positivos.

5.5.1 Curva ROCR del modelo KNN

library(pROC)
library(plotly)
library(caret)

set.seed(28)
knn_entrenado_prob <- knn3(Outcome_f ~ Glucose + BloodPressure + BMI + 
                             DiabetesPedigreeFunction + Age,
                           data = train,
                           k = knn_entrenado$bestTune$k)  

prob_knn <- predict(knn_entrenado_prob, newdata = test, type = "prob")

roc_knn <- roc(response = test$Outcome_f, 
               predictor = prob_knn[, "Si"], 
               levels = c("No", "Si"))

auc_knn <- auc(roc_knn)

df_roc_knn <- data.frame(
  FPR = 1 - roc_knn$specificities,
  TPR = roc_knn$sensitivities
)

plot_ly(df_roc_knn, x = ~FPR, y = ~TPR, type = 'scatter', mode = 'lines') %>%
  add_lines(
    name = paste("Curva ROC KNN (AUC =", round(auc_knn, 4), ")"),
    line = list(color = "#A23B72", width = 2.5)
  ) %>%
  add_lines(
    x = c(0, 1),
    y = c(0, 1),
    name = "Clasificador aleatorio (AUC = 0.5)",
    line = list(color = "gray", width = 1.5, dash = "dash")
  ) %>%
  layout(
    title = list(
      text = "<b>Curva ROC - Modelo KNN</b>",
      font = list(size = 16)
    ),
    xaxis = list(
      title = "<b>1 - Especificidad (Tasa de Falsos Positivos)</b>",
      range = c(0, 1),
      gridcolor = "#e8e8e8"
    ),
    yaxis = list(
      title = "<b>Sensibilidad (Tasa de Verdaderos Positivos)</b>",
      range = c(0, 1),
      gridcolor = "#e8e8e8"
    ),
    hovermode = "closest",
    plot_bgcolor = "#f8f9fa",
    paper_bgcolor = "white",
    legend = list(
      x = 0.7,
      y = 0.05,
      bgcolor = "rgba(255, 255, 255, 0.8)"
    )
  ) %>%
  config(
    displayModeBar = TRUE,
    modeBarButtonsToRemove = c("pan2d", "lasso2d", "select2d"),
    displaylogo = FALSE,
    toImageButtonOptions = list(
      format = "png",
      filename = "roc_knn_diabetes",
      width = 800,
      height = 600
    )
  )

5.5.2 Curva ROCR del modelo logit

library(pROC)
library(plotly)

roc_o <- roc(response = test$Outcome_f, predictor = p_hat, levels = c("No", "Si"))
auc_val <- auc(roc_o)


df_roc <- data.frame(
  FPR = 1 - roc_o$specificities,  
  TPR = roc_o$sensitivities     
)

plot_ly(df_roc, x = ~FPR, y = ~TPR) %>%
  add_lines(
    name = paste("Curva ROC (AUC =", round(auc_val, 4), ")"),
    line = list(color = "#2E86AB", width = 2.5)
  ) %>%
  add_lines(
    x = c(0, 1),
    y = c(0, 1),
    name = "Clasificador aleatorio (AUC = 0.5)",
    line = list(color = "gray", width = 1.5, dash = "dash")
  ) %>%
  layout(
    title = list(
      text = "<b>Curva ROC - Modelo de Regresión Logística</b>",
      font = list(size = 16)
    ),
    xaxis = list(
      title = "<b>1 - Especificidad (Tasa de Falsos Positivos)</b>",
      range = c(0, 1),
      gridcolor = "#e8e8e8"
    ),
    yaxis = list(
      title = "<b>Sensibilidad (Tasa de Verdaderos Positivos)</b>",
      range = c(0, 1),
      gridcolor = "#e8e8e8"
    ),
    hovermode = "closest",
    plot_bgcolor = "#f8f9fa",
    paper_bgcolor = "white",
    legend = list(
      x = 0.7,
      y = 0.05,
      bgcolor = "rgba(255, 255, 255, 0.8)"
    )
  ) %>%
  config(
    displayModeBar = TRUE,
    modeBarButtonsToRemove = c("pan2d", "lasso2d", "select2d"),
    displaylogo = FALSE,
    toImageButtonOptions = list(
      format = "png",
      filename = "roc_logit",
      width = 800,
      height = 600
    )
  )

5.5.3 Comparación de curvas ROCR

library(pROC)
library(plotly)

# Data frames
df_logit <- data.frame(FPR = 1 - roc_o$specificities, TPR = roc_o$sensitivities)
df_knn <- data.frame(FPR = 1 - roc_knn$specificities, TPR = roc_knn$sensitivities)

# Gráfico
plot_ly() %>%
  add_lines(data = df_logit, x = ~FPR, y = ~TPR,
            name = paste("Logit (AUC =", round(auc_val, 3), ")"),
            line = list(color = "#2E86AB", width = 2)) %>%
  add_lines(data = df_knn, x = ~FPR, y = ~TPR,
            name = paste("KNN (AUC =", round(auc_knn, 3), ")"),
            line = list(color = "#A23B72", width = 2)) %>%
  add_lines(x = c(0, 1), y = c(0, 1),
            name = "Azar",
            line = list(color = "gray", width = 1, dash = "dash")) %>%
  layout(title = "Comparación ROC: Logit vs KNN",
         xaxis = list(title = "1 - Especificidad"),
         yaxis = list(title = "Sensibilidad"),
         hovermode = "closest")

El Área Bajo la Curva (AUC) confirma la superioridad del modelo Logit con un valor de 0.797, frente al 0.753 del KNN. Ambos modelos superan ampliamente el valor de 0.5 correspondiente a un clasificador aleatorio, demostrando su utilidad predictiva. La curva ROC del Logit se sitúa consistentemente por encima de la del KNN en la mayor parte del rango, lo que indica un mejor equilibrio entre sensibilidad y especificidad.

6 Hallazgos clave

library(kableExtra)

tabla_hallazgos <- data.frame(
  Hallazgo = c(
    "Mejor modelo global",
    "Exactitud (Accuracy)",
    "Sensibilidad (Recall)",
    "Especificidad",
    "Precisión (VPP)",
    "Valor Predictivo Negativo (VPN)",
    "Kappa de Cohen",
    "Exactitud Balanceada",
    "AUC (ROC)",
    "Fortaleza principal",
    "Debilidad principal"
  ),
  KNN = c(
    "-",
    "77.08%",
    "53.73%",
    "89.60%",
    "73.47%",
    "78.32%",
    "0.4621",
    "71.67%",
    "0.753",
    "Alta especificidad",
    "Baja sensibilidad"
  ),
  Logit = c(
    "Ganador",
    "79.69%",
    "55.22%",
    "92.80%",
    "80.43%",
    "79.45%",
    "0.5179",
    "74.01%",
    "0.797",
    "Alta especificidad y precisión",
    "Sensibilidad moderada"
  ),
  Mejora = c(
    "-",
    "+2.61%",
    "+1.49%",
    "+3.20%",
    "+6.96%",
    "+1.13%",
    "+0.0558",
    "+2.34%",
    "+0.044",
    "-",
    "-"
  )
)

kable(tabla_hallazgos, 
      caption = "*Resumen comparativo de hallazgos entre modelos KNN y Regresión Logística",
      format = "html",
      align = c("l", "c", "c", "c")) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE,
                position = "center",
                font_size = 13) %>%
  row_spec(0, bold = TRUE, background = "#2C3E50", color = "white") %>%
  column_spec(1, bold = TRUE, width = "18em") %>%
  column_spec(2, width = "10em") %>%
  column_spec(3, width = "10em", background = "#e8f5e9") %>%
  column_spec(4, width = "8em") %>%
  add_header_above(c(" " = 1, "Modelo KNN" = 1, "Modelo Logit" = 1, "Mejora del Logit" = 1)) %>%
  footnote(
    general = "VPP: Valor Predictivo Positivo (Precisión) | VPN: Valor Predictivo Negativo | AUC: Área Bajo la Curva ROC",
    general_title = "Nota: ",
    symbol = c("Indica el modelo ganador", "Las mejoras positivas favorecen al modelo Logit")
  )
*Resumen comparativo de hallazgos entre modelos KNN y Regresión Logística
Modelo KNN
Modelo Logit
Mejora del Logit
Hallazgo KNN Logit Mejora
Mejor modelo global
Ganador
Exactitud (Accuracy) 77.08% 79.69% +2.61%
Sensibilidad (Recall) 53.73% 55.22% +1.49%
Especificidad 89.60% 92.80% +3.20%
Precisión (VPP) 73.47% 80.43% +6.96%
Valor Predictivo Negativo (VPN) 78.32% 79.45% +1.13%
Kappa de Cohen 0.4621 0.5179 +0.0558
Exactitud Balanceada 71.67% 74.01% +2.34%
AUC (ROC) 0.753 0.797 +0.044
Fortaleza principal Alta especificidad Alta especificidad y precisión
Debilidad principal Baja sensibilidad Sensibilidad moderada
Nota:
VPP: Valor Predictivo Positivo (Precisión) | VPN: Valor Predictivo Negativo | AUC: Área Bajo la Curva ROC
* Indica el modelo ganador
Las mejoras positivas favorecen al modelo Logit

El modelo de Regresión Logística presentó un mejor rendimiento que el KNN debido a la naturaleza de las variables clínicas incluidas en el estudio. Variables como glucosa, índice de masa corporal, presión arterial y edad mantienen una relación aproximadamente lineal con el logaritmo de la probabilidad de presentar diabetes, lo cual favorece el enfoque paramétrico de la regresión logística. KNN, al ser un modelo no paramétrico basado en distancias, no aprovecha esta estructura lineal y puede verse afectado por la escala de las variables y la dimensionalidad moderada del problema (cinco predictores). Además, con un tamaño muestral de 768 pacientes, la regresión logística tiende a ser más estable porque estima solo seis parámetros, mientras que KNN requiere una alta densidad de puntos en el espacio de características para funcionar de manera óptima.

7 Conclusiones

El presente estudio tuvo como propósito desarrollar y comparar modelos de clasificación supervisada capaces de predecir el diagnóstico de diabetes a partir de variables clínicas de los pacientes, con el fin de identificar cuál de los modelos presenta un mejor desempeño para distinguir entre pacientes diabéticos y no diabéticos.

Para ello, se entrenaron y evaluaron dos modelos: K-Nearest Neighbors (KNN) y Regresión Logística, utilizando una base de datos de 768 pacientes con una prevalencia de diabetes del 34.9%. Las variables predictoras incluidas fueron glucosa, presión arterial, índice de masa corporal (IMC), función del pedigrí diabético y edad.

7.1 Limitaciones del estudio

Entre las limitaciones del estudio se encuentra el desbalance moderado de clases, con una proporción de 65.1% de no diabéticos y 34.9% de diabéticos, lo que pudo haber influido en el rendimiento de los modelos, particularmente en la sensibilidad. Además, el análisis se limitó a cinco variables clínicas, dejando fuera factores relevantes como la hemoglobina glicosilada (HbA1c), el perfil lipídico, los hábitos alimenticios o la actividad física. Otra limitación es que los modelos fueron evaluados inicialmente con un umbral de clasificación fijo en 0.5, aunque se identificó que el punto óptimo según el método de Youden es 0.371, lo que sugiere que ajustes adicionales podrían mejorar el rendimiento.

7.2 ¿El modelo ajustado logró responder al objetivo de la investigación?

Sí, el modelo de Regresión Logística logró responder al objetivo principal de la investigación, aunque con limitaciones. El estudio buscaba desarrollar y comparar modelos capaces de predecir el diagnóstico de diabetes a partir de variables clínicas, identificando cuál ofrecía mejor desempeño. El modelo Logit alcanzó una exactitud del 79.69% y un AUC de 0.797, lo que confirma su capacidad para distinguir entre pacientes diabéticos y no diabéticos de manera aceptable.

Sin embargo, el modelo presentó una sensibilidad moderada (55.22%), detectando solo la mitad de los verdaderos casos positivos. Esto significa que, aunque el modelo cumple con el objetivo predictivo, no sería suficiente como única herramienta de diagnóstico clínico, pero sí útil como apoyo en tamizaje poblacional.

En cuanto a la comparación entre modelos, el estudio sí logró determinar que el Logit supera al KNN en todas las métricas evaluadas, cumpliendo así plenamente el objetivo comparativo de la investigación.

En conclusión, el modelo ajustado cumple de manera parcial el objetivo principal y de manera plena el objetivo comparativo, posicionándose como una herramienta útil pero perfectible para la predicción de diabetes.