Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

1. EDA, data leakage y modelos

Proyecto Integrador — Heart Failure Prediction

Nombres: Juan Camilo Oñoro Araujo - 200177329 y María Carolina Cantillo Orozco - 200179105

Fecha: 30/09/2026

MACHINE LEARNING

Notebook 1 — Etapa 1: Análisis exploratorio, preprocesamiento y detección de data leakage

EDA

Qué se hace en esta etapa y por qué

Esta es la Etapa 1 del proyecto. Antes de entrenar un modelo hay que conocer los datos: qué variables hay, de qué tipo son, si faltan valores, si hay errores de registro y qué variables se relacionan con la enfermedad.

Se hace por dos razones. La primera es que un modelo solo es tan bueno como los datos con que se entrena: si no se detectan problemas como los ceros imposibles de cholesterol, el modelo aprende de información falsa. La segunda es que las decisiones de preprocesamiento (cómo llenar los faltantes, qué escalar y cómo codificar las categóricas) salen de lo que se observa aquí.

Parte 1. Importar librerías necesarias y configuraciones

pandas 2.1.0 | numpy 1.25.2 | seaborn 0.12.2

Estilo de gráficas

Parte 2. Ruta de acceso al archivo y carga del dataset

Archivo  : E:\Documents\MachineLearningUN\heart-disease-mlops\data\heart.csv
Tamaño   : 35.9 KB en disco
Filas    : 918 pacientes
Columnas : 12
Memoria  : 324.8 KB
Carga en 0.005 s

Parte 3. Diccionario de variables y siglas para el texto

VariableTipoDescripción
ageNuméricaEdad del paciente (años)
sexCategóricaSexo: M masculino, F femenino
chestpaintypeCategóricaTipo de dolor de pecho: TA angina típica, ATA angina atípica, NAP dolor no anginoso, ASY asintomático
restingbpNuméricaPresión arterial en reposo (mm Hg)
cholesterolNuméricaColesterol sérico (mg/dl)
fastingbsBinariaGlucosa en ayunas: 1 si > 120 mg/dl, 0 en otro caso
restingecgCategóricaElectrocardiograma en reposo: Normal, ST (anomalía de onda ST-T), LVH (hipertrofia ventricular izquierda)
maxhrNuméricaFrecuencia cardíaca máxima alcanzada (60–202)
exerciseanginaCategóricaAngina inducida por ejercicio: Y sí, N no
oldpeakNuméricaDepresión del segmento ST inducida por ejercicio (mm)
st_slopeCategóricaPendiente del segmento ST en el pico del ejercicio: Up, Flat, Down
heartdiseaseObjetivo1 enfermedad cardíaca, 0 normal

fastingbs viene codificada como 0 o 1, así que en el EDA se analiza con las categóricas.

Numéricas   (5): ['Age', 'RestingBP', 'Cholesterol', 'MaxHR', 'Oldpeak']
Categóricas (6): ['Sex', 'ChestPainType', 'FastingBS', 'RestingECG', 'ExerciseAngina', 'ST_Slope']
Objetivo       : HeartDisease

Parte 4. info() y tipos de datos

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 918 entries, 0 to 917
Data columns (total 12 columns):
 #   Column          Non-Null Count  Dtype  
---  ------          --------------  -----  
 0   Age             918 non-null    int64  
 1   Sex             918 non-null    object 
 2   ChestPainType   918 non-null    object 
 3   RestingBP       918 non-null    int64  
 4   Cholesterol     918 non-null    int64  
 5   FastingBS       918 non-null    int64  
 6   RestingECG      918 non-null    object 
 7   MaxHR           918 non-null    int64  
 8   ExerciseAngina  918 non-null    object 
 9   Oldpeak         918 non-null    float64
 10  ST_Slope        918 non-null    object 
 11  HeartDisease    918 non-null    int64  
dtypes: float64(1), int64(6), object(5)
memory usage: 86.2+ KB
Loading...

El dataset tiene 918 pacientes y 12 columnas: 5 numéricas, 6 categóricas (contando fastingbs) y la variable objetivo. Las categóricas vienen como texto (object), así que habrá que codificarlas (One-Hot) antes de entrenar: MinMaxScaler y SVC no aceptan texto. La columna objetivo se llama heartdisease

Parte 5. Nulos explícitos y filas duplicadas

Loading...
Total de nulos explícitos: 0
Filas duplicadas         : 0
Duplicados sin el target : 0

info() reporta 918 valores no nulos en todas las columnas y no hay filas duplicadas. A primera vista el dataset está completo, pero la Parte 6 muestra que sí faltan datos: están codificados como 0 cuando es un valor imposible

Parte 6. Nulos ocultos: valores imposibles

Que no haya NaN no quiere decir que no falten datos. Un colesterol sérico o una presión arterial en reposo de 0 son fisiológicamente imposibles en un paciente vivo, así que corresponden a mediciones no registradas. En cambio, oldpeak = 0 es válido (no hay depresión del ST) y sus valores negativos también (elevación del ST).

Loading...
Nulos en la copia del EDA:
Loading...

Lo que sale:

  1. cholesterol tiene 172 ceros (18.7 % de los pacientes). Es el problema de calidad más importante del dataset. El valor mínimo real, sin contar los ceros, es 85 mg/dl.

  2. restingbp tiene un solo cero (0.1 %). Es un error de registro aislado.

  3. oldpeak tiene 368 ceros y 13 valores negativos. Ambos son válidos: 0 significa que no hubo depresión del ST, y un valor negativo, que hubo elevación.

