Prédire le diagnostic du patient
Les données proviennent de Kaggle
SVM
Objectif: Prédire le diagnostic du patient
Donnés: Les données proviennent de Kaggle
Méthologie: SVM
Voici les principaux outils que nous utilisons pour l'implémentation - Python - Pandas - Scitkit-learn
import pickle import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns from sklearn.model_selection import train_test_split, GridSearchCV from sklearn.preprocessing import LabelEncoder, StandardScaler from sklearn.svm import LinearSVC, SVC from sklearn.ensemble import RandomForestClassifier, BaggingClassifier, GradientBoostingClassifier from sklearn.neural_network import MLPClassifier from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, confusion_matrix
sns.set() sns.set_theme(style="white") # Reproductibility np.random.seed(42)
# Read data `Prostate_Cancer.csv` from my github. Original dataset come from Kaggle [https://www.kaggle.com/sajidsaifi/prostate-cancer] try: data = pd.read_csv("Prostate_Cancer.csv") except: data = pd.read_csv('https://raw.githubusercontent.com/joekakone/datasets/master/datasets/Prostate_Cancer.csv') # Show the 10 first rows data.head(10)
data.shape
(100, 10)
Le tableau contient 100 lignes et 10 colonnes. La première colonne id représente les identifiants des patient, elle ne nous sera pas utile dans notre travail, nous allons l'ignorer dans la suite. La colonnes diagnosis_result représnet quant à elle le résultat du diagnostic du patient, c'est cette valeur que nous allons prédire. Les autres colonnes décrivent l'état du patient, elles nous serviront çà prédire lle diagnostic du patient.
100
10
id
diagnosis_result
# Remove `id` column data.drop(["id"], axis=1, inplace=True)
data.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 100 entries, 0 to 99 Data columns (total 9 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 diagnosis_result 100 non-null object 1 radius 100 non-null int64 2 texture 100 non-null int64 3 perimeter 100 non-null int64 4 area 100 non-null int64 5 smoothness 100 non-null float64 6 compactness 100 non-null float64 7 symmetry 100 non-null float64 8 fractal_dimension 100 non-null float64 dtypes: float64(4), int64(4), object(1) memory usage: 7.2+ KB
np.sum(data.isna())
diagnosis_result 0 radius 0 texture 0 perimeter 0 area 0 smoothness 0 compactness 0 symmetry 0 fractal_dimension 0 dtype: int64
Dans notre tableau, il n'y a pas de données manquantes. Généralement ce n'est pas le cas et il faudra corriger cela.
diagnosis_result = data.diagnosis_result.value_counts() diagnosis_result
M 62 B 38 Name: diagnosis_result, dtype: int64
plt.figure(figsize=(8, 6)) diagnosis_result.plot(kind='bar') plt.title("Distribution des résulats de diagnostic") plt.show()
plt.figure(figsize=(8, 6)) sns.boxplot(data=data, orient="h") plt.title("Distribution des variables explicatives") plt.show()
vars = data.groupby(by="diagnosis_result").mean() vars
On constate une grande variation de perimeter et area en fonction de diagnosis_result
perimeter
area
fig = plt.figure(figsize=(16, 6)) fig.add_subplot(1, 2, 1) sns.distplot(x=data[data["diagnosis_result"]=="M"]["perimeter"]) sns.distplot(x=data[data["diagnosis_result"]=="B"]["perimeter"]) plt.title("perimeter") fig.add_subplot(1, 2, 2) sns.distplot(x=data[data["diagnosis_result"]=="M"]["area"]) sns.distplot(x=data[data["diagnosis_result"]=="B"]["area"]) plt.yticks([]) plt.title("area") plt.show()
/usr/local/lib/python3.7/dist-packages/seaborn/distributions.py:2557: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms). warnings.warn(msg, FutureWarning) /usr/local/lib/python3.7/dist-packages/seaborn/distributions.py:2557: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms). warnings.warn(msg, FutureWarning) /usr/local/lib/python3.7/dist-packages/seaborn/distributions.py:2557: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms). warnings.warn(msg, FutureWarning) /usr/local/lib/python3.7/dist-packages/seaborn/distributions.py:2557: FutureWarning: `distplot` is a deprecated function and will be removed in a future version. Please adapt your code to use either `displot` (a figure-level function with similar flexibility) or `histplot` (an axes-level function for histograms). warnings.warn(msg, FutureWarning)
Reagrdons ce qu'il en est des correlations éventuelles entre les variables explicatives.
plt.figure(figsize=(10, 8)) corr = data.drop(["diagnosis_result"], axis=1).corr() sns.heatmap(corr, annot=True) plt.title("Matrice de corrélation") plt.show()
sns.pairplot(data=data, hue="diagnosis_result") plt.show()
Regardons ce qu'il en ait des corrélations entre les variables explicatives.
Les variables perimeter et area sont fortement correlées ce qui n'est pas étonnant, le périmètre et la surface.
Dans la suite, on va convertir les valeurs de diagnosis_result par des nombres
dt = data.copy() encoder = LabelEncoder() encoder.fit(dt["diagnosis_result"]) dt["target"] = encoder.transform(dt["diagnosis_result"]) dt.head()
M est encodé en 1 et B en 0
plt.figure(figsize=(8, 6)) sns.scatterplot(x="area", y="smoothness", hue="target", data=dt) plt.title("area × smoothness") plt.show()
# Variables/Données X = dt.drop(["diagnosis_result", "target"], axis=1) n_features = X.shape[-1] # Étiquettes y = dt["target"] X.head()
X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, test_size=0.3, shuffle=True, random_state=42)
Le jeu de données a été séparé en deux parties, 80% serviront à l'entraînement et les 20% restants pour l'évaluation. Le soin a été pris de spécifier que l'échantionnalge doit conserver la même distribution suivant le diagnostic.
80%
20%
Les variables n'ont pas la même échelle de grandeur et il faut corriger cela. Si l'algorithme utilisé est juste ie s'il ne s'agit pas d'un algorithme dont l'apprentissage est itératif, on peut bien garder les valeurs comme telles. Mais dans notre exemple ici, nous allons raner toutes les variable à la même échelle de gradeur.
scaler = StandardScaler() scaler.fit(X_train) X_train_scaled = scaler.transform(X_train) X_train_scaled = pd.DataFrame(X_train_scaled, columns=list(X_train.columns))
fig = plt.figure(figsize=(16, 6)) fig.add_subplot(1, 2, 1) sns.boxplot(data=X_train, orient="h") plt.title("Distribution des variables explicatives") fig.add_subplot(1, 2, 2) sns.boxplot(data=X_train_scaled, orient="h") plt.yticks([]) plt.title("Distribution des variables explicatives\naprès normalisation") plt.show()
On applique la même opération à l'échantillon de test. Mais attention, on utilise les valeurs calculées sur l'échatiollon d'entraînement
X_test_scaled = scaler.transform(X_test) X_test_scaled = pd.DataFrame(X_test_scaled, columns=list(X_test.columns))
Il n'existe pas de modèle parfait capable de résoudre tous les problèmes. En général, il faut essayer plsuieurs et sélectionner le plus performant.
plt.figure(figsize=(8, 6)) sns.scatterplot(x=X_train["area"], y=X_train["smoothness"], hue=y_train) plt.title("area × smoothness") plt.show()
Nous allons trouver une droite pour séparer les points. Il s'agit de la droite qui sépare le mieux.
# def evaluate(model, X_test_, y_test_): # Accuracy y_pred = model.predict(X_test_) print("Accuracy Score: {:2.2%}".format(accuracy_score(y_test_, y_pred))) print("Precision Score: {:2.2%}".format(precision_score(y_test_, y_pred))) print("Recall Score: {:2.2%}".format(recall_score(y_test_, y_pred))) print("F1 Score: {:2.2%}".format(f1_score(y_test_, y_pred))) # Matrice de confusion confmat = confusion_matrix(y_test_, y_pred) sns.heatmap(confmat, annot=True, cbar=False) plt.ylabel("Bonne étiquette") plt.xlabel("Étiquette prédite") plt.title("Matrice de confusion") plt.show()
smoothness
svm = LinearSVC(C=0.8) X_train_2 = X_train[["area", "smoothness"]] svm.fit(X_train_2, y_train) print("Accuracy Score: {:2.2%}".format(svm.score(X_train_2, y_train)))
Accuracy Score: 61.43%
/usr/local/lib/python3.7/dist-packages/sklearn/svm/_base.py:947: ConvergenceWarning: Liblinear failed to converge, increase the number of iterations. "the number of iterations.", ConvergenceWarning)
Le nombre total d'itérations a été atteint sans que l'algorithme ne converge, cela est dû à la dfférence d'echelle entre les variables.
evaluate(svm, X_test[["area", "smoothness"]], y_test)
Accuracy Score: 63.33% Precision Score: 63.33% Recall Score: 100.00% F1 Score: 77.55%
# Genrate random values x_1 = np.random.uniform(X_train_2["area"].min(), X_train_2["area"].max(), 5000) x_2 = np.random.uniform(X_train_2["smoothness"].min(), X_train_2["smoothness"].max(), 5000) y_ = svm.predict(np.column_stack([x_1, x_2])) plt.figure(figsize=(14, 8)) sns.scatterplot(x=x_1, y=x_2, alpha=0.2, hue=y_) y_test_ = [int(i) for i in y_test] # astuce sns.scatterplot(x=X_test["area"], y=X_test["smoothness"], hue=y_test_) plt.legend(loc="best") plt.show()
Les performances sont assez modestes, voyons ce que ça donne avec les données centralisées.
svm = SVC(kernel="linear", C=0.8) X_train_2 = X_train_scaled[["area", "smoothness"]] svm.fit(X_train_2, y_train) print("Accuracy Score: {:2.2%}".format(svm.score(X_train_2, y_train)))
Accuracy Score: 84.29%
evaluate(svm, X_test_scaled[["area", "smoothness"]], y_test)
Accuracy Score: 86.67% Precision Score: 94.12% Recall Score: 84.21% F1 Score: 88.89%
Comme on peut le voir, la normalisation donne un meilleur résultat. En effet, la vitesse de convergence a largement augmenté.
# Genrate random values x_1 = np.random.uniform(X_train_2["area"].min(), X_train_2["area"].max(), 5000) x_2 = np.random.uniform(X_train_2["smoothness"].min(), X_train_2["smoothness"].max(), 5000) y_ = svm.predict(np.column_stack([x_1, x_2])) plt.figure(figsize=(14, 8)) sns.scatterplot(x=x_1, y=x_2, alpha=0.2, hue=y_) y_test_ = [int(i) for i in y_test] # astuce sns.scatterplot(x=X_test_scaled["area"], y=X_test_scaled["smoothness"], hue=y_test_) plt.legend(loc="best") plt.show()
On peut voir que certains points sont mal classés. En effet une droite ne peut pas séparer correctement l'ensemble des points
svm = SVC(kernel="sigmoid", C=0.8) X_train_2 = X_train_scaled[["area", "smoothness"]] svm.fit(X_train_2, y_train) print("Accuracy Score: {:2.2%}".format(svm.score(X_train_2, y_train)))
Accuracy Score: 81.43%
Accuracy Score: 83.33% Precision Score: 88.89% Recall Score: 84.21% F1 Score: 86.49%
Dans notre cas ici, l'utilisation d'un noyau n'a pas abouti à une amélioration significative, par contre une courbe pour séparer les données est bien plus appropriée que la droite.
svm = SVC(kernel="sigmoid", C=0.8) svm.fit(X_train_scaled, y_train) svm.score(X_train_scaled, y_train) print("Accuracy Score: {:2.2%}".format(svm.score(X_train_scaled, y_train)))
Accuracy Score: 87.14%
evaluate(svm, X_test_scaled, y_test)
Accuracy Score: 86.67% Precision Score: 89.47% Recall Score: 89.47% F1 Score: 89.47%
svm = SVC() param_grid = { "kernel": ["rbf", "linear", "poly", "sigmoid"], "C": [0.75, 0.8, 0.9, 1.0], "degree": [2, 3, 4, 5,], "gamma": ["scale", "auto"], } grid = GridSearchCV(svm, param_grid=param_grid, cv=5, n_jobs=-1, verbose=1) grid.fit(X_train_scaled, y_train) model = grid.best_estimator_ print(model) evaluate(model, X_test_scaled, y_test)
Fitting 5 folds for each of 128 candidates, totalling 640 fits
[Parallel(n_jobs=-1)]: Using backend LokyBackend with 2 concurrent workers. [Parallel(n_jobs=-1)]: Done 584 tasks | elapsed: 2.9s
SVC(C=1.0, break_ties=False, cache_size=200, class_weight=None, coef0=0.0, decision_function_shape='ovr', degree=2, gamma='auto', kernel='sigmoid', max_iter=-1, probability=False, random_state=None, shrinking=True, tol=0.001, verbose=False) Accuracy Score: 80.00% Precision Score: 88.24% Recall Score: 78.95% F1 Score: 83.33%
[Parallel(n_jobs=-1)]: Done 640 out of 640 | elapsed: 3.1s finished
model = SVC(degree=2, gamma='auto', kernel='sigmoid') model.fit(X_train_scaled, y_train) print("Train Accuracy Score: {:2.2%}".format(model.score(X_train_scaled, y_train))) evaluate(model, X_test_scaled, y_test)
Train Accuracy Score: 85.71% Accuracy Score: 80.00% Precision Score: 88.24% Recall Score: 78.95% F1 Score: 83.33%
with open("encoder.pkl", "wb") as f: pickle.dump(encoder, f) with open("scaler.pkl", "wb") as f: pickle.dump(scaler, f) with open("model.pkl", "wb") as f: pickle.dump(model, f)
Testez le modèle en production ici https://prostate-cancer-diagnosis-jk.herokuapp.com