Notebook 1
1. Análisis Exploratorio de Datos (EAD): Diagnóstico de Diabetes
Este cuaderno aborda el flujo completo de un Análisis Exploratorio de Datos (EAD) aplicado a un conjunto de variables clínicas y de salud de una población de mujeres nativas americanas (Pima). El objetivo principal es limpiar, transformar, resumir y visualizar los datos para identificar patrones significativos, evaluar inconsistencias biológicas y verificar los supuestos estadísticos antes de cualquier etapa de modelado predictivo.
# ==============================================================================
# CONFIGURACIÓN DEL ENTORNO E IMPORTACIÓN DE LIBRERÍAS
# ==============================================================================
# Si necesitas instalar los paquetes previos, ejecuta en tu terminal:
# pip install pandas numpy matplotlib seaborn scipy
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import scipy.stats as stats
# Configuración visual global para los gráficos
sns.set_theme(style="whitegrid")
plt.rcParams['figure.figsize'] = (10, 6)
plt.rcParams['axes.titlesize'] = 14
plt.rcParams['axes.labelsize'] = 122. Carga y Estructuración del Dataset
El conjunto de datos se importa desde un repositorio centralizado de Machine Learning. El dataset consta de 768 registros y 9 columnas correspondientes a variables biomédicas. Configuración inicial de las variables y tipado correcto:
# URL del repositorio con los datos crudos
url = "https://raw.githubusercontent.com/jbrownlee/Datasets/master/pima-indians-diabetes.data.csv"
# Definición de las variables clínicas y objetivo
column_names = [
'pregnant', # Número de embarazos
'glucose', # Concentración de glucosa plasmática a las 2 horas
'pressure', # Presión arterial diastólica (mm Hg)
'triceps', # Grosor del pliegue cutáneo del tríceps (mm)
'insulin', # Insulina sérica de 2 horas (mu U/ml)
'mass', # Índice de masa corporal (peso en kg / (altura en m)^2)
'pedigree', # Función de pedigrí de diabetes (historial familiar)
'age', # Edad (años)
'diabetes' # Variable objetivo (0 = No presenta, 1 = Presenta)
]
# Cargar los datos en un DataFrame de pandas
pima = pd.read_csv(url, names=column_names)
# Transformación de la variable objetivo a tipo categórico descriptivo ('neg', 'pos')
pima['diabetes'] = pima['diabetes'].map({0: 'neg', 1: 'pos'}).astype('category')
print("Dataset estructurado e inicializado correctamente.")Dataset estructurado e inicializado correctamente.
3. Inspección Inicial de la Estructura de Datos
Antes de realizar operaciones matemáticas, examinamos las primeras filas del DataFrame y sus metadatos (tipos de datos asignados por columna y conteo de valores no nulos).
print("=== VISTA PREVIA DE LOS PRIMEROS REGISTROS ===")
display(pima.head())
print("\n=== ESPECIFICACIONES TÉCNICAS DEL DATAFRAME ===")
pima.info()=== VISTA PREVIA DE LOS PRIMEROS REGISTROS ===
pregnant glucose pressure triceps insulin mass pedigree age diabetes
0 6 148 72 35 0 33.6 0.627 50 pos
1 1 85 66 29 0 26.6 0.351 31 neg
2 8 183 64 0 0 23.3 0.672 32 pos
3 1 89 66 23 94 28.1 0.167 21 neg
4 0 137 40 35 168 43.1 2.288 33 pos
=== ESPECIFICACIONES TÉCNICAS DEL DATAFRAME ===
<class 'pandas.DataFrame'>
RangeIndex: 768 entries, 0 to 767
Data columns (total 9 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 pregnant 768 non-null int64
1 glucose 768 non-null int64
2 pressure 768 non-null int64
3 triceps 768 non-null int64
4 insulin 768 non-null int64
5 mass 768 non-null float64
6 pedigree 768 non-null float64
7 age 768 non-null int64
8 diabetes 768 non-null category
dtypes: category(1), float64(2), int64(6)
memory usage: 48.9 KB
4. Auditoría de Calidad de Datos y Resumen Estadístico
Calculamos las métricas de tendencia central y dispersión de los atributos numéricos para detectar anomalías o inconsistencias físicas en las mediciones.
# Resumen descriptivo de los atributos numéricos y categóricos
pima.describe(include='all') pregnant glucose pressure triceps insulin \
count 768.000000 768.000000 768.000000 768.000000 768.000000
unique NaN NaN NaN NaN NaN
top NaN NaN NaN NaN NaN
freq NaN NaN NaN NaN NaN
mean 3.845052 120.894531 69.105469 20.536458 79.799479
std 3.369578 31.972618 19.355807 15.952218 115.244002
min 0.000000 0.000000 0.000000 0.000000 0.000000
25% 1.000000 99.000000 62.000000 0.000000 0.000000
50% 3.000000 117.000000 72.000000 23.000000 30.500000
75% 6.000000 140.250000 80.000000 32.000000 127.250000
max 17.000000 199.000000 122.000000 99.000000 846.000000
mass pedigree age diabetes
count 768.000000 768.000000 768.000000 768
unique NaN NaN NaN 2
top NaN NaN NaN neg
freq NaN NaN NaN 500
mean 31.992578 0.471876 33.240885 NaN
std 7.884160 0.331329 11.760232 NaN
min 0.000000 0.078000 21.000000 NaN
25% 27.300000 0.243750 24.000000 NaN
50% 32.000000 0.372500 29.000000 NaN
75% 36.600000 0.626250 41.000000 NaN
max 67.100000 2.420000 81.000000 NaN Diagnóstico de Inconsistencia Fisiológica
Al inspeccionar el resumen estadístico, se detecta un problema crítico de calidad de datos: las variables glucose, pressure, triceps, insulin y mass muestran un valor mínimo (min) de 0.0. En la práctica médica, un valor de cero en la presión arterial, el grosor de la piel, la insulina o el índice de masa corporal es biológicamente imposible para un paciente vivo. Estos ceros representan valores faltantes ocultos que deben ser tratados adecuadamente.
5. Tratamiento de Valores Faltantes Ocultos
Para corregir la distorsión estadística que provocan los ceros artificiales, los reemplazamos por el indicador estándar de ausencia de datos (NaN). Posteriormente, analizamos visualmente su distribución e imputamos mediante la mediana de cada atributo.
# 1. Reemplazo de ceros inválidos por NaN
invalid_zero_cols = ['glucose', 'pressure', 'triceps', 'insulin', 'mass']
for col in invalid_zero_cols:
pima[col] = pima[col].replace(0, np.nan)
# 2. Cuantificación de la ausencia de datos
print("=== VALORES NULOS DETECTADOS POR COLUMNA ===")
print(pima.isnull().sum())
# 3. Mapeo visual de la estructura de valores faltantes
plt.figure(figsize=(10, 5))
sns.heatmap(pima.isnull(), cbar=False, cmap='viridis')
plt.title('Distribución Espacial de Valores Faltantes (NaN) en el Dataset')
plt.xlabel('Variables')
plt.ylabel('Registros (Índices)')
plt.show()
# 4. Imputación robusta utilizando la mediana (evita la distorsión por valores extremos)
for col in invalid_zero_cols:
median_value = pima[col].median()
pima[col] = pima[col].fillna(median_value)
print("\nProceso de limpieza completado: Valores nulos imputados mediante la mediana.")=== VALORES NULOS DETECTADOS POR COLUMNA ===
pregnant 0
glucose 5
pressure 35
triceps 227
insulin 374
mass 11
pedigree 0
age 0
diabetes 0
dtype: int64
Proceso de limpieza completado: Valores nulos imputados mediante la mediana.
6. Análisis de Distribución de la Variable Objetivo (diabetes)
Evaluamos el equilibrio de las clases de nuestra variable dependiente mediante un gráfico de frecuencias para entender la proporción de pacientes con diagnóstico positivo y negativo.
plt.figure(figsize=(6, 4))
# Corrección aplicada: se asigna x a hue y se desactiva la leyenda implícita
sns.countplot(data=pima, x='diabetes', hue='diabetes', palette='Set2', legend=False)
plt.title("Proporción y Balance de Clases de la Variable Objetivo")
plt.xlabel("Diagnóstico Clínico")
plt.ylabel("Frecuencia Absoluta")
# Cálculo exacto de las proporciones en el dataset
counts = pima['diabetes'].value_counts()
percentages = pima['diabetes'].value_counts(normalize=True) * 100
for idx, (cnt, pct) in enumerate(zip(counts, percentages)):
print(f"Clase '{counts.index[idx]}': {cnt} registros ({pct:.2f}%)")
plt.show()Clase 'neg': 500 registros (65.10%)
Clase 'pos': 268 registros (34.90%)
7. Análisis de Relación Numérica-Categórica e Identificación de Outliers
Utilizamos diagramas de caja (boxplots) para segmentar los atributos numéricos en función del diagnóstico médico. Esto nos permite visualizar simultáneamente la dispersión, la diferencia entre medianas y la presencia de valores atípicos (outliers) en las variables predictoras clave.
# Selección de variables clínicas de alto impacto predictivo
key_features = ['glucose', 'mass', 'age']
colors = ['Pastel1', 'Pastel2', 'Set3']
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
for i, var in enumerate(key_features):
# Corrección aplicada: se especifica hue='diabetes' y legend=False para silenciar los FutureWarnings
sns.boxplot(data=pima, x='diabetes', y=var, hue='diabetes', ax=axes[i], palette=colors[i], legend=False)
axes[i].set_title(f'Distribución de {var.capitalize()} por Diagnóstico')
axes[i].set_xlabel('Resultado (Diabetes)')
axes[i].set_ylabel(var)
plt.tight_layout()
plt.show()Interpretación de Gráficos de Caja
Se evidencia una separación visual marcada en las medianas de la concentración de glucose (glucosa) y mass (IMC) entre ambos grupos diagnósticos. El grupo identificado con diabetes (pos) exhibe rangos intercuartílicos significativamente más altos, lo que perfila a estas variables como fuertes predictores clínicos.
8. Evaluación de Supuestos Estadísticos: Normalidad
Para fundamentar la selección de futuras pruebas estadísticas (paramétricas o no paramétricas), determinamos si los atributos numéricos provienen de poblaciones con distribuciones Gaussianas (Normales).
A. Evaluación Analítica:
Prueba de Shapiro-Wilk
Definición del contraste de hipótesis con un nivel de significancia :
- : Los datos siguen una distribución normal.
- : Los datos no siguen una distribución normal.
numeric_cols = pima.select_dtypes(include=[np.number]).columns
for col in numeric_cols:
stat, p_value = stats.shapiro(pima[col])
decision = "No Normal (Rechaza H0)" if p_value < 0.05 else "Normal (No rechaza H0)"
print(f"Variable: {col:10} | Estadística W: {stat:.4f} | p-value: {p_value:.5f} -> {decision}")Variable: pregnant | Estadística W: 0.9043 | p-value: 0.00000 -> No Normal (Rechaza H0)
Variable: glucose | Estadística W: 0.9696 | p-value: 0.00000 -> No Normal (Rechaza H0)
Variable: pressure | Estadística W: 0.9878 | p-value: 0.00001 -> No Normal (Rechaza H0)
Variable: triceps | Estadística W: 0.9278 | p-value: 0.00000 -> No Normal (Rechaza H0)
Variable: insulin | Estadística W: 0.6415 | p-value: 0.00000 -> No Normal (Rechaza H0)
Variable: mass | Estadística W: 0.9794 | p-value: 0.00000 -> No Normal (Rechaza H0)
Variable: pedigree | Estadística W: 0.8365 | p-value: 0.00000 -> No Normal (Rechaza H0)
Variable: age | Estadística W: 0.8748 | p-value: 0.00000 -> No Normal (Rechaza H0)
Al arrojar todas las variables un p-value inferior a , se concluye analíticamente que ninguno de los componentes predictivos se distribuye de manera estrictamente normal.
B. Evaluación Gráfica: Histograma y Gráfico Q-Q (Caso de Estudio: glucose)
Para comprender la desviación matemática detectada en la prueba analítica, graficamos el comportamiento de los residuos y las colas de la variable glucose.
fig, axes = plt.subplots(1, 2, figsize=(16, 6))
# 1. Histograma empírico acoplado con Estimación de Densidad Kernel (KDE)
sns.histplot(pima['glucose'], kde=True, ax=axes[0], color='skyblue', stat="density")
axes[0].set_title('Histograma y Curva de Densidad para Glucosa')
axes[0].set_xlabel('Glucosa (mg/dL)')
axes[0].set_ylabel('Densidad Probabilística')
# 2. Gráfico Cuantil-Cuantil (Q-Q Plot) contra una distribución teórica normal
stats.probplot(pima['glucose'], dist="norm", plot=axes[1])
# Ajustes estéticos para identificar la tendencia lineal
axes[1].get_lines()[0].set_markerfacecolor('gray')
axes[1].get_lines()[0].set_markeredgecolor('gray')
axes[1].get_lines()[1].set_color('red')
axes[1].set_title('Gráfico de Probabilidad Cuantil-Cuantil (Q-Q) para Glucosa')
plt.tight_layout()
plt.show()Análisis Gráfico de la Distribución
Los puntos extremos en las zonas terminales del gráfico Q-Q se desvían de la recta diagonal roja. El desprendimiento en el extremo superior derecho evidencia una cola derecha alargada (asimetría positiva). Clínicamente, este comportamiento es coherente con la naturaleza de la población bajo estudio, donde existe un subgrupo de individuos con niveles glucémicos crónicos e inusualmente elevados como consecuencia patológica directa de la diabetes.