En la copia df_eda estos ceros se convierten en NaN para que no distorsionen las estadísticas. El df original no se toca, porque la imputación se hará dentro del Pipeline usando solo el conjunto de entrenamiento, para no introducir un data leakeage

A. Variables numéricas

Parte 7. Estadísticos descriptivos e IQR (con los ceros imposibles como faltantes)

Loading...
Efecto de los ceros imposibles en la media y la mediana:
Loading...

Si los ceros se dejaran como datos reales, la media de cholesterol bajaría de 244.6 a 198.8 mg/dl (−46 mg/dl) y la mediana de 237 a 223. El conjunto de datos parecería mucho más sano de lo que es. En restingbp el efecto de un solo cero es despreciable.

La edad va de 28 a 77 años (mediana 54). La presión en reposo tiene mediana 130 mm Hg. maxhr va de 60 a 202 latidos por minuto. oldpeak tiene mediana 0.6 y está concentrada cerca de 0. Conla mediana, el 50% de las personas registradas parecen tener problemas de presión arterial alta (lo normal es 120 en una persona sana)

Parte 8. Outliers (regla de 1.5 × IQR)

Loading...
RestingBP    (27 outliers): bajos = [80.0] | altos = [172.0, 174.0, 178.0, 180.0, 185.0, 190.0, 192.0, 200.0]
Cholesterol  (23 outliers): bajos = [85.0, 100.0] | altos = [412.0, 417.0, 458.0, 466.0, 468.0, 491.0, 518.0, 529.0, 564.0, 603.0]
MaxHR        (2 outliers): bajos = [60, 63] | altos = []
Oldpeak      (16 outliers): bajos = [-2.6] | altos = [3.8, 4.0, 4.2, 4.4, 5.0, 5.6, 6.2]

Hay pocos outliers: como máximo 3 % por variable. Todos son clínicamente plausibles:

  1. restingbp: 27 outliers entre 172 y 200 mm Hg, que corresponden a hipertensión severa, y un 80 mm Hg.

  2. cholesterol: 23 outliers. Los altos van de 412 a 603 mg/dl (hipercolesterolemia) y los bajos son 85 y 100.

  3. oldpeak: 16 outliers. Los altos, de 3.8 a 6.2, indican isquemia marcada, y el bajo es −2.6.

  4. maxhr: solo 2 valores bajos (60 y 63).

No se eliminan: son pacientes reales con valores extremos, justamente los que más interesan para detectar enfermedad. Con 918 filas, quitar outliers además costaría información.

Parte 9. Histogramas

El diagrama 6 muestra cholesterol con los ceros originales, para ver el tamaño del pico de faltantes.

<Figure size 1540x880 with 6 Axes>
  1. age y maxhr tienen forma casi simétrica, con la media y la mediana prácticamente iguales.

  2. restingbp muestra picos en 120, 130, 140, 150 y 160: el personal médico redondea las lecturas al valor más cercano, especialmente cuando es tomada con un instrumento de aguja, no digital. Se podrían establecer hiótesis si se conociera la fecha del dataset o los recursos económicos donde se tomaron las muestras

  3. cholesterol, sin los ceros, tiene asimetría a la derecha, con una cola hasta 603.

  4. oldpeak tiene un pico enorme en 0 (40 % de los pacientes) y una cola larga a la derecha.

  5. El sexto diargama muestra lo grave que es el problema de cholesterol: la barra de los ceros es la más alta de toda la distribución, separada del resto por un hueco entre 0 y 85.

Parte 10. Box plots

<Figure size 1210x990 with 5 Axes>

Los box plots confirman la Parte 8. restingbp y cholesterol tienen outliers en ambos extremos y oldpeak sobre todo en el superior. age no tiene ninguno y maxhr solo dos. Lo cual es coherente, personas con enfermedades cardíacas avanzadas y con valroes tan altos en emogramas son outliers y no es lo común, por eso el modelo deberá centrarse en buscar anomalías por fuera de los rangos “seguros”. El dataset debería incluir variables como si hace o no ejercicio, si fuma o no. Ya que son preguntas médicas que van relacionadas al desarrollo de enfermedades, el modelo probablmenete podría encontrar la relación que las personas sedantarias tieneden a desarrollar más enfermedades.

Parte 11. Normalidad y transformaciones

Con 918 pacientes se puede usar Shapiro-Wilk (admite hasta 5,000 datos). Se reporta también D’Agostino-Pearson como segunda prueba.

Loading...
<Figure size 1760x396 with 5 Axes>

Ninguna variable es normal según Shapiro-Wilk ni según D’Agostino-Pearson (todas con p < 0.001). Aun así, el grado de desviación es muy distinto entre variables:

  1. age y maxhr: tienen asimetría menor que 0.25 y un W de Shapiro superior a 0.99. Con 918 datos la prueba detecta desviaciones muy pequeñas, pero en la práctica su forma es casi normal.

  2. cholesterol (asimetría 1.24) y oldpeak (1.02): tienen asimetría fuerte. En oldpeak hay ceros y negativos, así que no sirve el logaritmo. Si se quisiera transformar, habría que usar Yeo-Johnson.

