import pandas #Manipulation des données
import numpy as np
import matplotlib.pyplot as plt
import seaborn #Pour la heatmap et pairplot
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix
from sklearn.metrics import ConfusionMatrixDisplay #Pour le visuel de la matrice de confusion
import plotly.express as px #plot
#Classifieurs :
from sklearn.linear_model import LogisticRegression
from sklearn.discriminant_analysis import QuadraticDiscriminantAnalysis
from sklearn.svm import SVC
from sklearn.neighbors import KNeighborsClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import GradientBoostingClassifier
import lightgbm as lgbm #lgbm classifier
#Aides pour les metriques :
from sklearn.metrics import accuracy_score #Calcul de l'accuracy sur le réseau de neurone
from sklearn.metrics import precision_recall_fscore_support
#Selection de modèle :
from sklearn.model_selection import GridSearchCV
from sklearn.model_selection import RandomizedSearchCV
#Stat (pour le LGBM) :
from scipy.stats import randint as sp_randint
from scipy.stats import uniform as sp_uniform
#Reseau de neurones :
from keras.models import Sequential
from keras.layers import Dense, Dropout
#Clustering :
from sklearn.cluster import KMeans
#Scaling :
from sklearn.preprocessing import StandardScaler
#ignore warning messages :
import warnings
warnings.filterwarnings('ignore')
Pour introduire le projet commençons par une petite présentation du cadre, des données que nous avons et des variables qui compose notre base de données. Ensuite nous ferons une petite visualisation des données et une étude de leur corrélation afin de mieux comprendre les liens entre les variables. Ensuite nous nous essayerons à un clustering (apprentissage non supervisée) de type K-means pour voir si nous arrivons à identifier des groupes dans nos données. Et enfin, pour finir nous partirons sur la partie entrainement des différents classifieurs (en trois étapes : paramétrique, non paramétrique et deep learning) que nous avons vu en cours avant de conclure sur le choix que nous ferons ici pour notre problème.
Nous sommes dans un cas classique de classification, on veut prédire une variable discrète (ici 0 ou 1), supervisée car on a des données étiquetées. L'objectif de ce projet de Machine Learning est de concevoir un modèle d'apprentissage permettant de prédire avec le moins d'erreur possible (ie le score le plus élevé possible) si une nouvelle personne ayant un profil similaire mais n'appartenant pas à nos données, est diabétique ou non.
Pour cela nous disposons d'une base de données composées uniquement de femme d'au moins 21 ans et d'origine ameridienne (du peuple Pima) sur lesquelles nous avons mesuré 8 valeurs (variables explicatives) : Pregnancies, Glucose, BloodPressure, SkinThickness, Insulin, BMI, DiabetesPedigreeFunction, Age et pour lesquelles nous savons si elles sont atteinte ou non de diabète : Outcome (1 : positive). Regardons ce que représentent les variables explicatives :
- Pregnancies : Nombre de grossesses.
- Glucose : Concentration de glucose plasmatique deux heures après la consommation du sucre lors d'un test de tolérance au glucose.
- BloodPressure : Tension artérielle diastolique. Elle est considérée haute à partir de 90, basse a partir de 60.
- SkinThickness : Epaisseur du pli cutané (quand on pince la peau) au niveau du triceps, plus elle est élevé plus la personne à de risque de maladie cardiovasculaire.
- Insulin : Taux d'insuline.
- BMI : Body Mass Index (IMC)
- DiabetesPedigreeFunction : fonction indiquant la vraisemblance d'être diabétique. (Variable pas comprise)
- Age : L'âge en année.
NB : lien de vers la base de données : https://www.kaggle.com/datasets/akshaydattatraykhare/diabetes-dataset
##Recupération des données :
data=pandas.read_csv('E:/Projet M2/Machine Learning (Ok)/diabetes.csv')
##Séparation variables explicatives/outcome
X=data.get(["Pregnancies","Glucose","BloodPressure",
"SkinThickness","Insulin","BMI","DiabetesPedigreeFunction","Age"])
y=data["Outcome"]
##Visualisation d'une partie :
print(data.head(),"\n")
print("Shape des données : ", data.shape)
data_true=data[data["Outcome"]==1]
data_false=data[data["Outcome"]==0]
print("Nombre de diabétique :", len(data_true)," Proportion de diabétique :", len(data_true)/len(data))
print("Nombre de non-diabétique :", len(data_false)," Proportion de non-diabétique :", len(data_false)/len(data))
Pregnancies Glucose BloodPressure SkinThickness Insulin BMI \ 0 6 148 72 35 0 33.6 1 1 85 66 29 0 26.6 2 8 183 64 0 0 23.3 3 1 89 66 23 94 28.1 4 0 137 40 35 168 43.1 DiabetesPedigreeFunction Age Outcome 0 0.627 50 1 1 0.351 31 0 2 0.672 32 1 3 0.167 21 0 4 2.288 33 1 Shape des données : (768, 9) Nombre de diabétique : 268 Proportion de diabétique : 0.3489583333333333 Nombre de non-diabétique : 500 Proportion de non-diabétique : 0.6510416666666666
Après visualisation des premières lignes du dataset on en connait maintenant la forme générale, on retrouve bien la structure avec 8 variables explicatives et la variable de sortie. On connait maintenant également la taille de notre base de donnée ainsi que les proportions de diabétiques et non-diabétiques. On a 768 personnes au total, dont 268 diabétiques (35%) et 500 non-diabétiques (65%). La classe majoritaire est donc la classe 0 : les non-diabétiques.
On peut aussi déjà remarquer des variables avec des valeurs "anormales" : insuline à 0, bloodpressure à 0, skinthickness à 0. Au vu de ce que sont les variables ces valeurs n'ont aucun sens. Une hypothèse serait que ce sont des données manquantes mises à 0 par convention de la part de la personne ayant constitué la base de donnée.
Regardons de plus près ces données atypiques : combien il y en a, leur composition,...
insulin_0=data[data["Insulin"]==0]
skin_0=data[data["SkinThickness"]==0]
bp_0=data[data["BloodPressure"]==0]
gluco_0=data[data["Glucose"]==0]
bmi_0=data[data["BMI"]==0]
donnees_completes=data[(data["BloodPressure"]!=0) & (data["SkinThickness"]!=0)
& (data["Insulin"]!=0) & (data["Glucose"]!=0) & (data["BMI"]!=0)]
print("Nombre de données Insulin à 0 :" ,len(insulin_0))
print("Nombre de données SkinThickness à 0 :" ,len(skin_0))
print("Nombre de données BloodPressure à 0 :",len(bp_0))
print("Nombre de données Glucose à 0 :",len(gluco_0))
print("Nombre de données BMI à 0 :",len(bmi_0))
print("Nombre de données complètes :",len(donnees_completes) )
print("Nombre de données avec au moins un variable à 0 :", len(data)-len(donnees_completes))
print("Nombre d'insuline rempli dans ST_0 :",len(skin_0[skin_0["Insulin"]!=0]))
print("Nombre d'insuline rempli dans BP_0 :",len(bp_0[bp_0["Insulin"]!=0]))
print("Nombre d'insuline rempli dans Glu_0 :",len(gluco_0[gluco_0["Insulin"]!=0]))
print("Nombre d'insuline rempli dans Bmi_0 :",len(bmi_0[bmi_0["Insulin"]!=0]))
Nombre de données Insulin à 0 : 374 Nombre de données SkinThickness à 0 : 227 Nombre de données BloodPressure à 0 : 35 Nombre de données Glucose à 0 : 5 Nombre de données BMI à 0 : 11 Nombre de données complètes : 392 Nombre de données avec au moins un variable à 0 : 376 Nombre d'insuline rempli dans ST_0 : 0 Nombre d'insuline rempli dans BP_0 : 0 Nombre d'insuline rempli dans Glu_0 : 1 Nombre d'insuline rempli dans Bmi_0 : 1
On a donc 376 personnes avec des données "incomplètes" (un zéro dans au moins une des cinq variables). Il nous reste 392 données si on retire celles avec des 0 suspectées d'être des données non obtenues, manquantes. On remarque d'ailleurs que dans la plupart des cas le taux d'insuline est manquant lorsqu'une des quatres autre est manquante.
Question : Les proportions de diabétiques/non-diabétiques sont-elles identiques dans les données complètes et dans les données manquantes ou y a-t-il un comportement différent du dataset global ?
donnees_manquantes=data[(data["Glucose"]==0) | (data["Insulin"]==0)| (data["BMI"]==0)]
donnees_manquantes_true=donnees_manquantes[donnees_manquantes["Outcome"]==0]
print("Nombre de données manquantes :", len(donnees_manquantes))
print("Nombre de diabétiques parmi eux :",len(donnees_manquantes_true))
print("Proportion de diabétique :",len(donnees_manquantes_true)/len(donnees_manquantes))
print("Nombre de non-diabétiques parmi eux :", len(donnees_manquantes)-len(donnees_manquantes_true))
print("Proportion de non-diabétiques :",(len(donnees_manquantes)-len(donnees_manquantes_true))/len(donnees_manquantes))
Nombre de données manquantes : 376 Nombre de diabétiques parmi eux : 238 Proportion de diabétique : 0.6329787234042553 Nombre de non-diabétiques parmi eux : 138 Proportion de non-diabétiques : 0.3670212765957447
On oserve donc une forte majorité de diabétiques dans les données manquantes, la tendance s'est totalement renversée par rapport au dataset entier. On a ici 63% de diabétiques contre 37% de non-diabétiques ici alors qu'on avait 35% de diabétiques et 65% de non-diabétiques dans le dataset entier.
print("Nombre de données complètes :",len(donnees_completes))
donnees_completes_true=donnees_completes[donnees_completes["Outcome"]==1]
print("Nombre de diabétiques parmi eux :",len(donnees_completes_true))
print("Proportion de diabétique dedans :",len(donnees_completes_true)/len(donnees_completes))
print("Nombre de non-diabétiques parmi eux :", len(donnees_completes)-len(donnees_completes_true))
print("Proportion de non-diabétiques dedans :",(len(donnees_completes)-len(donnees_completes_true))/len(donnees_completes))
Nombre de données complètes : 392 Nombre de diabétiques parmi eux : 130 Proportion de diabétique dedans : 0.33163265306122447 Nombre de non-diabétiques parmi eux : 262 Proportion de non-diabétiques dedans : 0.6683673469387755
Parmi nos 394 personnes sans données manquantes, on a 130 diabétiqeus (33%) et 264 non-diabétiques (67%). Les proportions ne changent pas beaucoup par rapport aux proportions du dataset de base.
Plusieurs solutions s'offrent à nous concernant le traitement des données incomplètes, on pourrait :
1 - Les retirer tout simplement mais cela resulterait en une base de données beaucoup plus petite. Notre base étant déjà petite nous ne ferons pas cela.
2 - Les laisser tel quel, tout en sachant que les 0 peuvent un peu biaiser nos resultats. C'est l'option que nous allons developper dans ce projet.
3 - Remplacer les valeurs nulles par la moyenne des données disponible pour cette variable. Cette option a été developpé ici par une autre personne : https://www.analyticsvidhya.com/blog/2021/07/diabetes-prediction-with-pycaret/.
Regardons la corrélation entre les variables explicatives du dataset afin de mieux comprendre leurs liens.
- Corrélation : lien reciproque entre deux variables, notion qui contredit l'indépendance.
- Valeur de corrélation : Valeur entre -1 et 1 qui quantifie la liaison entre deux variables et comment elles évoluent les unes par rapport aux autres.
-> 0 : les variables sont non corrélées linéairement.
-> 1 : les variables évoluent dans le même sens (c'est une fonction affine croissante de l'autre).
-> -1 : les variables évoluent dans des sens opposés (fonction affine décroissante de l'autre).
-> Plus on est proche d'un extrême et plus la relation de corrélation est linéaire.
Essayons deux méthodes de visualisation pour voir cela : une heatmap avec les valeurs de corrélation linéaires entre variables et un pairplot pour essayer d'identifier des tendances (principalement non linéaires car la heatmap devrait suffire pour les linéaires).
La heatmap est une methode pour visualiser la matrice composée des coefficients de corrélation linéaire des variables entre elles en couleur afin de la rentre plus visuelle et compréhensible.
#Heatmap sur les donénes complètes :
plt.subplots(figsize=(8,8))
corrmat=donnees_completes.corr()
seaborn.heatmap(corrmat,vmax=1,square=True,annot=True)
plt.show()
#Heatmap sur les données totales
plt.subplots(figsize=(8,8))
corrmat=data.corr()
seaborn.heatmap(corrmat,vmax=1,square=True,annot=True)
plt.show()
Nous regardons les heatmap des données complètes et celle de la totalité des données afin d'essayer de voir des différences et d'éviter de tomber sur des cas un peu atypiques où le coefficient de corrélation se retrouve élevé à cause des 0 ou inversement des cas où le coefficient de corrélation est plus faible que ce qu'il devrait être.
Comme on pouvait s'y attendre les resultats sur les données dites "totales" (ie avec les données manquantes) sont différents de ceux uniquement sur les données dites "complètes". On remarque notamment le BMI/SkinThickness qui perd de sa superbe en rajoutant les 0 et BMI/Insulin qui se retrouve fortement boosté par ceux-ci.
Les plus grosses corrélations sur les données complètes sont : Age/Pregnancies, BMI/SkinThickness, Glucose/Insulin, Glucose/Outcome.
Toutes ces corrélations sont "logiques" si on y réflechit un peu et qu'on connait l'influence qu'a l'insuline sur la régulation du glucose, ou la relation entre un fort taux glucose (Hyperglycémie) et un diagnostique de diabète mais c'est une bonne chose de pouvoir confirmer sur nos données que ces relations "logiques" ressortent.
Remarquons d'ailleurs que la variable DiabetesPedigreeFunction n'est que très peu corrélé à la variable Outcome alors qu'on aurait pu s'attendre à une très forte corrélation entre les deux au vu de ce qu'elle est sensée être (la vraisemblance d'être diabétique). Cette variable étant très peu comprise il aurait été intéressant dans un contexte professionnel de se renseigner au près d'un professionnel pour savoir ce qu'elle représente vraiment.
Le pairplot nous permet de créer les plots des différentes combinaisons de variables de notre dataset avec en diagonal les densités marginales des variables.
Nous l'utiliserons principalement dans le but d'identifier des tendances que nous n'aurions pas pu voir avec la heatmap au sein de nos données.
Afin de ne pas être pertuber par la présence de 0 lors de notre observation des résultats nous utiliserons le pairplot sur les données complètes comme nous l'avons fait avec la heatmap (bien qu'on puisse perdre un peu d'exhaustivité dans nos informations notamment sur les variables où peu de données sont manquantes).
On omettra également la variable Outcome qui ne nous apportera rien ici.
seaborn.pairplot(donnees_completes,hue="Outcome",vars=["Pregnancies","Glucose","BloodPressure","SkinThickness","Insulin",
"BMI","DiabetesPedigreeFunction","Age"])
<seaborn.axisgrid.PairGrid at 0x1ac94b57fa0>
On peut observer quelques belles tendances linéaires telle que BMI/SkinThickness ce qui vient valider les résultats trouvés précedemment avec la heatmat.
Un peu moins évident nous avons Age/Pregnancies et Insulin/Glucose qui se démarquent un peu par un début de tendance, pas forcément linéaire pour Insulin/Glucose car la "queue" semble évoluer plus vite que la "base" ce qui pourrait faire penser à une tendance plutôt quadratique ou exponentielle bien que très faible.
On obtient globalement les mêmes informations qu'avec la heatmap, nous n'identifions pas de forte corrélation entre nos variables (à part skinthickness/bmi qui ressort bien ici).
Ce n'est pas un résultat "évident" car la heatmap est composée des coefficients de corrélation linéaire entre les variables. Il aurait suffit qu'on ait une corrélation non linéaire pour que le coefficient de corrélation linéaire ne nous alerte pas d'un possible lien entre nos variable. Le pairplot quant à lui nous aurait aidé à la voir bien qu'il soit fortement dépendant de l'analyse de la personne l'utilisant (je peux ne pas avoir identifié de corrélation là où un expert aurait pu en trouver une).
Comme dernière remarque sur ce pairplot, nous pouvons voir que la variable Glucose est très discriminante ou en tout cas beaucoup plus que les autres. En effet, on pourrait presque tracé une ligne entre les diabétiques et les non-diabétiques.
Afin de finir notre partie de visualisation des données et des liens entre les variables nous allons brièvement observer les resultats d'une ACP (analyse par composantes principales) sur nos données. Cette méthode est très utilisée et permet notamment d'identifier des possibles réduction de dimension, très utile dans des cas où nos avons beaucoup de variables explicatives et peu de données (ce n'est pas notre cas).
#Library pour ACP :
from sklearn.decomposition import PCA
#Normalisation :
ss = StandardScaler()
X_std = ss.fit_transform(X)
ACP_X_std=PCA().fit(X_std)
#Application de l'ACP sur nos données (sans la sortie) :
print("Composantes principales :\n", ACP_X_std.components_,"\n")
print("Pourcentage de variance expliquée par composante principale :\n",ACP_X_std.explained_variance_ratio_,"\n")
print("Nombre de composantes estimée par l'ACP :", ACP_X_std.n_components_,"\n")
print("Nombre de composantes réelles :", ACP_X_std.n_features_,"\n")
Composantes principales : [[ 0.1284321 0.39308257 0.36000261 0.43982428 0.43502617 0.45194134 0.27061144 0.19802707] [ 0.59378583 0.17402908 0.18389207 -0.33196534 -0.25078106 -0.1009598 -0.122069 0.62058853] [-0.01308692 0.46792282 -0.53549442 -0.2376738 0.33670893 -0.36186463 0.43318905 0.07524755] [ 0.08069115 -0.40432871 0.05598649 0.03797608 -0.34994376 0.05364595 0.8336801 0.0712006 ] [-0.47560573 0.46632804 0.32795306 -0.48786206 -0.34693481 0.25320376 0.11981049 -0.10928996] [ 0.19359817 0.09416176 -0.6341159 0.00958944 -0.27065061 0.68537218 -0.08578409 -0.03335717] [-0.58879003 -0.06015291 -0.19211793 0.28221253 -0.13200992 -0.03536644 -0.08609107 0.71208542] [ 0.11784098 0.45035526 -0.01129554 0.5662838 -0.54862138 -0.34151764 -0.00825873 -0.21166198]] Pourcentage de variance expliquée par composante principale : [0.26179749 0.21640127 0.12870373 0.10944113 0.09529305 0.08532855 0.05247702 0.05055776] Nombre de composantes estimée par l'ACP : 8 Nombre de composantes réelles : 8
PCA_values = np.arange(ACP_X_std.n_components_) + 1
plt.plot(PCA_values, ACP_X_std.explained_variance_ratio_, 'o-', linewidth=2, color='blue')
plt.title('Scree Plot')
plt.xlabel('Component Principal')
plt.ylabel('Proportion de la variance')
plt.show()
Nous pouvons tout d'abord remarqué que l'ACP n'estime pas qu'une reduction de dimension soit à envisager. En effet nous avons dans notre dataset 8 variables explicatives et l'algorithme après calcul suggère d'en garder 8, donc de garder la totalité de nos variables. Ceci est confirmé par la faible quantité de variance expliquée par nos composantes principales. En effet, la plus haute variance expliquée ne s'élève qu'à 26% et même en prenant les 3 premières composantes principales nous n'arrivons qu'à un total de 60% de variance expliquée par nos composantes principales.
Passons maintenant à l'étape de clustering (apprentissage non-supervisé) afin d'essayer d'identifier des profils au sein de nos données. Nous allons nous servir de l'algorithme des K-means afin d'essayer de voir si (très peu probable), l'algorithme arrive (sans lui donner les labels) à retrouver les groupes que nous avons identifié (diabétique/non-diabétique). Une fois que nous aurons fait cela nous verrons et quantifirons à quel point il se "trompe" par rapport à ce à quoi nous nous attendions.
Ce n'est pas parce que nous savons qu'il y a une séparation possible (diabétiques et non-diabétiques) que c'est la seule séparation en deux groupes dans nos données, encore moins la seule séparation possible ni même la plus "robuste" en terme de variance intra et inter-classe.
Il aurait pu être intéressant de regarder grâce à une méthode type graphe silhouette (en faisant les K-means avec k=2,3,4,5 par exemple) quelle est le nombre de groupe le plus robuste.
kmeans = KMeans(n_clusters=2,random_state=0).fit(X_std)
labels = kmeans.labels_
labels_vrai=sum(y==labels)
labels_faux=len(labels)-labels_vrai
print("Resultat : %d sur %d échantillons sont \"correctement\" classés" % (labels_vrai, y.size))
print("Proportion d\'échantillons bien classés : {0:0.2f}". format(labels_vrai/float(y.size)))
Resultat : 250 sur 768 échantillons sont "correctement" classés Proportion d'échantillons bien classés : 0.33
A la vue des résultats nous pouvons aisément conclure que l'algorithme des K-means ne retrouve pas les classes "diabétique" et "non-diabétique" mais bel et bien un tout autre groupement. En effet, parmi nos données il n'y en a que 33% qui se retrouve dans la classe au numéro de leur top (1 ou 0). Si nous considérons une inversion simple des noms de classes on aurait donc que 67% des échantillons sont bien classés ce qui n'est pas assez satisfaisant pour dire que les groupes retrouvés correspondent à ceux que nous cherchions (comme on s'y attendait).
Il faudrait pousser l'étude des classes un peu plus loin et avec un avis d'expert métier afin d'identifier les groupes créer par les K-means à deux classes. On pourrait, comme dit précedemment, essayer de voir si deux classes est le meilleur choix pour l'algorithme des K-means ou s'il est plus robuste sur 3,4,5 ou 6 classes par exemple.
Essayons brièvement la méthodes proposé plus haut : Les graphes silhouettes pour identifier le nombre de classe qui nous paraitrait le plus robuste à l'aide de deux indicateurs : les graphes et scores silhouettes.
- Un score silhouette peut varier entre 1 (bien classé) et -1 (mal classé).
- Le score silhouette associé à un graphe est la moyenne de tous les scores silhouette.
NB : Le code est très majoritairement une copie de celui disponible dans le lien suivant avec quelques modifications et suppressions légères : https://scikit-learn.org/stable/auto_examples/cluster/plot_kmeans_silhouette_analysis.html
#Library nécessaires :
from sklearn.metrics import silhouette_samples, silhouette_score
import matplotlib.cm as cm
#Choix de la liste de n_clusters à tester :
range_n_clusters = [2, 3, 4, 5, 6]
#Corps : (Fait les K-means, Calcul le score silhouette, Trace les graphes)
for n_clusters in range_n_clusters:
fig, ax1 = plt.subplots(1, 1)
clusterer = KMeans(n_clusters=n_clusters, random_state=0)
cluster_labels = clusterer.fit_predict(X_std)
silhouette_avg = silhouette_score(X_std, cluster_labels) ###Score silhouette
print("Score silhouette moyen :",silhouette_avg, "avec", n_clusters,"classes")
# Compute the silhouette scores for each sample
sample_silhouette_values = silhouette_samples(X_std, cluster_labels)
y_lower = 10
for i in range(n_clusters):
# Aggregate the silhouette scores for samples belonging to
# cluster i, and sort them
ith_cluster_silhouette_values = sample_silhouette_values[cluster_labels == i]
ith_cluster_silhouette_values.sort()
size_cluster_i = ith_cluster_silhouette_values.shape[0]
y_upper = y_lower + size_cluster_i
color = cm.nipy_spectral(float(i) / n_clusters)
ax1.fill_betweenx(
np.arange(y_lower, y_upper),
0,
ith_cluster_silhouette_values,
facecolor=color,
edgecolor=color,
alpha=0.7,
)
# Label the silhouette plots with their cluster numbers at the middle
ax1.text(-0.05, y_lower + 0.5 * size_cluster_i, str(i))
# Compute the new y_lower for next plot
y_lower = y_upper + 10 # 10 for the 0 samples
# The vertical line for average silhouette score of all the values
ax1.axvline(x=silhouette_avg, color="red", linestyle="--")
plt.show()
Score silhouette moyen : 0.1956540901138544 avec 2 classes Score silhouette moyen : 0.1784643674529339 avec 3 classes Score silhouette moyen : 0.20039690719846023 avec 4 classes Score silhouette moyen : 0.15291607045244326 avec 5 classes Score silhouette moyen : 0.16760637284099414 avec 6 classes
Ici nous obtenons des scores silhouette très faibles (maximum 0.2 pour n=4 classes) et les graphes silhouettes nous indiquent toujours que dans au moins une classe, une partie grande partie des observations de la classe sont mal classées (ie plus proche d'une autre classe que de celle la).
L'algorithme des K-means à donc du mal à trouver un bon groupement pour nos données. Ces resultats renforcent l'idée que nous nous faisions déjà : les données sont très groupées et peu séparables, un peu comme si elles ne formaient qu'un seul paquet.
Si nous ne devions nous fier qu'au score silhouette nous choisirions le k-means à 4 classes comme étant le plus robuste mais on peut voir que la classe 1 a quasiment 1/3 de son effectif mal classé. Ce n'est pas très rassurant.
Nous entrons à présent dans la partie la plus importante du projet : l'entrainement des modèles ainsi que la sélection du modèle qui nous aura parut le plus performant sur un échantillon dit de test et pertinant compte tenu de nos attentes.
Dans ce but, nous allons passer par plusieurs étapes : la composition des ensembles de train et de test sur lesquels l'algorithme apprendra et sera évalué, la normalisation des données afin de réduire la possible variabilité au niveau des échelles dans celles-ci, la présentation des éléments que nous allons utiliser afin d'évaluer nos modèles et ainsi nous finirons par une conclusion sur l'ensemble de nos modèles avant de finalement choisir celui qui se démarque positivement (s'il y en a un).
Dans notre étude nous allons nous essayer de prédire avec le moins d'erreur possible le fait qu'une personne soit diabétique ou non et avec de préference le moins de faux-négatif possible car dans notre contexte un faux-négatif pourrait avoir des conséquences sur la santé de la personne testée et faussement dite négative. C'est un choix que nous faisons de considérer qu'un faux négatif est plus grave qu'un faux positif. L'inverse aurait pu être pris.
La séparation train/test ne se fait qu'une fois et je l'enregistre afin d'obtenir toujours les mêmes resultats (ce qui n'est pas le cas si je relance un train_test_split).
Erratum : je pouvais aussi juste fixer le random_state dans la méthode train_test_split à une valeur et le split restait unique même en le relançant.
##Recupération des tables train et test créées : (Loïc)
data_train=pandas.read_csv('E:/Projet M2/Machine Learning (Ok)/data_train.csv')
data_test=pandas.read_csv('E:/Projet M2/Machine Learning (Ok)/data_test.csv')
##Séparation variables explicatives/variable expliquée
X_train=data_train.get(["Pregnancies","Glucose","BloodPressure","SkinThickness","Insulin","BMI","DiabetesPedigreeFunction","Age"])
y_train=data_train["Outcome"]
X_test=data_test.get(["Pregnancies","Glucose","BloodPressure","SkinThickness","Insulin","BMI","DiabetesPedigreeFunction","Age"])
y_test=data_test["Outcome"]
##Normalisation des données : (Khoa Anh)
ss = StandardScaler()
X_train_std = ss.fit_transform(X_train)
X_test_std = ss.transform(X_test)
##Etude de la qualité de la séparation :
print("Nombre de données train : ",len(data_train)," Nombre de diabétiques :",sum(data_train["Outcome"]))
print("Nombre de données test : ",len(data_test)," Nombre de diabétiques :",sum(data_test["Outcome"]))
Nombre de données train : 576 Nombre de diabétiques : 205 Nombre de données test : 192 Nombre de diabétiques : 63
Afin d'évaluer notre prédicteur nous allons utiliser plusieurs outils pour mesurer et visualiser sa performance sur l'échantillon de test :
- La matrice de confusion : matrice permettant de visualiser les resultats du prédicteur en 4 catégories : Vrai positif (TP), faux positif (FP), faux négatif (FN), vrai négatif (TN).
- L'accuracy (que nous appelerons score bien que ce soit un choix de prendre l'accuracy comme mesure de scoring) : la somme de ses bonnes prédictions sur le nombre total de prédiction qu'il a fait.
- La precision : TP / (TP + FP)
- Le Recall : TP / (TP + FN)
- Le F1 score : 2 x ((Precision x Recall) / (Precision + Recall))
Afin de nous créer un modèle de "référence", par rapport auquel nous allons nous comparer, nous définissons le prédicteur que nous appelerons bête ou naïf qui prédit toujours la classe dominante (ici non-diabétique : 0). On regardera son score et comparera le score des prochains classifieurs à celui-ci afin de "mesurer" ce qu'on gagne en changeant pour un autre classifieur.
score_bete=(len(y_test)-sum(y_test))/len(y_test)
print("Score du prédicteur naïf :", score_bete)
y_pred = 0*y_test
cm=confusion_matrix(y_test, y_pred)
cm_display = ConfusionMatrixDisplay(cm).plot()
print('Taux de mauvaise classification sur la classe 1 :',1) #On se trompe tout le temps
mv_class_bete=1
res_pred_bete=list([0,0,0]) ###Toujours 0 car il a aucun TP vu qu'il prédit jamais de positif
Score du prédicteur naïf : 0.671875 Taux de mauvaise classification sur la classe 1 : 1
Le prédicteur répondant toujours non-diabétique a un score de 0.672, ce qui est très faible surtout dans un contexte impactant la santé des personnes testée. Il se trompe quasiment 1 fois sur 3 en disant qu'un diabétique n'est pas diabétique. Rien que sur notre échantillon de test 63 diabétiques n'aurait pas été détéctés en tant que tel.
Commençons par comparer notre prédicteur bête à deux des classifieurs que je trouve les plus intuitifs, à savoir l'arbre de décision (CART) et les K-NN, afin de nous donner une petite idée de ce que nous pouvons espérer avoir des classifieurs plus complexes.
Ces deux modèles font partie de la famille des modèles non paramétriques et son, il me semble, les deux modèles les plus connus de cette famille car très simple à comprendre et donc explicable à des personnes avec un bangage non mathématique (typiquement aux supérieurs en entreprise).
Le principe de l'algorithme CART est de decouper notre espace de données par rapport à certaines features et cela jusqu'à atteindre une certaine profondeur d'arbre (ce que nous utiliserons ici) ou un certains nombre d'éléments minimum dans chaque "feuille" ou qu'on passe en dessous d'un certains nombre d'élément par feuille. Après l'étape de séparation des données en feuille l'algorithme fera un choix en fonction de la majorité représentée dans la feuille (1 ou 0). Le choix de la profondeur de l'arbre est un des principaux paramètres (voir le principal) qu'on a à optimiser lorsqu'on utilise cette méthode.
grid_dt=GridSearchCV(estimator=DecisionTreeClassifier(),param_grid={'max_depth':np.arange(1,50,step=1)},
scoring="accuracy",cv=10,verbose=1)
grid_dt.fit(X_train_std,y_train)
print("Meilleurs paramètres :",grid_dt.best_params_)
best_DT=grid_dt.best_score_
print("Meilleur score du Decision Tree sur le train :",best_DT)
score_DT=grid_dt.score(X_test_std,y_test)
print("Score du Decision Tree sur le test :",score_DT)
#Matrice de confusion :
y_pred=grid_dt.predict(X_test_std)
cm=confusion_matrix(y_test, y_pred)
cm_display = ConfusionMatrixDisplay(cm).plot()
mv_class_dt=cm[1,0]/(cm[1,0]+cm[1,1])
print('Taux de mauvaise classification sur la classe 1 :',mv_class_dt) #FN/(FN+TP)
res_dt=precision_recall_fscore_support(y_test,y_pred,average='weighted')[0:3]
Fitting 10 folds for each of 49 candidates, totalling 490 fits Meilleurs paramètres : {'max_depth': 2} Meilleur score du Decision Tree sur le train : 0.7411978221415608 Score du Decision Tree sur le test : 0.7239583333333334 Taux de mauvaise classification sur la classe 1 : 0.7301587301587301
L'algorithme des K-NN (pour K-Nearest Neighbors) est un algorithme tout aussi simple à comprendre que l'arbre de régression (voir plus) car il s'agit de regarder les k plus proches voisins du point dont on cherche à prédire la sortie et de regarder qu'elle est la majorité représentée parmi ce groupe de k personne. A l'instant du choix de la profondeur de l'arbre pour le CART, le choix de K joue un rôle important ici est doit être optimisé au mieux.
grid_knn=GridSearchCV(estimator=KNeighborsClassifier(),param_grid={'n_neighbors':np.arange(2,100,step=1)},
scoring="accuracy",cv=10,verbose=1)
grid_knn.fit(X_train_std,y_train)
print("Meilleurs paramètres :",grid_knn.best_params_)
best_knn=grid_knn.best_score_
print("Meilleur score des K-Nearest Neighbour sur le train :",best_knn)
score_knn=grid_knn.score(X_test_std,y_test)
print("Score des K-Nearest Neighbour sur le test :",score_knn)
#Matrice de confusion :
y_pred=grid_knn.predict(X_test_std)
cm=confusion_matrix(y_test, y_pred)
cm_display = ConfusionMatrixDisplay(cm).plot()
mv_class_knn=cm[1,0]/(cm[1,0]+cm[1,1])
print('Taux de mauvaise classification sur la classe 1 :',mv_class_knn) #FN/(FN+TP)
res_knn=precision_recall_fscore_support(y_test,y_pred,average='weighted')[0:3]
Fitting 10 folds for each of 98 candidates, totalling 980 fits Meilleurs paramètres : {'n_neighbors': 45} Meilleur score des K-Nearest Neighbour sur le train : 0.7463399879007865 Score des K-Nearest Neighbour sur le test : 0.8020833333333334 Taux de mauvaise classification sur la classe 1 : 0.5555555555555556
Après implémentation des deux algorithmes nous pouvons voir que l'utilisons même d'un simple arbre de regression (très rapide et appelé weak-learner à cause de ses faibles capacités) améliore déjà les prédictions que ce qu'on aurait pu avoir en utilisant le prédicteur de base. On passe d'un score de 0.672 à 0.724 soit quasiment 5 points de plus et 17 faux négatifs de moins !
L'algorithme des K-NN lui obtient un score très louable de 0.802, battant l'arbre de décision de près de 8 points (soit 13 de plus que le prédicteur de base) et un total de 35 faux négatifs.
On peut aussi remarquer que nos deux algorithmes (K-NN et CART) ont un taux de faux positisf très faible : 7 pour le cart et 3 pour les k-nn mais un taux de reussite sur la prédiction de la classe 1 très faible aussi ! Il se trompe plus d'une fois sur deux lorsqu'il doit prédire cette classe !
Passons maintenant à des modèles paramètriques, relativement plus complèxes que les précedents. Nous les utiliserons dans l'ordre du cours sans raison particulière derrière cela.
Nous utiliserons donc à tour de rôle la régression logistique (sur laquelle nous optimiserons le paramètre de regularisation et le solveur utilisé), une QDA (Quadratic Discriminant Analysis) qui fait l'hypothèse que les données sont distribuées selon des gaussiennes et enfin un SVM (Support Vector Machine) avec kernel (rbf ou sigmoid) optimisé en même temps que le paramètre de regularisation.
grid_lr=GridSearchCV(estimator=LogisticRegression(max_iter=5000),param_grid={'C':np.arange(0.5,30,step=0.5),'solver':['newton-cg','lbfgs','liblinear']},
scoring="accuracy",cv=10,verbose=1)
grid_lr.fit(X_train_std,y_train)
print("Meilleurs paramètres :",grid_lr.best_params_)
best_reg=grid_lr.best_score_
print("Meilleur score de la Regression Logistique sur le train :",best_reg)
score_reg=grid_lr.score(X_test_std,y_test)
print("Score de la Regression Logistique sur le test :",score_reg)
#Matrice de confusion :
y_pred=grid_lr.predict(X_test_std)
cm=confusion_matrix(y_test, y_pred)
cm_display = ConfusionMatrixDisplay(cm).plot()
mv_class_lr=cm[1,0]/(cm[1,0]+cm[1,1])
print('Taux de mauvaise classification sur la classe 1 :',mv_class_lr) #FN/(FN+TP)
res_reg_log=precision_recall_fscore_support(y_test,y_pred,average='weighted')[0:3]
Fitting 10 folds for each of 177 candidates, totalling 1770 fits Meilleurs paramètres : {'C': 1.0, 'solver': 'liblinear'} Meilleur score de la Regression Logistique sur le train : 0.7743799153055052 Score de la Regression Logistique sur le test : 0.8177083333333334 Taux de mauvaise classification sur la classe 1 : 0.4444444444444444
clf_qda=QuadraticDiscriminantAnalysis().fit(X_train_std,y_train)
score_qda=clf_qda.score(X_test_std,y_test)
print("Score du QDA sur le test :", score_qda)
#Matrice de confusion :
y_pred=clf_qda.predict(X_test_std)
cm=confusion_matrix(y_test, y_pred)
cm_display = ConfusionMatrixDisplay(cm).plot()
mv_class_qda=cm[1,0]/(cm[1,0]+cm[1,1])
print('Taux de mauvaise classification sur la classe 1 :',mv_class_qda) #FN/(FN+TP)
res_qda=precision_recall_fscore_support(y_test,y_pred,average='weighted')[0:3]
Score du QDA sur le test : 0.8020833333333334 Taux de mauvaise classification sur la classe 1 : 0.4444444444444444
grid_svc=GridSearchCV(estimator=SVC(),param_grid={'C':np.arange(0,20,step=1),'kernel':['rbf','sigmoid'],'gamma':['auto','scale']},
scoring="accuracy",cv=10,verbose=1,refit=True)
grid_svc.fit(X_train_std,y_train)
print("Meilleur paramètres :",grid_svc.best_params_)
best_svc=grid_svc.best_score_
print("Meilleure score du SVM sur le train :",best_svc)
score_svc=grid_svc.score(X_test_std,y_test)
print("Score du SVM sur le test :",score_svc)
#Matrice de confusion :
y_pred=grid_svc.predict(X_test_std)
cm=confusion_matrix(y_test, y_pred)
cm_display = ConfusionMatrixDisplay(cm).plot()
mv_class_svm=cm[1,0]/(cm[1,0]+cm[1,1])
print('Taux de mauvaise classification sur la classe 1 :',mv_class_svm) #FN/(FN+TP)
res_svm=precision_recall_fscore_support(y_test,y_pred,average='weighted')[0:3]
Fitting 10 folds for each of 80 candidates, totalling 800 fits Meilleur paramètres : {'C': 1, 'gamma': 'auto', 'kernel': 'rbf'} Meilleure score du SVM sur le train : 0.7534180278281911 Score du SVM sur le test : 0.796875 Taux de mauvaise classification sur la classe 1 : 0.5079365079365079
Des trois méthodes implémentées nous pouvons déjà dire qu'elle ont toutes un score relativement proche (dans une fourchette de plus ou moins 2% entre la plus petite et la plus grande valeur) mais avec quand même une démarcation de la regression logistique qui est pourtant un algorithme de prédiction de base mais qui, ici, sur ce jeu de données et avec ces paramètres là, fonctionne très bien et obtient un score de 0.818 ce qui en fait le prédicteur le plus éfficace jusqu'à présent.
Le SVM et le QDA eux se retrouve un peu derrière avec des resultats très proche (0.797 et 0.802 respectivement). Ils se placent donc, juste compte tenu des scores, au même niveau que les K-NN.
Du côté des faux-négatifs, comme pour le score, la régression logistique l'emporte aussi sur ce point avec 28 faux-négatifs, à égalité d'ailleurs avec le QDA. Quant au SVM il se retrouve à 32 faux-négatifs, ce qui rapproche ses performances des K-NN (35) et qui fait du QDA un meilleur choix que les K-NN de notre point de vue.
La même conclusion peut être faite ici concernant les performances des classifieurs sur la classe 1 (très mauvais) et ceux sur la classe 0 (bon).
Après ces deux étapes d'implémentation de modèles d'apprentissage nous pouvons déjà voir qu'un algorithme sort du lot avec des performances raisonnable bien que pas excellent non plus, 81.8% de réussite ne suffisent pas à faire de la régression logistique un algorithme satisfaisant. Voyons si nous ne pouvons pas faire mieux en allant du coté non paramètrique et si, par hasard, nous ne pourrions pas trouver un classifieur qui obtienne des bon resultats même sur la classe 1.
Les modèles non-paramétriques que nous allons utiliser sont des méthodes dites de forêt. Ce sont des méthodes qui consistent en l'utilisation de plusieurs weak-learners (des CART) régroupés afin de créer de la diversité et rendre l'algorithme plus fort, plus robuste et plus précis.
Dans un premier temps nous ferons une Random Forest (donc un même par aggregation d'arbre en parallèle) sur laquelle nous choisirons le nombre de weak-learners ainsi la profondeur maximale de ceux-ci.
Ensuite nous verrons deux méthodes de Gradient Boosting (aggrégation d'arbre séquentiellement avec des poids sur chacun), le gradient boosting disponible dans sklearn et celui de LightGBM.
grid_rf=GridSearchCV(estimator=RandomForestClassifier(),param_grid={"n_estimators":np.arange(20,100,step=1),
'max_depth':np.arange(2,15,step=1)}
,scoring="accuracy",cv=5,verbose=1)
grid_rf.fit(X_train_std,y_train)
print("Meilleurs paramètres :",grid_rf.best_params_)
best_RF=grid_rf.best_score_
print("Meilleur score de la Random Forest sur le train :",best_RF)
score_RF=grid_rf.score(X_test_std,y_test)
print("Score de la Random Forest sur le test :",score_RF)
#Matrice de confusion :
y_pred=grid_rf.predict(X_test_std)
cm=confusion_matrix(y_test, y_pred)
cm_display = ConfusionMatrixDisplay(cm).plot()
mv_class_rf=cm[1,0]/(cm[1,0]+cm[1,1])
print('Taux de mauvaise classification sur la classe 1 :',mv_class_rf) #FN/(FN+TP)
res_rf=precision_recall_fscore_support(y_test,y_pred,average='weighted')[0:3]
Fitting 5 folds for each of 1040 candidates, totalling 5200 fits Meilleurs paramètres : {'max_depth': 6, 'n_estimators': 94} Meilleur score de la Random Forest sur le train : 0.7622638680659671 Score de la Random Forest sur le test : 0.7864583333333334 Taux de mauvaise classification sur la classe 1 : 0.5079365079365079
grid_gb=GridSearchCV(GradientBoostingClassifier(),param_grid={'learning_rate': [0.05, 0.1, 0.5],
'max_features': np.arange(1,10,step=1), 'max_depth': np.arange(1,5,step=1)},
cv=10, scoring='accuracy',verbose=1)
grid_gb.fit(X_train_std,y_train)
print("Meilleurs paramètres :",grid_gb.best_params_)
best_gb=grid_gb.best_score_
print("Meilleur score du Gradient Boosting sur le train :",best_gb)
score_gb=grid_gb.score(X_test_std,y_test)
print("Score du Gradient Boosting sur le test :",score_gb)
#Matrice de confusion :
y_pred=grid_gb.predict(X_test_std)
cm=confusion_matrix(y_test, y_pred)
cm_display = ConfusionMatrixDisplay(cm).plot()
mv_class_gb=cm[1,0]/(cm[1,0]+cm[1,1])
print('Taux de mauvaise classification sur la classe 1 :',mv_class_gb) #FN/(FN+TP)
res_gb=precision_recall_fscore_support(y_test,y_pred,average='weighted')[0:3]
Fitting 10 folds for each of 108 candidates, totalling 1080 fits Meilleurs paramètres : {'learning_rate': 0.05, 'max_depth': 1, 'max_features': 4} Meilleur score du Gradient Boosting sur le train : 0.7639745916515426 Score du Gradient Boosting sur le test : 0.7760416666666666 Taux de mauvaise classification sur la classe 1 : 0.5555555555555556
fit_params = {"early_stopping_rounds" : 100,
"eval_metric" : 'auc',
"eval_set" : [(X,y)],
'eval_names': ['valid'],
'verbose': 0,
'categorical_feature': 'auto'}
param_test = {'learning_rate' : np.arange(0.1,0.5,step=0.1),
'n_estimators' : np.arange(100,500,step=100),
'max_depth': [3, 4, 5]}
#intialize lgbm and launch the search
clf_LGBM = lgbm.LGBMClassifier(random_state=50, silent=True, metric='None', n_jobs=4)
#gridsearch
grid = RandomizedSearchCV(
estimator=clf_LGBM, param_distributions=param_test,
n_iter=400,
scoring='accuracy',
cv=10,
refit=True,
random_state=50,
verbose=1)
grid.fit(X_train_std, y_train, **fit_params)
print("Meilleurs parametres :",grid.best_params_)
print("Meilleur score de LGBM Classifier sur le train :",grid.best_score_)
score_LGBM=grid.score(X_test_std,y_test)
print("Score de LGBM sur le test : ", score_LGBM)
#Matrice de confusion :
y_pred=grid.predict(X_test_std)
cm=confusion_matrix(y_test, y_pred)
cm_display = ConfusionMatrixDisplay(cm).plot()
mv_class_lgbm=cm[1,0]/(cm[1,0]+cm[1,1])
print('Taux de mauvaise classification sur la classe 1 :',mv_class_lgbm) #FN/(FN+TP)
res_lgbm=precision_recall_fscore_support(y_test,y_pred,average='weighted')[0:3]
Fitting 10 folds for each of 48 candidates, totalling 480 fits Meilleurs parametres : {'n_estimators': 100, 'max_depth': 5, 'learning_rate': 0.2} Meilleur score de LGBM Classifier sur le train : 0.7464912280701755 Score de LGBM sur le test : 0.7916666666666666 Taux de mauvaise classification sur la classe 1 : 0.4126984126984127
Remarquons tout d'abord que le bagging obtient un meilleur score que le boosting sur notre jeu de donnée mais le LightGBM arrive quand même à être proche (en terme de score) de ce que peut faire la Random Forest et arrive même à avoir un nombre de faux négatif inférieur bien que très proche (26 contre 28).
On peut aussi s'appercevoir que le Gradient Boosting avec le package LightGBM est meilleur que celui de sklearn à la fois en score et en nombre de faux négatifs. En effet, le LightGBM obtient 0.792 de score avec 26 faux-négatifs. Le Gradient Boosting de sklearn donne quant à lui des resultats pire que le SVM.
Aucun algorithme n'arrive à prédire de façon satisfaisante la classe 1.
Pour conclure la partie entrainement de prédicteur nous allons nous essayer aux reseaux de neurones grâce au package Keras vu en cours ainsi qu'un perceptron multicouche disponible dans sklearn.
# creer le modele, ajouter les layers
model = Sequential()
# Ajouter le "input layer" et le 1er "hidden layer"
model.add(Dense(units= 6, kernel_initializer = 'uniform', activation = 'relu'))
# Ajouter le 2eme "hidden layer"
model.add(Dense(units= 6, kernel_initializer = 'uniform', activation = 'relu'))
# Ajouter le "output layer"
model.add(Dense(units = 1, kernel_initializer = 'uniform', activation = 'sigmoid'))
# compiler le modele, avec adam gradient descent
model.compile(loss="binary_crossentropy", optimizer="adam", metrics=['accuracy'])
# Entrainer le network
clf_rn=model.fit(X_train_std, y_train, epochs = 1000, batch_size=10, validation_data=(X_test_std, y_test),verbose=0)
y_pred_test = model.predict(X_test_std)
y_pred_test = y_pred_test>0.5
y_pred_train = model.predict(X_train_std)
y_pred_train = y_pred_train>0.5
score_RN = accuracy_score(y_test,y_pred_test)
print("Score sur le train = ",accuracy_score(y_train,y_pred_train))
print("Score sur le test = ",score_RN)
#Matrice de confusion :
cm = confusion_matrix(y_test, y_pred_test)
cm_display = ConfusionMatrixDisplay(cm).plot()
mv_class_rn=cm[1,0]/(cm[1,0]+cm[1,1])
print('Taux de mauvaise classification sur la classe 1 :',mv_class_rn) #FN/(FN+TP)
#Calcul des différents score :
prec=cm[1,1]/(cm[1,1]+cm[0,1])
recall=cm[1,1]/(cm[1,1]+cm[1,0])
f1=prec*recall*2/(prec+recall)
res_rn=[prec,recall,f1]
Score sur le train = 0.8055555555555556 Score sur le test = 0.8020833333333334 Taux de mauvaise classification sur la classe 1 : 0.31746031746031744
L'utilisation de ce reseau de neurone ne donne pas du tout des resultats concluant mais permet tout de même, avec seulement 2 couches cachées d'obtenir des resultats proche de ceux de notre LightGBM.
#https://scikit-learn.org/stable/modules/generated/sklearn.neural_network.MLPClassifier.html#sklearn.neural_network.MLPClassifier
from sklearn.neural_network import MLPClassifier
##Activation logistique car classification binaire,
grip_mlp = GridSearchCV(MLPClassifier(max_iter=300,random_state=0,activation='logistic',solver='adam'),param_grid={
'alpha' : [0,1e-8,1e-7,1e-6,1e-5, 1e-4], #Param de regul
'learning_rate': ['constant', 'invscaling', 'adaptive'],
'hidden_layer_sizes':[100,150,200]},
cv=10, scoring='accuracy',verbose=1)
grip_mlp.fit(X_train_std,y_train)
print("Meilleurs paramètres :",grip_mlp.best_params_)
best_mlp=grip_mlp.best_score_
print("Meilleur score sur le train :", best_mlp)
score_mlp=grip_mlp.score(X_test_std, y_test)
print("Score du MLP sur le test : ", score_mlp)
#Matrice de confusion :
y_pred=grip_mlp.predict(X_test_std)
cm=confusion_matrix(y_test, y_pred)
cm_display = ConfusionMatrixDisplay(cm).plot()
mv_class_mlp=cm[1,0]/(cm[1,0]+cm[1,1])
print('Taux de mauvaise classification sur la classe 1 :',mv_class_mlp) #FN/(FN+TP)
res_mlp=precision_recall_fscore_support(y_test,y_pred,average='weighted')[0:3]
Fitting 10 folds for each of 54 candidates, totalling 540 fits Meilleurs paramètres : {'alpha': 0, 'hidden_layer_sizes': 100, 'learning_rate': 'constant'} Meilleur score sur le train : 0.7743799153055052 Score du MLP sur le test : 0.8177083333333334 Taux de mauvaise classification sur la classe 1 : 0.4444444444444444
On obtient avec un MLP les mêmes resultats (exactement) que pour la régression logistique. C'est très étonnant mais cela peut-être expliqué par la faible taille de notre base de données.
Les resultats du MLP en font l'un des meilleurs algorithme pour notre problème et sur notre dataset.
model_names = ["Prediction bete", "DT","KNN","Reg", "QDA", "SVM","RF","GB","LGBM","RN","MLP"]
# Accuracy :
fig = px.bar(x=model_names, y=np.array([score_bete,score_DT,score_knn,score_reg, score_qda,score_svc,score_RF,score_gb,score_LGBM,score_RN,score_mlp]), title="Comparaison score des modeles", color=model_names)
fig.show()
res_1=np.zeros((11,5))
res_1[0,0]=score_bete
res_1[0,1:4]=res_pred_bete
res_1[0,4]=mv_class_bete
res_1[1,0]=score_reg
res_1[1,1:4]=res_reg_log
res_1[1,4]=mv_class_lr
res_1[2,0]=score_qda
res_1[2,1:4]=res_qda
res_1[2,4]=mv_class_qda
res_1[3,0]=score_svc
res_1[3,1:4]=res_svm
res_1[3,4]=mv_class_svm
res_1[4,0]=score_DT
res_1[4,1:4]=res_dt
res_1[4,4]=mv_class_dt
res_1[5,0]=score_RF
res_1[5,1:4]=res_rf
res_1[5,4]=mv_class_rf
res_1[6,0]=score_knn
res_1[6,1:4]=res_knn
res_1[6,4]=mv_class_knn
res_1[7,0]=score_gb
res_1[7,1:4]=res_gb
res_1[7,4]=mv_class_gb
res_1[8,0]=score_LGBM
res_1[8,1:4]=res_lgbm
res_1[8,4]=mv_class_lgbm
res_1[9,0]=score_RN
res_1[9,1:4]=res_rn
res_1[9,4]=mv_class_rn
res_1[10,0]=score_mlp
res_1[10,1:4]=res_mlp
res_1[10,4]=mv_class_mlp
resultats=pandas.DataFrame(res_1)
resultats.insert(0,"Model",['Predicteur de base','Regression Logistique','QDA','SVM',
'Decision Tree','Random Forest','K-NN',"Gradient Boosting","LightGBM","Reseau Neuronaux","MLP"])
resultats=resultats.set_index("Model")
resultats=resultats.rename(columns = {0: 'Accuracy', 1: 'Precision', 2: 'Recall', 3: 'F1-Score',4:'Erreur sur la classe 1'})
resultats.sort_values(by=['Accuracy','Precision','Recall','F1-Score'],ascending=False)
Accuracy | Precision | Recall | F1-Score | Erreur sur la classe 1 | |
---|---|---|---|---|---|
Model | |||||
Regression Logistique | 0.817708 | 0.819896 | 0.817708 | 0.806340 | 0.444444 |
MLP | 0.817708 | 0.819896 | 0.817708 | 0.806340 | 0.444444 |
K-NN | 0.802083 | 0.822186 | 0.802083 | 0.779315 | 0.555556 |
QDA | 0.802083 | 0.799107 | 0.802083 | 0.792044 | 0.444444 |
Reseau Neuronaux | 0.802083 | 0.704918 | 0.682540 | 0.693548 | 0.317460 |
SVM | 0.796875 | 0.799946 | 0.796875 | 0.780708 | 0.507937 |
LightGBM | 0.791667 | 0.786035 | 0.791667 | 0.785331 | 0.412698 |
Random Forest | 0.786458 | 0.784725 | 0.786458 | 0.771356 | 0.507937 |
Gradient Boosting | 0.776042 | 0.776342 | 0.776042 | 0.756110 | 0.555556 |
Decision Tree | 0.723958 | 0.720331 | 0.723958 | 0.680211 | 0.730159 |
Predicteur de base | 0.671875 | 0.000000 | 0.000000 | 0.000000 | 1.000000 |
Une première chose que nous pouvons voir et qui est commune à tous nos classifieurs est que malgré les scores relativement élevés (82% au maximum), l'augmentation n'est que très faible par rapport à notre prédicteur naïf (67%). La deuxième chose commune à tous nos classifieurs est qu'ils ont tous de très mauvais resultats sur la prédiction de la classe 1 (diabétique), en effet, ils se trompent quasiment tous dans 50% des cas voir plus pour certains. Cela peut être expliqué par le deséquilibre entre les tailles de nos classes à l'interieur de nos données (imbalanced dataset à 65%/35%, presque 2:1) sans compter que beaucoup de nos individus topés 1 ont des données manquantes ce qui n'aide pas à la prédiction.
A l'aide du tableau juste au dessus contenant le score que nous avons utilisé (l'accuracy) ainsi que les autres metriques introduites plus tôt dans la partie entraînement des modèles nous pouvons conclure que la régression logistique est le classifieur qui fait le mieux, à égalité avec le MLP, parmi ceux que nous avons vu. Bien évidemment cela n'en fait pas le meilleur classifeur universel ni même le meilleur sur ce sujet mais tout simplement le meilleur au vu de ce qu'on a fait. En effet, un LightGBM mieux optimisé ou une autre méthode type XGBoost pourrait battre les résultats de la régression logistique.
Si nous devions en choisir un classifieur entre la Régression Logistique et le MLP nous choisirions la regression logistique car plus simple d'utilisation, d'explication et elle a l'avantage d'avoir moins de paramètres à optimiser.
Nous pouvons aussi remarquer que peu importe le classifieur que nous aurions choisi on aurait fait mieux que si on était resté avec le prédicteur bête malgré des faibles performances sur la classe 1 ils restent quand même meilleurs que le prédicteur qui n'essaye pas de la prédire.
Conclusion synthétique : Régression logistique : 82% de bonnes prévisions, 44% de mauvaise prédiction sur la classe 1, 5% de mauvaise prédiction sur la classe 0.
Une dernière chose intéressante que nous pouvons faire vu que nous en avons la possibilité est de comparer les resultats que nous obtenons avec ceux du lien dans la note plus bas. Tout comme nous il obtient un score maximal proche de 82% mais contrairement à nous, son meilleur classifieur est une Random Forest. Il obtient même des meilleurs résultats que nous car son score est de 0.827 et fait seulement 14 faux-négatifs (bien qu'il ait lui 26 faux-positifs), il arrive donc à correctement prédire la classe 1 au détriment d'une moins bonne prédiction sur la classe 0.
Ces resultats nous font donc penser que lors de la phase de visualisation des données, au moment de choisir ce que nous devions faire des données manquantes, l'hypothèse de changer les 0 par la moyenne des valeurs disponibles dans nos données aurait amélioré nos resultats. Peut-être pas considérablement mais suffisemment pour que cela soit impactant surtout dans un contexte médical.
NB : Je remercie la personne ayant alimenté le site https://www.analyticsvidhya.com/blog/2021/07/diabetes-prediction-with-pycaret/. Celui-ci m'a inspiré des idées de visualisation de données et essayer une hypothèse de gestion des données manquantes que je n'ai pas fait ici ce qui m'a permis d'obtenir une comparaison entre ce que j'ai fait et ce que j'aurai pu faire.
Essayons de voir si nous ne pouvons pas faire mieux sur la classe 1 snas pour autant utiliser la méthode du site plus haut.
Plusieurs méthodes peuvent être utilisées pour tenter de régler un problème d'imbalanced dataset mais l'objectif commun à toutes est de construire artificiellement une base de données avec un ratio entre les classes majoritaires et minoritaires de base plus proche de 1:1 qu'avant (ie avoir quasiment autant d'individu dans chaque classe). On a par exemple :
- L'Undersampling : qui consiste à retirer des données appartenant à la classe majoritaire. Cela réduit donc la taille de la base ce qui n'est pas recommandé lorsque nous avons peu de données (comme ici) mais peu être utilisé lorsqu'on a des dixaines de milliers de données par exemple.
- L'Oversampling : qui consiste à ajouter des copies d'individus de la classe minoritaire. Cela augmente la taille de la base et est recommandé lorsqu'on a peu de données. Nous allons utiliser cette méthode par la suite.
##Récupération des data train et data test :
data_train=pandas.read_csv('E:/Projet M2/Machine Learning (Ok)/data_train.csv')
data_test=pandas.read_csv('E:/Projet M2/Machine Learning (Ok)/data_test.csv')
X_train=data_train.get(["Pregnancies","Glucose","BloodPressure",
"SkinThickness","Insulin","BMI","DiabetesPedigreeFunction","Age"])
y_train=data_train["Outcome"]
X_test=data_test.get(["Pregnancies","Glucose","BloodPressure",
"SkinThickness","Insulin","BMI","DiabetesPedigreeFunction","Age"])
y_test=data_test["Outcome"]
#Library nécessaire :
from imblearn.over_sampling import RandomOverSampler
##Oversampling sur les données avec sampler naïf aléatoire :
ros = RandomOverSampler(random_state=0)
X_resampled, y_resampled = ros.fit_resample(X_train, y_train)
y_train=y_resampled #Pour pas s'embéter avec des renames après
##Normalisation des X_test et X_train :
ss = StandardScaler()
X_train_std = ss.fit_transform(X_resampled)
X_test_std = ss.transform(X_test)
##Repartition dans les données "complètes" resamplées :
print("Nombre de 1 dans resampled total: ",sum(y_train),"Taille total: ",len(y_train),"Ratio : ", sum(y_train)/len(y_train))
print("Nombre de 1 dans le test : ",sum(y_test),"Taille total: ",len(y_test),"Ratio : ", sum(y_test)/len(y_test))
Nombre de 1 dans resampled total: 371 Taille total: 742 Ratio : 0.5 Nombre de 1 dans le test : 63 Taille total: 192 Ratio : 0.328125
On a à peu près 1 diabétique sur 3 dans nos données train, on est donc bien dans un cas deséquillibré. Essayons la bibliothèque imblearn de sklearn et sa methode RandomOverSampler qui fait un oversampling aléatoire dans nos données. C'est-à-dire qu'elle remet exactement les mêmes individus que nous avons déjà dans notre base pour leur rajouter du poids. Le choix des individus à tirer pour être doublé est aléatoire.
https://imbalanced-learn.org/stable/over_sampling.html
Installation sur Anaconda : conda install -c conda-forge imbalanced-learn
##Naïf :
score_bete=(len(y_test)-sum(y_test))/len(y_test)
print("Score du prédicteur bête :", score_bete)
y_pred = 0*y_test
cm_bete=confusion_matrix(y_test, y_pred)
mv_class_bete=1
res_bete=[0,0,0]
##Logistic regression :
grid_lr=GridSearchCV(estimator=LogisticRegression(max_iter=5000),param_grid={'C':np.arange(0.5,30,step=0.5),'solver':['newton-cg','lbfgs','liblinear']},
scoring="accuracy",cv=10,verbose=1)
grid_lr.fit(X_train_std,y_train)
score_reg=grid_lr.score(X_test_std,y_test)
print("Score de la Regression Logistique sur le test :",score_reg)
y_pred=grid_lr.predict(X_test_std)
cm_lr=confusion_matrix(y_test, y_pred)
mv_class_lr=cm_lr[1,0]/(cm_lr[1,0]+cm_lr[1,1])
res_rl=precision_recall_fscore_support(y_test,y_pred,average='weighted')[0:3]
##Decision tree :
grid_dt=GridSearchCV(estimator=DecisionTreeClassifier(),param_grid={'max_depth':np.arange(1,50,step=1)},
scoring="accuracy",cv=5,verbose=1)
grid_dt.fit(X_train_std,y_train)
score_DT=grid_dt.score(X_test_std,y_test)
print("Score du Decision Tree sur le test :",score_DT)
y_pred=grid_dt.predict(X_test_std)
cm_dt=confusion_matrix(y_test, y_pred)
mv_class_dt=cm_dt[1,0]/(cm_dt[1,0]+cm_dt[1,1])
res_dt=precision_recall_fscore_support(y_test,y_pred,average='weighted')[0:3]
##K-nn :
grid_knn=GridSearchCV(estimator=KNeighborsClassifier(),param_grid={'n_neighbors':np.arange(2,100,step=1)},
scoring="accuracy",cv=10,verbose=1)
grid_knn.fit(X_train_std,y_train)
score_knn=grid_knn.score(X_test_std,y_test)
print("Score des K-Nearest Neighbour sur le test :",score_knn)
y_pred=grid_knn.predict(X_test_std)
cm_knn=confusion_matrix(y_test, y_pred)
mv_class_knn=cm_knn[1,0]/(cm_knn[1,0]+cm_knn[1,1])
res_knn=precision_recall_fscore_support(y_test,y_pred,average='weighted')[0:3]
##QDA :
clf_qda=QuadraticDiscriminantAnalysis().fit(X_train_std,y_train)
score_qda=clf_qda.score(X_test_std,y_test)
print("Score du QDA sur le test :", score_qda)
y_pred=clf_qda.predict(X_test_std)
cm_qda=confusion_matrix(y_test, y_pred)
mv_class_qda=cm_qda[1,0]/(cm_qda[1,0]+cm_qda[1,1])
res_qda=precision_recall_fscore_support(y_test,y_pred,average='weighted')[0:3]
##SVM :
grid_svc=GridSearchCV(estimator=SVC(),param_grid={'C':np.arange(0.5,20,step=0.5),'kernel':['rbf','sigmoid'],'gamma':['auto','scale']},
scoring="accuracy",cv=10,verbose=1,refit=True)
grid_svc.fit(X_train_std,y_train)
score_svc=grid_svc.score(X_test_std,y_test)
print("Score du SVM sur le test :",score_svc)
y_pred=grid_svc.predict(X_test_std)
cm_svm=confusion_matrix(y_test, y_pred)
mv_class_svm=cm_svm[1,0]/(cm_svm[1,0]+cm_svm[1,1])
res_svm=precision_recall_fscore_support(y_test,y_pred,average='weighted')[0:3]
##Random Forest :
grid_rf=GridSearchCV(estimator=RandomForestClassifier(),param_grid={"n_estimators":np.arange(20,100,step=1),
'max_depth':np.arange(2,15,step=1)}
,scoring="accuracy",cv=10,verbose=1)
grid_rf.fit(X_train_std,y_train)
score_RF=grid_rf.score(X_test_std,y_test)
print("Score de la Random Forest sur le test :",score_RF)
y_pred=grid_rf.predict(X_test_std)
cm_rf=confusion_matrix(y_test, y_pred)
mv_class_rf=cm_rf[1,0]/(cm_rf[1,0]+cm_rf[1,1])
res_rf=precision_recall_fscore_support(y_test,y_pred,average='weighted')[0:3]
##Gradient Boosting :
grid_gb=GridSearchCV(GradientBoostingClassifier(),param_grid={'learning_rate': [0.05, 0.1, 0.5],
'max_features': np.arange(1,10,step=1), 'max_depth': np.arange(1,5,step=1)},
cv=10, scoring='accuracy',verbose=1)
grid_gb.fit(X_train_std,y_train)
score_gb=grid_gb.score(X_test_std,y_test)
print("Score du Gradient Boosting sur le test :",score_gb)
y_pred=grid_gb.predict(X_test_std)
cm_gb=confusion_matrix(y_test, y_pred)
mv_class_gb=cm_gb[1,0]/(cm_gb[1,0]+cm_gb[1,1])
res_gb=precision_recall_fscore_support(y_test,y_pred,average='weighted')[0:3]
##LightGBM :
fit_params = {"early_stopping_rounds" : 100,
"eval_metric" : 'auc',
"eval_set" : [(X,y)],
'eval_names': ['valid'],
'verbose': 0,
'categorical_feature': 'auto'}
param_test = {'learning_rate' : np.arange(0.1,0.5,step=0.1),
'n_estimators' : np.arange(100,500,step=100),
'max_depth': [3, 4, 5]
}
#intialize lgbm and launch the search
clf_LGBM = lgbm.LGBMClassifier(random_state=50, silent=True, metric='None', n_jobs=4)
#gridsearch
grid = RandomizedSearchCV(estimator=clf_LGBM, param_distributions=param_test, n_iter=400,scoring='accuracy',cv=10,
refit=True,random_state=50,verbose=1)
grid.fit(X_train_std, y_train, **fit_params)
score_LGBM=grid.score(X_test_std,y_test)
print("Score de LGBM : ", score_LGBM)
y_pred=grid.predict(X_test_std)
cm_lgbm=confusion_matrix(y_test, y_pred)
mv_class_lgbm=cm_lgbm[1,0]/(cm_lgbm[1,0]+cm_lgbm[1,1])
res_lgbm=precision_recall_fscore_support(y_test,y_pred,average='weighted')[0:3]
##MLP :
from sklearn.neural_network import MLPClassifier
grip_mlp = GridSearchCV(MLPClassifier(max_iter=300,random_state=0,activation='logistic',solver='adam'),param_grid={
'alpha' : [0,1e-8,1e-7,1e-6,1e-5, 1e-4], #Param de regul
'learning_rate': ['constant', 'invscaling', 'adaptive'],
'hidden_layer_sizes':[100,150,200]},
cv=10, scoring='accuracy',verbose=1)
grip_mlp.fit(X_train_std,y_train)
score_mlp=grip_mlp.score(X_test_std, y_test)
print("Score de MLP : ", score_mlp)
y_pred=grip_mlp.predict(X_test_std)
cm_mlp=confusion_matrix(y_test, y_pred)
mv_class_mlp=cm_mlp[1,0]/(cm_mlp[1,0]+cm_mlp[1,1])
res_mlp=precision_recall_fscore_support(y_test,y_pred,average='weighted')[0:3]
Score du prédicteur bête : 0.671875 Fitting 10 folds for each of 177 candidates, totalling 1770 fits Score de la Regression Logistique sur le test : 0.8333333333333334 Fitting 5 folds for each of 49 candidates, totalling 245 fits Score du Decision Tree sur le test : 0.703125 Fitting 10 folds for each of 98 candidates, totalling 980 fits Score des K-Nearest Neighbour sur le test : 0.7708333333333334 Score du QDA sur le test : 0.8072916666666666 Fitting 10 folds for each of 156 candidates, totalling 1560 fits Score du SVM sur le test : 0.8125 Fitting 10 folds for each of 1040 candidates, totalling 10400 fits Score de la Random Forest sur le test : 0.7864583333333334 Fitting 10 folds for each of 108 candidates, totalling 1080 fits Score du Gradient Boosting sur le test : 0.7708333333333334 Fitting 10 folds for each of 48 candidates, totalling 480 fits Score de LGBM : 0.796875 Fitting 10 folds for each of 54 candidates, totalling 540 fits Score de MLP : 0.8229166666666666
cm_display_bete = ConfusionMatrixDisplay(cm_bete).plot()
plt.title("Prédicteur Naïf")
plt.show()
mv_class_bete=1
print('Taux de mauvaise classification sur la classe 1 :',mv_class_bete) #Se trompe tout le temps sur la classe 1.
cm_display_lr = ConfusionMatrixDisplay(cm_lr).plot()
plt.title("Logistic reg")
plt.show()
mv_class_lr=cm_lr[1,0]/(cm_lr[1,0]+cm_lr[1,1])
print('Taux de mauvaise classification sur la classe 1 :',mv_class_lr) #FN/(FN+TP)
cm_display_dt = ConfusionMatrixDisplay(cm_dt).plot()
plt.title("Decision Tree")
plt.show()
mv_class_dt=cm_dt[1,0]/(cm_dt[1,0]+cm_dt[1,1])
print('Taux de mauvaise classification sur la classe 1 :',mv_class_dt) #FN/(FN+TP)
cm_display_knn = ConfusionMatrixDisplay(cm_knn).plot()
plt.title("K-NN")
plt.show()
mv_class_knn=cm_knn[1,0]/(cm_knn[1,0]+cm_knn[1,1])
print('Taux de mauvaise classification sur la classe 1 :',mv_class_knn) #FN/(FN+TP)
cm_display_qda = ConfusionMatrixDisplay(cm_qda).plot()
plt.title("QDA")
plt.show()
mv_class_qda=cm_qda[1,0]/(cm_qda[1,0]+cm_qda[1,1])
print('Taux de mauvaise classification sur la classe 1 :',mv_class_qda) #FN/(FN+TP)
cm_display_svm = ConfusionMatrixDisplay(cm_svm).plot()
plt.title("SVM")
plt.show()
mv_class_svm=cm_svm[1,0]/(cm_svm[1,0]+cm_svm[1,1])
print('Taux de mauvaise classification sur la classe 1 :',mv_class_svm) #FN/(FN+TP)
cm_display_rf = ConfusionMatrixDisplay(cm_rf).plot()
plt.title("Random Forest")
plt.show()
mv_class_rf=cm_rf[1,0]/(cm_rf[1,0]+cm_rf[1,1])
print('Taux de mauvaise classification sur la classe 1 :',mv_class_rf) #FN/(FN+TP)
cm_display_gb = ConfusionMatrixDisplay(cm_gb).plot()
plt.title("Gradient Boosting")
plt.show()
mv_class_gb=cm_gb[1,0]/(cm_gb[1,0]+cm_gb[1,1])
print('Taux de mauvaise classification sur la classe 1 :',mv_class_gb) #FN/(FN+TP)
cm_display_lgbm = ConfusionMatrixDisplay(cm_lgbm).plot()
plt.title("LightGBM")
plt.show()
mv_class_lgbm=cm_lgbm[1,0]/(cm_lgbm[1,0]+cm_lgbm[1,1])
print('Taux de mauvaise classification sur la classe 1 :',mv_class_lgbm) #FN/(FN+TP)
cm_display_mlp = ConfusionMatrixDisplay(cm_mlp).plot()
plt.title("MLP")
plt.show()
mv_class_mlp=cm_mlp[1,0]/(cm_mlp[1,0]+cm_mlp[1,1])
print('Taux de mauvaise classification sur la classe 1 :',mv_class_mlp) #FN/(FN+TP)
Taux de mauvaise classification sur la classe 1 : 1
Taux de mauvaise classification sur la classe 1 : 0.2222222222222222
Taux de mauvaise classification sur la classe 1 : 0.4126984126984127
Taux de mauvaise classification sur la classe 1 : 0.25396825396825395
Taux de mauvaise classification sur la classe 1 : 0.38095238095238093
Taux de mauvaise classification sur la classe 1 : 0.2698412698412698
Taux de mauvaise classification sur la classe 1 : 0.3968253968253968
Taux de mauvaise classification sur la classe 1 : 0.38095238095238093
Taux de mauvaise classification sur la classe 1 : 0.31746031746031744
Taux de mauvaise classification sur la classe 1 : 0.23809523809523808
res=np.zeros((10,5))
res[0,0]=score_bete
res[0,1:4]=res_bete
res[0,4]=mv_class_bete
res[1,0]=score_reg
res[1,1:4]=res_rl
res[1,4]=mv_class_lr
res[2,0]=score_qda
res[2,1:4]=res_qda
res[2,4]=mv_class_qda
res[3,0]=score_svc
res[3,1:4]=res_svm
res[3,4]=mv_class_svm
res[4,0]=score_DT
res[4,1:4]=res_dt
res[4,4]=mv_class_dt
res[5,0]=score_RF
res[5,1:4]=res_rf
res[5,4]=mv_class_rf
res[6,0]=score_knn
res[6,1:4]=res_knn
res[6,4]=mv_class_knn
res[7,0]=score_gb
res[7,1:4]=res_gb
res[7,4]=mv_class_gb
res[8,0]=score_LGBM
res[8,1:4]=res_lgbm
res[8,4]=mv_class_lgbm
res[9,0]=score_mlp
res[9,1:4]=res_mlp
res[9,4]=mv_class_mlp
resultats=pandas.DataFrame(res)
resultats.insert(0,"Model",['Predicteur de base','Regression Logistique','QDA','SVM',
'Decision Tree','Random Forest','K-NN',"Gradient Boosting","LightGBM","MLP"])
resultats=resultats.set_index("Model")
resultats=resultats.rename(columns = {0: 'Accuracy', 1: 'Precision', 2: 'Recall', 3: 'F1-Score',4:'Erreur sur la classe 1'})
resultats.sort_values(by=['Accuracy','Precision','Recall','F1-Score'],ascending=False)
Accuracy | Precision | Recall | F1-Score | Erreur sur la classe 1 | |
---|---|---|---|---|---|
Model | |||||
Regression Logistique | 0.833333 | 0.836597 | 0.833333 | 0.834585 | 0.222222 |
MLP | 0.822917 | 0.826325 | 0.822917 | 0.824247 | 0.238095 |
SVM | 0.812500 | 0.814151 | 0.812500 | 0.813232 | 0.269841 |
QDA | 0.807292 | 0.802790 | 0.807292 | 0.802015 | 0.380952 |
LightGBM | 0.796875 | 0.796080 | 0.796875 | 0.796455 | 0.317460 |
Random Forest | 0.786458 | 0.781061 | 0.786458 | 0.781844 | 0.396825 |
K-NN | 0.770833 | 0.785620 | 0.770833 | 0.775208 | 0.253968 |
Gradient Boosting | 0.770833 | 0.767531 | 0.770833 | 0.768826 | 0.380952 |
Decision Tree | 0.703125 | 0.709537 | 0.703125 | 0.705857 | 0.412698 |
Predicteur de base | 0.671875 | 0.000000 | 0.000000 | 0.000000 | 1.000000 |
Pour débuter cette conclusion finale du projet, remarquons que nous avons bien atteint l'objectif que nous nous étions fixé au début de cette partie : réduire les erreurs faites sur la classe 1. En effet, nous passons d'environ 50% d'erreur de prédiction sur la classe 1 à environ 25% grâce à une méthode d'oversampling aléatoire !
Au niveau des scores (leur accuracy) de nos prédicteurs, ceux-ci ont tendances à augmenter par rapport à la partie sans oversampling, on passe d'un meilleur score à 81.7% à un meilleur à 83.3%. Ce n'est évidemment pas une amélioration incroyable si nous nous basons juste sur le score mais considérant notre cadre (faire le moins d'erreur possible sur la classe 1) nous améliorons de façon significative nos resultats.
En conclusion et en observant uniquement le tableau récapitulatif ci-dessus, nous arrivons à une conclusion similaire à celle faite dans la conclusion précedente : la régression logistique est le meilleur estimateur sur notre jeu de donnée, suivi de près par le MLP.
Synthétiquement : Régression logistique : 83% d'accuracy, 22% d'erreur sur la classe 1, 14% d'erreur sur la classe 0.
Remarquons que nous perdons en précision sur la prédiction de la classe 0 par rapport à si nous n'utilisions pas d'oversampling, le choix de la classe que nous voulons le mieux prédire est donc important. Voyons aussi que nous n'avons plus deux prédicteurs avec exactement les mêmes resultats comme on a pu le voir plus tôt.
Pour finir je tiens à préciser que la méthode d'oversampling utilisée (oversampling aléatoire) est loin d'être la seule qui existe ni même forcément la meilleure ou la plus adaptée à nos données. Il existe des méthodes dites SMOTE et ADASYN qui ne tire pas exactement les mêmes individus mais les bruites afin de 'simuler' des nouveaux individus de notre classe minoritaire proche de nos individus de base.
La méthode d'oversampling n'est même pas l'unique méthode pour résoudre un problème d'imbalanced dataset et on aurait pu comparer les différents résultats obtenus avec les différentes méthodes afin de choisir celle que nous convient le plus.