Projet : Système d'aide au dépistage précoce du risque de maladie cardiaque
Bouzouita Hayette
Notebook 1 : EDA
Section 1 : Importation et compréhension des donnnées
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
from pathlib import Path
pd.set_option("display.max_columns",200)
pd.set_option("display.max_rows",200)
#Chargement du fichier csv
DATA_PATH = Path("..")/"data"/"raw"
heart = pd.read_csv(DATA_PATH/"heart.csv")
#check rapide du dataset : taille, typage, NaN
def quick_profile(df, name):
print(f"\n=== {name} ===")
print("shape:", df.shape)
print("\nDtypes:\n", df.dtypes)
na = (df.isna().mean().sort_values(ascending=False) * 100).round(1)
print("\n% NA (top 15):\n", na.head(15))
quick_profile(heart, "HEART")
=== HEART === shape: (918, 12) Dtypes: Age int64 Sex str ChestPainType str RestingBP int64 Cholesterol int64 FastingBS int64 RestingECG str MaxHR int64 ExerciseAngina str Oldpeak float64 ST_Slope str HeartDisease int64 dtype: object % NA (top 15): Age 0.0 Sex 0.0 ChestPainType 0.0 RestingBP 0.0 Cholesterol 0.0 FastingBS 0.0 RestingECG 0.0 MaxHR 0.0 ExerciseAngina 0.0 Oldpeak 0.0 ST_Slope 0.0 HeartDisease 0.0 dtype: float64
heart.duplicated().sum()
np.int64(0)
Le dataset contient 918 observations. Une observation = Un patient.
Le dataset ne contient pas d'identifiant patient, cela n'est pas problématique car chaque observation est indépendante, pas de doublons et aucun suivi longitudinal n'est nécessaire.
Il n'y a pas de valeurs manquantes explicites dans le jeu de données.
Le typage des variables est cohérent.
Il faudra vérifier la qualité des variables catégorielles/numériques.
La variable HeartDisease est la target (variable cible).
heart.head()
| Age | Sex | ChestPainType | RestingBP | Cholesterol | FastingBS | RestingECG | MaxHR | ExerciseAngina | Oldpeak | ST_Slope | HeartDisease | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 40 | M | ATA | 140 | 289 | 0 | Normal | 172 | N | 0.0 | Up | 0 |
| 1 | 49 | F | NAP | 160 | 180 | 0 | Normal | 156 | N | 1.0 | Flat | 1 |
| 2 | 37 | M | ATA | 130 | 283 | 0 | ST | 98 | N | 0.0 | Up | 0 |
| 3 | 48 | F | ASY | 138 | 214 | 0 | Normal | 108 | Y | 1.5 | Flat | 1 |
| 4 | 54 | M | NAP | 150 | 195 | 0 | Normal | 122 | N | 0.0 | Up | 0 |
categorical_cols = heart.select_dtypes(include="object").columns
for col in categorical_cols:
print(f"\n--- {col} ---")
print(heart[col].value_counts())
--- Sex --- Sex M 725 F 193 Name: count, dtype: int64 --- ChestPainType --- ChestPainType ASY 496 NAP 203 ATA 173 TA 46 Name: count, dtype: int64 --- RestingECG --- RestingECG Normal 552 LVH 188 ST 178 Name: count, dtype: int64 --- ExerciseAngina --- ExerciseAngina N 547 Y 371 Name: count, dtype: int64 --- ST_Slope --- ST_Slope Flat 460 Up 395 Down 63 Name: count, dtype: int64
C:\Users\bouzo\AppData\Local\Temp\ipykernel_16836\4107697228.py:1: Pandas4Warning: For backward compatibility, 'str' dtypes are included by select_dtypes when 'object' dtype is specified. This behavior is deprecated and will be removed in a future version. Explicitly pass 'str' to `include` to select them, or to `exclude` to remove them and silence this warning. See https://pandas.pydata.org/docs/user_guide/migration-3-strings.html#string-migration-select-dtypes for details on how to write code that works with pandas 2 and 3. categorical_cols = heart.select_dtypes(include="object").columns
Compréhension des variables et des modalités :
Age: age du patient (en années)
Sex: sexe (M= homme, F= femme)
ChestPainType: type de douleur thoracique (ATA -> douleur atypique du coeur ,NAP -> douleur non liée au coeur,ASY -> pas de symptôme ,TA -> douleur typique cardiaque )
RestingBP: pression artérielle (tension) au repos
Cholesterol: cholesterol (graisse dans le sang)
FastingBS: glycémie à jeun
RestingECG: ECG au repos (Normal -> OK, ST -> anomalie, LVH -> problème de muscle cardiaque)
MaxHR: Fréquence cardiaque max
ExerciseAngina: Angine à l'effort (Y -> Oui, N -> Non)
Oldpeak: dépression ST
ST_Slope: pente ST (Up -> montante, Flat -> plate, Down -> descendante)
HeartDisease: maladie cardiaque
Section 2 : Nettoyage, Préparation et Analyse des données (EDA)
heart.describe()
| Age | RestingBP | Cholesterol | FastingBS | MaxHR | Oldpeak | HeartDisease | |
|---|---|---|---|---|---|---|---|
| count | 918.000000 | 918.000000 | 918.000000 | 918.000000 | 918.000000 | 918.000000 | 918.000000 |
| mean | 53.510893 | 132.396514 | 198.799564 | 0.233115 | 136.809368 | 0.887364 | 0.553377 |
| std | 9.432617 | 18.514154 | 109.384145 | 0.423046 | 25.460334 | 1.066570 | 0.497414 |
| min | 28.000000 | 0.000000 | 0.000000 | 0.000000 | 60.000000 | -2.600000 | 0.000000 |
| 25% | 47.000000 | 120.000000 | 173.250000 | 0.000000 | 120.000000 | 0.000000 | 0.000000 |
| 50% | 54.000000 | 130.000000 | 223.000000 | 0.000000 | 138.000000 | 0.600000 | 1.000000 |
| 75% | 60.000000 | 140.000000 | 267.000000 | 0.000000 | 156.000000 | 1.500000 | 1.000000 |
| max | 77.000000 | 200.000000 | 603.000000 | 1.000000 | 202.000000 | 6.200000 | 1.000000 |
Age : [28;77] cohérent pour une population à risque cardiovasculaire
Cholesterol, RestingBP : minimum à 0, impossible biologiquement (très probablement des valeurs manquantes déguisées), valeur abhérente max 600 pour cholesterol
FastingBS (glycémie) : variable binaire, distribution désequilibrée
Oldpeak : valeurs négatives (un peu suspectes -> à vérifier)
HeartDisease : dataset assez équilibré
heart.nunique()
Age 50 Sex 2 ChestPainType 4 RestingBP 67 Cholesterol 222 FastingBS 2 RestingECG 3 MaxHR 119 ExerciseAngina 2 Oldpeak 53 ST_Slope 3 HeartDisease 2 dtype: int64
Correction des valeurs aberrantes :
heart['RestingBP'] = heart['RestingBP'].replace(0, np.nan)
heart['Cholesterol'] = heart['Cholesterol'].replace(0, np.nan)
heart.isnull().sum()
Age 0 Sex 0 ChestPainType 0 RestingBP 1 Cholesterol 172 FastingBS 0 RestingECG 0 MaxHR 0 ExerciseAngina 0 Oldpeak 0 ST_Slope 0 HeartDisease 0 dtype: int64
heart['RestingBP'] = heart['RestingBP'].fillna(heart['RestingBP'].median())
La variable Cholesterol contenait environ 19% de valeurs manquantes (valeurs initialement égales à 0, physiologiquement impossibles).
Les valeurs à 0 dans ce dataset ne sont pas juste “manquantes” , elles peuvent signifier :
- données non mesurées
- contexte clinique particulier
Donc le fait que la valeur soit manquante est une information. Ainsi, nous créons une variable indicatrice (Cholesterol_missing).
Les valeurs manquantes seront ensuite imputées par la médiane, méthode robuste aux valeurs extrêmes.
#création d'un flag
heart['Cholesterol_missing'] = heart['Cholesterol'].isnull().astype(int)
#imputation
heart['Cholesterol'] = heart['Cholesterol'].fillna(heart['Cholesterol'].median())
Analyse de la variable target¶
heart['HeartDisease'].value_counts()
HeartDisease 1 508 0 410 Name: count, dtype: int64
heart['HeartDisease'].value_counts(normalize=True)
HeartDisease 1 0.553377 0 0.446623 Name: proportion, dtype: float64
sns.countplot(data=heart, x='HeartDisease')
plt.title("Distribution de la variable cible")
plt.show()
Le dataset présente un léger désequilibre de classes (~55% vs ~45%).
Aucune technique de réequilibrage (oversampling/undersampling) n'est nécessaire à ce stade, mais cela pourra être testé lors de la phase de modélisation.
Analyse des variables numériques¶
heart.hist(figsize=(12,10))
array([[<Axes: title={'center': 'Age'}>,
<Axes: title={'center': 'RestingBP'}>,
<Axes: title={'center': 'Cholesterol'}>],
[<Axes: title={'center': 'FastingBS'}>,
<Axes: title={'center': 'MaxHR'}>,
<Axes: title={'center': 'Oldpeak'}>],
[<Axes: title={'center': 'HeartDisease'}>,
<Axes: title={'center': 'Cholesterol_missing'}>, <Axes: >]],
dtype=object)
for col in ['Age', 'RestingBP', 'Cholesterol', 'MaxHR', 'Oldpeak']:
sns.boxplot(x=heart[col])
plt.title(col)
plt.show()
Des valeurs extrêmes ont été observées dans certaines variables (notamment Cholesterol).
Ces valeurs peuvent correspondre à des cas médicaux réels.
plt.figure(figsize=(10,8))
sns.heatmap(heart.corr(numeric_only=True), annot=True, cmap='coolwarm')
<Axes: >
Les variables Oldpeak et MaxHR présentent les corrélations les plus fortes avec la variable cible.
- Oldpeak est positivement corrélé avec la maladie cardiaque (plus élevé -> plus de risque)
- MaxHR est négativement corrélé (plus élevé -> moins de risque)
- La variable Cholesterol_missing présente une corrélation significative, indiquant que l'absence de donnée peut être informative
- Le Cholesterol brut présente une faible corrélation, suggérant une relation non linéaire ou un bruit dans les données
- Aucune multicolinéarité forte n'a été détectée
Analyse des variables catégorielles¶
for col in categorical_cols:
sns.countplot(data=heart,x=col)
plt.xticks(rotation=10)
plt.show()
Relation avec la target¶
Problématique de cette partie : Quels sont les facteurs de risque des maladies cardiaques ?
A - Variables numériques vs TARGET¶
num_cols = heart.select_dtypes(include=['int64', 'float64']).columns
num_continue_cols = [col for col in num_cols
if heart[col].nunique() > 2 and col != "HeartDisease"]
for col in num_continue_cols:
sns.boxplot(data=heart, x="HeartDisease", y=col)
plt.title(col)
plt.show()
Analyse des variables numériques :
- L’âge, Oldpeak et MaxHR apparaissent comme les variables les plus discriminantes
- Oldpeak et MaxHR montrent une séparation nette entre patients sains et malades
- Le cholestérol et la pression artérielle ne semblent pas fortement discriminants individuellement (mais peut aider en combinaison)
- Certaines variables présentent des valeurs extrêmes, mais elles sont conservées
sns.countplot(x="FastingBS", hue="HeartDisease", data=heart)
<Axes: xlabel='FastingBS', ylabel='count'>
Une glycémie élevée semble être associée à un risque accru
sns.countplot(x="Cholesterol_missing", hue="HeartDisease", data=heart)
<Axes: xlabel='Cholesterol_missing', ylabel='count'>
Les valeurs manquantes ne sont pas aléatoires, elles semblent corrélées à la présence de maladie cardiaque
B - Variables catégorielles vs TARGET¶
for col in categorical_cols:
sns.countplot(data=heart, x=col, hue="HeartDisease")
plt.title(col)
plt.xticks(rotation=15)
plt.show()
Analyse des variables catégorielles :
- Le sexe masculin est associé à un risque plus élevé de maladie cardiaque
- Le type de douleur thoracique est fortement discriminant, en particulier la catégorie ASY
- L’angine à l’effort est un indicateur clé de la maladie
- La pente du segment ST est très informative, avec une forte séparation entre les classes
- Les résultats ECG au repos sont moins discriminants
Bilan :
Variables très discriminantes :
- ChestPainType
- ExerciseAngina
- ST_Slope
- Oldpeak (vu avant)
Variables modérément utiles :
- Sex
- Age
- MaxHR
Variables faibles seules :
- RestingECG
- Cholesterol
- RestingBP
Exportation
Préparation à la modélisation (encodage, standardisation...)¶
heart.to_csv("../data/processed/heart_cleaning.csv", index=False)