La decisión para el modelado es no transformar. Los modelos basados en árboles no lo necesitan, y para SVC, KNN y Regresión Logística basta con escalar dentro del Pipeline. La transformación queda como hiperparámetro opcional.

B. Variables categóricas

Parte 12. Frecuencia absoluta y relativa

Como el dataset es pequeño, el umbral de categoría rara es 5 %.

Loading...

Sex  (2 categorías)
Loading...

ChestPainType  (4 categorías)
Loading...

FastingBS  (2 categorías)
Loading...

RestingECG  (3 categorías)
Loading...

ExerciseAngina  (2 categorías)
Loading...

ST_Slope  (3 categorías)
Loading...

Parte 13. Gráficos de barras

<Figure size 1540x770 with 6 Axes>
  1. El dataset está muy desbalanceado: 79 % hombres y 21 % mujeres.

  2. chestpaintype: la categoría más frecuente es ASY, dolor asintomático (54 %). TA, angina típica, queda justo en el umbral (5.0 %, 46 pacientes).

  3. st_slope: casi todos los pacientes tienen pendiente Flat o Up, y Down es poco frecuente (6.9 %).

  4. fastingbs y exerciseangina: están desbalanceadas pero con suficientes casos en cada clase.

Ninguna categoría queda por debajo del 5 %, así que no hace falta agrupar categorías raras u “otros”, como se hizo en la priemra tarea

C. Variable objetivo

Parte 14. Distribución de heartdisease

Loading...
Razón de clases (1 : 0) = 1.24 : 1
<Figure size 770x495 with 1 Axes>

Hay 508 pacientes con enfermedad (55.3 %) y 410 sin ella (44.7 %), una razón de 1.24 : 1. El desbalance es leve, así que no hace falta sobremuestreo ni pesos de clase.

  1. El split train/test se hará estratificado para conservar la proporción.

  2. La métrica principal será AUC, que no depende del umbral, y se reportará también Accuracy, como pide el enunciado.

  3. El modelo trivial que siempre predice “con enfermedad” tendría 55.3 % de accuracy. Ese es el piso que cualquier modelo tiene que superar. También conviene no tener falsos positivos porque en el mundo real se iniciarían tratamientos que pueden ser invasivos con medicamentos fuertes, que podrían terminar por agravar o dañar otros órganos. Por ejemplo, usar muchso analgésicos y pastillas para la presión daña los riñones.

Preparación

A. Variables numéricas vs heartdisease

Parte 15. Boxplots y violin plots por clase

<Figure size 1980x880 with 10 Axes>
  1. oldpeak: es la variable que mejor separa las clases. Sin enfermedad, la mediana es 0 y la distribución está concentrada. Con enfermedad, la mediana es 1.2 y los valores están mucho más dispersos.

  2. maxhr: se comporta al revés. Los pacientes enfermos alcanzan una frecuencia cardíaca máxima menor (mediana 126 contra 150), lo que es coherente con la menor capacidad de ejercicio.

  3. age: los enfermos son unos 6 años mayores (57 contra 51).

  4. restingbp y cholesterol: las cajas se solapan casi por completo, así que por sí solas diferencian poco.

Parte 16. Diferencias de medias, t de Welch, Mann-Whitney y correlación punto biserial

En la Parte 11 ninguna variable resultó normal, así que la prueba principal es Mann-Whitney; la t de Welch se reporta para comparar. El tamaño del efecto se mide con la correlación rank-biserial (|r| < 0.1 despreciable, < 0.3 pequeño, < 0.5 mediano, ≥ 0.5 grande).

Loading...

Las 5 variables tienen diferencias significativas (p < 0.05) según Mann-Whitney y t de Welch, pero el tamaño del efecto no es el mismo:

  1. Efecto mediano: oldpeak (r = +0.47), maxhr (r = −0.47) y age (r = +0.34).

  2. Efecto pequeño: restingbp (+0.13) y cholesterol (+0.13).

El colesterol, que clínicamente es un factor de riesgo clásico, resulta de las variables menos discriminantes en este dataset. Parte de la razón es que el 18.7 % de sus valores faltan y quedan fuera de la prueba. Además, la Parte 24 muestra que el hecho de que falte sí está muy relacionado con la enfermedad.COnla presión pasa algo similar. Otra posible mejora sería la posibilidad de remisión o no con otras especialidad, por ejemplo, presión alta daña riñones, glucosa alta higado y páncreas, presión alta y glucosa alta dañan la retina. El profesor podría explorar la psobilidad de incluirlo como algo opcional para la tarea.

Parte 17. KDE comparativos por clase

Las variables se ordenan por |r punto biserial| de la Parte 16.

<Figure size 1980x418 with 5 Axes>

Las curvas KDE confirman el orden. En oldpeak y maxhr las densidades de las dos clases están claramente desplazadas. En age el desplazamiento es moderado, y en restingbp y cholesterol las curvas casi se superponen.

B. Variables categóricas vs heartdisease

Parte 18. Tablas de contingencia, chi cuadrado y V de Cramér

La V de Cramér va de 0 a 1 y mide la fuerza de la asociación. Para saber qué categorías explican la asociación se usan los residuos estandarizados ajustados, con corrección de Bonferroni.

Loading...

