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

In [3]:
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)
In [4]:
#Chargement du fichier csv 

DATA_PATH = Path("..")/"data"/"raw"
heart = pd.read_csv(DATA_PATH/"heart.csv")
In [5]:
#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
In [6]:
heart.duplicated().sum()
Out[6]:
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).

In [7]:
heart.head()
Out[7]:
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
In [8]:
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

image.png

Section 2 : Nettoyage, Préparation et Analyse des données (EDA)

In [9]:
heart.describe()
Out[9]:
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é

In [10]:
heart.nunique()
Out[10]:
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 :

In [11]:
heart['RestingBP'] = heart['RestingBP'].replace(0, np.nan)
heart['Cholesterol'] = heart['Cholesterol'].replace(0, np.nan)
In [12]:
heart.isnull().sum()
Out[12]:
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
In [13]:
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.

In [14]:
#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¶

In [15]:
heart['HeartDisease'].value_counts()
Out[15]:
HeartDisease
1    508
0    410
Name: count, dtype: int64
In [16]:
heart['HeartDisease'].value_counts(normalize=True)
Out[16]:
HeartDisease
1    0.553377
0    0.446623
Name: proportion, dtype: float64
In [17]:
sns.countplot(data=heart, x='HeartDisease')
plt.title("Distribution de la variable cible")
plt.show()
No description has been provided for this image

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¶

In [18]:
heart.hist(figsize=(12,10))
Out[18]:
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)
No description has been provided for this image
In [19]:
for col in ['Age', 'RestingBP', 'Cholesterol', 'MaxHR', 'Oldpeak']:
    sns.boxplot(x=heart[col])
    plt.title(col)
    plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

Des valeurs extrêmes ont été observées dans certaines variables (notamment Cholesterol).
Ces valeurs peuvent correspondre à des cas médicaux réels.

In [20]:
plt.figure(figsize=(10,8))
sns.heatmap(heart.corr(numeric_only=True), annot=True, cmap='coolwarm')
Out[20]:
<Axes: >
No description has been provided for this image

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¶

In [21]:
for col in categorical_cols:
    sns.countplot(data=heart,x=col)
    plt.xticks(rotation=10)
    plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

Relation avec la target¶

Problématique de cette partie : Quels sont les facteurs de risque des maladies cardiaques ?

A - Variables numériques vs TARGET¶

In [22]:
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()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

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
In [23]:
sns.countplot(x="FastingBS", hue="HeartDisease", data=heart)
Out[23]:
<Axes: xlabel='FastingBS', ylabel='count'>
No description has been provided for this image

Une glycémie élevée semble être associée à un risque accru

In [24]:
sns.countplot(x="Cholesterol_missing", hue="HeartDisease", data=heart)
Out[24]:
<Axes: xlabel='Cholesterol_missing', ylabel='count'>
No description has been provided for this image

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¶

In [25]:
for col in categorical_cols:
    sns.countplot(data=heart, x=col, hue="HeartDisease")
    plt.title(col)
    plt.xticks(rotation=15)
    plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

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...)¶

In [26]:
heart.to_csv("../data/processed/heart_cleaning.csv", index=False)