ST_Slope  ·  χ² = 355.9  ·  p < 0.001  ·  V = 0.623
Loading...

ChestPainType  ·  χ² = 268.1  ·  p < 0.001  ·  V = 0.540
Loading...

ExerciseAngina  ·  χ² = 222.3  ·  p < 0.001  ·  V = 0.494
Loading...

Sex  ·  χ² = 84.1  ·  p < 0.001  ·  V = 0.305
Loading...

FastingBS  ·  χ² = 64.3  ·  p < 0.001  ·  V = 0.267
Loading...

RestingECG  ·  χ² = 10.9  ·  p = 0.004  ·  V = 0.109
Loading...

Las 6 variables categóricas están asociadas significativamente con heartdisease, y ninguna celda tiene frecuencia esperada menor que 5, así que la prueba χ² es válida. Ordenadas por V de Cramér:

VariableVMagnitudCategoría de mayor riesgo
st_slope0.623GrandeFlat (82.8 %) y Down (77.8 %); Up solo 19.7 %
chestpaintype0.540GrandeASY asintomático (79.0 %); ATA solo 13.9 %
exerciseangina0.494ModeradaY (85.2 %) contra N (35.1 %)
sex0.305ModeradaM (63.2 %) contra F (25.9 %)
fastingbs0.267Pequeña1 (79.4 %) contra 0 (48.0 %)
restingecg0.109PequeñaST (65.7 %)

Hay un resultado contraintuitivo: el dolor asintomático (ASY) es el de mayor riesgo. Los pacientes sin síntomas típicos llegan a la prueba con la enfermedad ya avanzada, mientras que la angina atípica suele tener otras causas. En chestpaintype, la categoría TA no difiere significativamente de la tasa global después de la corrección de Bonferroni. En restingecg, tampoco LVH.

Parte 19. Barras apiladas y tasa de enfermedad por categoría

A la izquierda, la frecuencia apilada por clase; a la derecha, la tasa de enfermedad cardíaca comparada con la global.

<Figure size 1430x379.5 with 2 Axes>
<Figure size 1430x440 with 2 Axes>
<Figure size 1430x319 with 2 Axes>
<Figure size 1430x319 with 2 Axes>
<Figure size 1430x319 with 2 Axes>
<Figure size 1430x379.5 with 2 Axes>

Las barras naranjas marcan las categorías con tasa por encima de la global (55.3 %). Las más riesgosas son exerciseangina = Y (85.2 %), st_slope = Flat (82.8 %), fastingbs = 1 (79.4 %) y chestpaintype = ASY (79.0 %). Las que más protegen son chestpaintype = ATA (13.9 %) y st_slope = Up (19.7 %).

C. Multicolinealidad

Parte 20. Correlación de Pearson y Spearman entre variables numéricas

Como las variables no son normales (Parte 11), se muestra también Spearman, que se basa en rangos y no asume linealidad.

<Figure size 1540x660 with 4 Axes>
Pares con |r| > 0.7: 0
Loading...

No hay multicolinealidad entre las variables numéricas: ningún par supera |r| = 0.7. El par más correlacionado es age con maxhr (r = −0.38), porque la frecuencia cardíaca máxima baja con la edad. Pearson y Spearman dan resultados parecidos. Con la variable objetivo, las correlaciones más fuertes son las de oldpeak (Spearman +0.42) y maxhr (−0.41). Se conservan las 5 variables numéricas. Se usaría Pearson para un reporte hipotético.

Parte 21. V de Cramér entre pares de variables categóricas

Loading...
<Figure size 880x715 with 2 Axes>

Entre las variables predictoras, la asociación más fuerte es exerciseangina con st_slope (V = 0.46), seguida de chestpaintype con exerciseangina (0.44). Ambas son moderadas, lejos de la redundancia: en Lending Club, grade y sub_grade tenían V = 1.00. Las tres variables describen la respuesta al esfuerzo físico y comparten información, pero cada una aporta algo propio.

Parte 22. Redundancia entre tipos de variables: razón de correlación η

Para una variable categórica frente a una numérica se usa la razón de correlación η: va de 0 a 1 y mide qué parte de la varianza de la numérica explica la categoría.

<Figure size 880x550 with 2 Axes>

El valor η más alto es st_slope con oldpeak (η = 0.50). Es esperable, porque ambas miden el segmento ST del electrocardiograma de esfuerzo. Le siguen exerciseangina con oldpeak (0.41) y st_slope, chestpaintype y exerciseangina con maxhr (0.37–0.38). Ninguna relación indica redundancia (η > 0.7), así que no se elimina ninguna variable.

D. Visualizaciones avanzadas

Parte 23. Pairplot / matriz de dispersión

Con 918 pacientes se usan todos los datos (no hace falta muestrear).

<Figure size 1416.38x1265 with 20 Axes>

En el pairplot, los pacientes con enfermedad se concentran en la zona de maxhr bajo y oldpeak alto. Es la combinación que más separa las clases. No aparecen fronteras no lineales complejas ni grupos aislados. Con cholesterol y restingbp las nubes de puntos de ambas clases se superponen.

E. Valores faltantes (análisis detallado)

Parte 24. ¿Los ceros de cholesterol faltan al azar? (prueba de MCAR)

MCAR significa que el dato falta por puro azar. Si fuera así, la tasa de enfermedad y la distribución de las demás variables deberían ser iguales cuando el colesterol falta y cuando está presente.

Cholesterol faltante vs HeartDisease: χ² = 91.8, p < 0.001, V = 0.319
Loading...
Loading...
<Figure size 770x330 with 1 Axes>

Lo que sale es clave para el modelado:

  1. Tasa de enfermedad: es del 88.4 % cuando falta el colesterol y del 47.7 % cuando está presente (χ² = 91.8, p < 0.001, V = 0.32). Que el dato falte se asocia con la enfermedad incluso más que sex (V = 0.305).

  2. Resto de variables: se rechaza MCAR en 9 de las 10 (todas menos oldpeak). Los pacientes sin colesterol son mayores (mediana 57.5 contra 54), casi todos hombres (94 % contra 76 %), tienen maxhr más baja (120 contra 140) y más glucosa alta en ayunas (fastingbs = 1: 52 % contra 17 %).

Conclusión: los datos no faltan al azar. Este dataset combina 5 bases de datos clínicas: Cleveland, Hungría, Suiza, Long Beach VA y Statlog. Algunos centros no registraron el colesterol y tenían poblaciones con más enfermedad. El mecanismo es MAR, porque depende del hospital de origen.

Qué implica para el modelado:

  1. Si se imputa solo con la mediana, se pierde esta señal. Se imputará con la mediana y además se agregará un indicador de faltante (SimpleImputer(add_indicator=True)).

  2. El indicador refleja el hospital de origen, no la fisiología del paciente. Es una fuente de sesgo que el modelo en producción podría no encontrar. Se documenta como limitación.

  3. La mediana de imputación se calcula solo con el conjunto de entrenamiento, dentro del Pipeline.

Parte 25. Plan de tratamiento por variable

Reglas del preprocesamiento. Todos los pasos con parámetros aprendidos van dentro del Pipeline, para que se ajusten solo con train y no haya fuga:

VariableTipoTratamiento¿Aprende de los datos?
age, maxhrNuméricaEscalarSí (en el Pipeline)
oldpeakNuméricaEscalar; se conservan ceros y negativosSí (en el Pipeline)
restingbpNumérica0 → NaN, imputar con la mediana y escalarSí (en el Pipeline)
cholesterolNumérica0 → NaN, imputar con la mediana, agregar indicador de faltante y escalarSí (en el Pipeline)
sex, chestpaintype, restingecg, exerciseangina, st_slopeCategóricaOneHotEncoder(handle_unknown=“ignore”)Sí (en el Pipeline)
fastingbsBinaria (0/1)Se deja como estáNo
heartdiseaseObjetivoSplit estratificado 80/20—

Otras decisiones:

  1. Outliers: se conservan, porque son clínicamente plausibles (Parte 8).

  2. Transformaciones: no se aplican (Parte 11).

  3. Variables: no se elimina ninguna por redundancia (Partes 20 a 22).

  4. 0 → NaN: reemplazar los ceros es una regla fija que no usa estadísticas del dataset, así que no produce fuga y puede hacerse antes del split. En cambio, la mediana para imputar sí se aprende de los datos y va en el Pipeline.

Resumen ejecutivo

  1. Datos: 918 pacientes, 11 predictores (5 numéricos y 6 categóricos) y la variable objetivo binaria heartdisease. No hay NaN explícitos ni filas duplicadas.

  2. Nulos ocultos: el 18.7 % de los valores de cholesterol (172 pacientes) son ceros imposibles, que en realidad son faltantes. No faltan al azar: el 88.4 % de esos pacientes tiene enfermedad, contra el 47.7 % del resto. Se imputan con la mediana más un indicador de faltante, dentro del Pipeline.

  3. Clases: 55.3 % con enfermedad y 44.7 % sin ella. El desbalance es leve y se usará un split estratificado.

  4. Variables más predictivas:

    1. Categóricas: st_slope (V = 0.62), chestpaintype (V = 0.54) y exerciseangina (V = 0.49).

    2. Numéricas: oldpeak (r = +0.47) y maxhr (r = −0.47).

    3. cholesterol y restingbp son las que menos discriminan.

  5. Perfil de alto riesgo: hombre mayor, con dolor de pecho asintomático, angina inducida por ejercicio, pendiente ST plana, oldpeak alto y maxhr baja.

  6. Calidad: hay pocos outliers (≤ 3 %) y todos son plausibles, así que se conservan. No hay multicolinealidad (ningún |r| ni V supera 0.7).

  7. Consecuencia para el modelado: como hay variables categóricas y faltantes, el preprocesamiento necesita un ColumnTransformer con imputación, One-Hot y escalado. Todo va dentro del Pipeline, para que la validación cruzada de GridSearchCV no tenga fuga

Preprocesamiento

Los modelos de scikit-learn solo aceptan números y no aceptan valores faltantes. Por eso hay que convertir las variables categóricas en columnas numéricas, llenar los faltantes y poner las variables numéricas en una escala comparable.

Lo más importante es el orden. Primero se separan los datos de entrenamiento y de prueba, y solo después se calculan las medianas, los mínimos y los máximos, usando únicamente los de entrenamiento. Así el conjunto de prueba se comporta como un grupo de pacientes nuevos que el modelo nunca vio.

Parte 26. Librerías de modelado

scikit-learn 1.3.0

Parte 27. Matriz de variables, objetivo y ceros imposibles

Se separan las variables (X) del objetivo (y). Los ceros de restingbp y cholesterol se convierten en NaN antes del split. Esto no produce fuga porque es una regla fija,porque estos valors no son poisbles como se mencionó, que no usa ninguna estadística calculada con los datos. Lo que sí se aprende de los datos, como la mediana para imputar, se deja para el Pipeline.

X: 918 filas x 11 columnas | y: 918 valores
Nulos en X después de marcar los ceros imposibles:
Loading...

Parte 28. División train/test antes de cualquier ajuste

El conjunto de prueba se separa antes de calcular cualquier estadística. Se usa un split estratificado 80/20, para que las dos partes conserven la proporción de enfermos (Parte 14).

Loading...

Parte 29. Preprocesador con ColumnTransformer

Se aplica el plan de la Parte 25. Cada grupo de columnas recibe su propio tratamiento:

GrupoColumnasTratamiento
numage, maxhr, oldpeakMinMaxScaler
num_nulosrestingbp, cholesterolImputación con la mediana + indicador de faltante, y después MinMaxScaler
catsex, chestpaintype, restingecg, exerciseangina, st_slopeOneHotEncoder
binfastingbsSin cambios (ya es 0/1)

Se usa MinMaxScaler. La función recibe columnas numéricas adicionales para poder agregar la variable fugada en la Parte 30.

Columnas antes: 11 -> después del preprocesamiento: 21
['Age', 'MaxHR', 'Oldpeak', 'RestingBP', 'Cholesterol', 'missingindicator_Cholesterol', 'Sex_F', 'Sex_M', 'ChestPainType_ASY', 'ChestPainType_ATA', 'ChestPainType_NAP', 'ChestPainType_TA', 'RestingECG_LVH', 'RestingECG_Normal', 'RestingECG_ST', 'ExerciseAngina_N', 'ExerciseAngina_Y', 'ST_Slope_Down', 'ST_Slope_Flat', 'ST_Slope_Up', 'FastingBS']
Loading...
Loading...

Lo que sale: las 11 columnas originales pasan a 21. Las 5 categóricas se convierten en 15 columnas One-Hot, y se agrega missingindicator_Cholesterol, que vale 1 cuando faltaba el colesterol.

La mediana que usa el imputador para cholesterol es 238 mg/dl, calculada solo con train. La del dataset completo es 237. La diferencia es pequeña, pero muestra que el preprocesador no vio los datos de prueba.

No aparece un indicador para restingbp. SimpleImputer solo crea el indicador para las columnas que tienen nulos en train, y el único cero de restingbp quedó en test. En test ese valor se imputa con la mediana de train (130), que es justo lo que pasaría con un paciente nuevo en producción.

Detección de data leakage

La fuga ocurre cuando el modelo tiene acceso durante el entrenamiento a información que no tendría en la vida real, por ejemplo datos del conjunto de prueba o una variable que contiene la respuesta. El resultado es un modelo que parece excelente en la evaluación pero falla con pacientes nuevos.

Parte 30. Ejemplo del profesor adaptado al dataset

Igual que en el enunciado, los dos flujos tienen la variable artificial leaky_feature, que es el objetivo más un ruido muy pequeño.

AUC con fuga: 1.000
AUC sin fuga: 1.000

Los dos flujos dan AUC = 1.000. Esto muestra algo que el ejemplo del enunciado no dice explícitamente: el Pipeline no protege contra la fuga del objetivo. leaky_feature es prácticamente la respuesta, así que cualquier modelo que la reciba acierta siempre, sin importar si el escalado se hizo antes o después del split. Un AUC perfecto en un problema clínico real es, casi siempre, una señal de fuga y no un buen modelo. Para ver qué corrige el Pipeline hay que separar las dos fugas, como se hace en la Parte 31.

Parte 31. Los dos tipos de fuga por separado

En el ejemplo del enunciado se mezclan dos fugas distintas:

  1. Fuga del objetivo (target leakage): una variable que contiene la respuesta. El Pipeline no la corrige, porque el problema está en los datos y no en el orden de los pasos.

  2. Fuga de preprocesamiento: los parámetros del escalador o del imputador se calculan con datos de prueba. Esto es lo que el Pipeline sí evita.

Para ver el efecto de cada una se prueban las 4 combinaciones con el mismo SVC y la misma búsqueda de hiperparámetros.

Loading...
  1. Con leaky_feature: AUC = 1.000 en las dos variantes. La fuga del objetivo domina todo.

  2. Sin leaky_feature: con el preprocesamiento antes del split, AUC CV = 0.9263 y AUC test = 0.9370. Dentro del Pipeline, AUC CV = 0.9269 y AUC test = 0.9374.

En este dataset la fuga de preprocesamiento casi no cambia el resultado: la diferencia está en el cuarto decimal. Para confirmar que no es cosa de una sola partición, la Parte 32 repite el experimento 50 veces.

Parte 32. ¿Cuánto infla el preprocesamiento antes del split? (50 repeticiones)

Una sola partición puede dar una diferencia por azar. Se repite el experimento con 50 seeds distintas y una regresión logística, comparando el AUC de test con el preprocesamiento ajustado antes del split y dentro del Pipeline.

Loading...
Wilcoxon pareado: p = 0.189
Repeticiones donde la fuga da mayor AUC: 13 de 50

En 50 particiones distintas, la diferencia media es −0.00005, y el preprocesamiento con fuga solo da mayor AUC en 13 de las 50. La prueba de Wilcoxon pareada no encuentra diferencia (p = 0.189).

El efecto es nulo porque los parámetros que aprende este preprocesador son muy estables. El mínimo y el máximo de MinMaxScaler y la mediana del imputador casi no cambian entre 734 y 918 pacientes, y el OneHotEncoder solo aprende la lista de categorías, que es la misma en train y en test. Aun así, ajustar antes del split sigue siendo un error metodológico:

  1. La evaluación deja de simular lo que pasa en producción, donde el paciente nuevo no estaba cuando se ajustó el escalador.

  2. Si el preprocesamiento aprende más de los datos, el sesgo crece. Pasa con la codificación por la media del objetivo, la selección de variables, el sobremuestreo con SMOTE o PCA. La Parte 33 muestra un ejemplo.

  3. Ponerlo dentro del Pipeline no cuesta nada y lo garantiza en cada fold de GridSearchCV.

Parte 33. Fuga por selección de variables (un caso donde la fuga sí engaña)

Se construye un caso extremo: 5,000 variables de ruido puro, generadas al azar y sin ninguna relación con heartdisease. Un modelo honesto debería dar AUC ≈ 0.5, que es lo mismo que adivinar.

  1. Con fuga: se eligen las 20 variables más relacionadas con y usando todo el dataset, y después se hace la validación cruzada.

  2. Sin fuga: la selección va dentro del Pipeline, así que en cada fold se elige solo con los datos de entrenamiento de ese fold.

AUC CV con fuga (selección antes de la CV) : 0.716 ± 0.021
AUC CV sin fuga (selección en el Pipeline) : 0.531 ± 0.045

Aquí la fuga sí engaña. Con variables que son ruido puro, la validación cruzada con fuga reporta AUC = 0.716 ± 0.021, como si el modelo tuviera capacidad predictiva. Con la selección dentro del Pipeline, el AUC baja a 0.531 ± 0.045, prácticamente el azar, que es la respuesta correcta.

La razón es que, entre 5,000 variables aleatorias, siempre habrá unas 20 que por casualidad se relacionan con y en esas 918 filas. Si se eligen mirando todas las filas, también se eligen mirando las de validación, y el modelo parece aprender un patrón que no existe.

Parte 34. Resumen gráfico de los experimentos de fuga

<Figure size 1100x528 with 1 Axes>

Modelado y comparación (flujo sin fuga)

Qué se hace y por qué

Con el flujo sin fuga ya definido, se entrenan varios modelos con el mismo preprocesamiento y la misma validación cruzada, y se comparan con AUC y Accuracy.

Se prueban varios porque no existe un modelo que sea el mejor en todos los problemas. Unos capturan relaciones lineales, como la regresión logística, y otros relaciones no lineales, como SVC, KNN y los modelos de árboles. GridSearchCV busca los mejores hiperparámetros de cada uno. El Pipeline garantiza que, en cada fold, el preprocesamiento se ajuste solo con los datos de entrenamiento de ese fold.

Parte 35. Funciones reutilizables

Se encapsula la lógica de entrenamiento y evaluación en funciones, como sugiere el enunciado. train_pipeline sigue la firma del ejemplo del profesor, pero usa el preprocesador de la Parte 29 en lugar de un MinMaxScaler solo, porque el dataset tiene categóricas y nulos.

Parte 36. Modelos y espacios de búsqueda

Se implementan los modelos vistos en el curso: el SVC del enunciado, los cuatro recomendados (LogisticRegression, RandomForestClassifier, KNeighborsClassifier y GradientBoostingClassifier) y dos más, un árbol de decisión y Naive Bayes gaussiano. Las grillas son pequeñas porque el dataset tiene solo 734 filas de entrenamiento.

Loading...

Parte 37. Entrenamiento con GridSearchCV

SVC                    12 combinaciones x 5 folds  ·     2.1 s  ·  AUC CV = 0.9269  ·  AUC test = 0.9374
LogisticRegression      8 combinaciones x 5 folds  ·     0.2 s  ·  AUC CV = 0.9292  ·  AUC test = 0.9301
KNN                    28 combinaciones x 5 folds  ·     0.7 s  ·  AUC CV = 0.9273  ·  AUC test = 0.9401
DecisionTree           40 combinaciones x 5 folds  ·     0.8 s  ·  AUC CV = 0.9091  ·  AUC test = 0.8977
RandomForest           18 combinaciones x 5 folds  ·     7.0 s  ·  AUC CV = 0.9343  ·  AUC test = 0.9341
GradientBoosting       24 combinaciones x 5 folds  ·     5.4 s  ·  AUC CV = 0.9329  ·  AUC test = 0.9311
GaussianNB              4 combinaciones x 5 folds  ·     0.1 s  ·  AUC CV = 0.9152  ·  AUC test = 0.9102

Tiempo total: 16.4 s

Parte 38. Ranking comparativo (AUC y Accuracy)

Loading...
Mejores hiperparámetros:
  1. KNN                  {'n_neighbors': 51, 'p': 2, 'weights': 'distance'}
  2. SVC                  {'C': 1, 'gamma': 0.1, 'kernel': 'rbf'}
  3. RandomForest         {'max_depth': None, 'max_features': 'sqrt', 'min_samples_leaf': 5, 'n_estimators': 300}
  4. GradientBoosting     {'learning_rate': 0.03, 'max_depth': 3, 'n_estimators': 100, 'subsample': 0.8}
  5. LogisticRegression   {'C': 1, 'penalty': 'l2'}
  6. GaussianNB           {'var_smoothing': 0.001}
  7. DecisionTree         {'criterion': 'gini', 'max_depth': 4, 'min_samples_leaf': 20}

Diferencia de AUC de test frente al SVC:
Loading...

Ranking por AUC de test:

  1. KNN: AUC = 0.940 y Accuracy = 0.913. Es el primero en las dos métricas.

  2. SVC: AUC = 0.937 y Accuracy = 0.880.

  3. RandomForest: AUC = 0.934 y Accuracy = 0.891.

  4. GradientBoosting: AUC = 0.931 y Accuracy = 0.880.

  5. LogisticRegression: AUC = 0.930 y Accuracy = 0.880.

  6. GaussianNB: AUC = 0.910 y Accuracy = 0.886.

  7. DecisionTree: AUC = 0.898 y Accuracy = 0.832.

Frente al SVC del enunciado, solo KNN lo supera (+0.003). RandomForest, GradientBoosting y la regresión logística quedan a menos de 0.008. Naive Bayes y el árbol de decisión quedan claramente por debajo, con −0.027 y −0.040.

  1. Las diferencias entre los 5 primeros son de menos de 0.01 de AUC. Son mucho menores que la variación entre folds (AUC_CV_std ≈ 0.025–0.037). Con solo 184 pacientes de prueba, un par de pacientes clasificados distinto cambia el orden. En la práctica, los 5 primeros empatan.

  2. Por AUC de validación cruzada, que usa 734 pacientes en lugar de 184, el orden cambia: RandomForest es el primero (0.934), seguido de GradientBoosting (0.933) y la regresión logística (0.929). KNN y SVC quedan con 0.927.

  3. KNN eligió weights = distance, así que en train cada punto es su propio vecino y memoriza los datos. Eso explica su brecha train–CV de 0.073, la mayor de todos. La regresión logística y Naive Bayes casi no tienen brecha (menos de 0.01).

  4. Todos tienen sensibilidad (recall) alta, entre 0.91 y 0.94, salvo el árbol (0.82). La especificidad es más baja y varía más: KNN llega a 0.878 y SVC y RandomForest se quedan en 0.829. Como se dijo en la Parte 14, los falsos positivos también importan, así que la especificidad debería pesar en la elección.

El modelo trivial tendría 55.4 % de accuracy, y todos los modelos lo superan por más de 27 puntos, por lo que SÍ se consiguió un amejora.

Parte 39. Gráficas comparativas

<Figure size 1870x550 with 3 Axes>

Curvas ROC de todos los modelos en el conjunto de prueba

<Figure size 825x715 with 1 Axes>
  1. Barras y curvas ROC: las curvas ROC de los 5 mejores se superponen casi por completo. Solo el árbol de decisión y Naive Bayes se separan hacia abajo. El árbol tiene una curva con pocos escalones, porque con max_depth = 4 solo produce unas pocas probabilidades distintas.

  2. Panel de folds: todos los modelos tienen un fold difícil, con AUC entre 0.84 y 0.89, y otros por encima de 0.93. Esa variación entre folds es mayor que la diferencia entre modelos, lo que confirma que el ranking de test no es concluyente entre los primeros puestos.

Conclusiones de la Etapa 1

  1. EDA: el dataset no tiene NaN explícitos, pero el 18.7 % de los valores de cholesterol son ceros imposibles y no faltan al azar. Se tratan con la mediana más un indicador de faltante, dentro del Pipeline.

  2. Preprocesamiento: se usa un ColumnTransformer con imputación, MinMaxScaler y One-Hot. El split estratificado se hace antes de ajustar cualquier cosa.

  3. Data leakage:

    1. La fuga del objetivo (leaky_feature) lleva el AUC a 1.000, y el Pipeline no la corrige: hay que detectarla revisando las variables.

    2. La fuga de preprocesamiento no cambia el resultado en este dataset (diferencia media −0.00005 en 50 repeticiones, p = 0.189), porque el escalado y la mediana son estadísticas estables.

    3. La fuga por selección de variables sí es grave: con ruido puro da AUC = 0.716 en lugar de 0.531.

    4. El Pipeline es la forma segura de evitar las dos últimas en cada fold de GridSearchCV.

  4. Modelos: los 7 modelos superan ampliamente al modelo trivial. KNN (AUC = 0.940, Accuracy = 0.913), SVC, RandomForest, GradientBoosting y la regresión logística quedan empatados en la práctica, con AUC de test entre 0.930 y 0.940. El árbol de decisión es el más débil (0.898).

  5. Para la Etapa 2: el modelo final no debe elegirse por el AUC de test. Si se usa test para escoger entre modelos, se introduce otra forma de fuga, porque test deja de ser una evaluación independiente. La elección se hará con el AUC de validación cruzada, donde RandomForest (0.934) y GradientBoosting (0.933) son los mejores. Test se usa una sola vez al final, para la matriz de confusión, la curva ROC y el AUC del notebook 2.