Aller au contenu

Open In Colab

Webinar Workflow ML

Objectif: Prédire le diagnostic du patient

Donnés: Les données proviennent de Kaggle

Méthologie: SVM

Implémentation

  1. Importation des données
  2. Exploration
  3. Conversion des variables catégorielles en numérique
  4. Séparation du jeu de données
  5. Entraînement
  6. Sélection de modèle

Voici les principaux outils que nous utilisons pour l'implémentation - Python - Pandas - Scitkit-learn

Librairies

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)

Importation des données

# 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)
id diagnosis_result radius texture perimeter area smoothness compactness symmetry fractal_dimension
0 1 M 23 12 151 954 0.143 0.278 0.242 0.079
1 2 B 9 13 133 1326 0.143 0.079 0.181 0.057
2 3 M 21 27 130 1203 0.125 0.160 0.207 0.060
3 4 M 14 16 78 386 0.070 0.284 0.260 0.097
4 5 M 9 19 135 1297 0.141 0.133 0.181 0.059
5 6 B 25 25 83 477 0.128 0.170 0.209 0.076
6 7 M 16 26 120 1040 0.095 0.109 0.179 0.057
7 8 M 15 18 90 578 0.119 0.165 0.220 0.075
8 9 M 19 24 88 520 0.127 0.193 0.235 0.074
9 10 M 25 11 84 476 0.119 0.240 0.203 0.082
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.

# Remove `id` column
data.drop(["id"], axis=1, inplace=True)

Nettoyage

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.

Exploration

Distribution de la variable objectif

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()
No description has been provided for this image

Distribution des variables explicatives

plt.figure(figsize=(8, 6))
sns.boxplot(data=data, orient="h")
plt.title("Distribution des variables explicatives")
plt.show()
No description has been provided for this image

Distribution des variables explicatives par la variable objectif

vars = data.groupby(by="diagnosis_result").mean()
vars
radius texture perimeter area smoothness compactness symmetry fractal_dimension
diagnosis_result
B 17.947368 17.763158 78.500000 474.342105 0.099053 0.086895 0.184053 0.064605
M 16.177419 18.516129 107.983871 842.951613 0.104984 0.151097 0.198758 0.064742

On constate une grande variation de perimeter et area en fonction de diagnosis_result

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)

No description has been provided for this image

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()
No description has been provided for this image
sns.pairplot(data=data, hue="diagnosis_result")
plt.show()
No description has been provided for this image

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.

Conversion en numérique

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()
diagnosis_result radius texture perimeter area smoothness compactness symmetry fractal_dimension target
0 M 23 12 151 954 0.143 0.278 0.242 0.079 1
1 B 9 13 133 1326 0.143 0.079 0.181 0.057 0
2 M 21 27 130 1203 0.125 0.160 0.207 0.060 1
3 M 14 16 78 386 0.070 0.284 0.260 0.097 1
4 M 9 19 135 1297 0.141 0.133 0.181 0.059 1

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()
No description has been provided for this image

Échantillonnage

# Variables/Données
X = dt.drop(["diagnosis_result", "target"], axis=1)
n_features = X.shape[-1]
# Étiquettes
y = dt["target"]
X.head()
radius texture perimeter area smoothness compactness symmetry fractal_dimension
0 23 12 151 954 0.143 0.278 0.242 0.079
1 9 13 133 1326 0.143 0.079 0.181 0.057
2 21 27 130 1203 0.125 0.160 0.207 0.060
3 14 16 78 386 0.070 0.284 0.260 0.097
4 9 19 135 1297 0.141 0.133 0.181 0.059
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.

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.

Centrage et Réduction

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()
No description has been provided for this image

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

Modélisation

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()
No description has been provided for this image

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()

Séparatrices à Vastes Marges

1. Avec deux variables area et 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%

No description has been provided for this image
# 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()
No description has been provided for this image

Les performances sont assez modestes, voyons ce que ça donne avec les données centralisées.

2. Avec deux variables area et smoothness normalisé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%

No description has been provided for this image

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()
No description has been provided for this image

On peut voir que certains points sont mal classés. En effet une droite ne peut pas séparer correctement l'ensemble des points

3. Avec un noyau non linéaire

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%

evaluate(svm, X_test_scaled[["area", "smoothness"]], y_test)
Accuracy Score: 83.33%
Precision Score: 88.89%
Recall Score: 84.21%
F1 Score: 86.49%

No description has been provided for this image
# 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()
No description has been provided for this image

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.

4. Avec l'ensemble des huit variables

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%

No description has been provided for this image

Optimisation des hyperparamètres

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

No description has been provided for this image

Sélection de modèle

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%

No description has been provided for this image

Exportation

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

Pistes d'amélioration

  • Éliminer les variables les moins pertinentes
  • Essayer des approches ensemblistes
  • Plus de données, il n'y a que 100 données dans l'exemple
  • ...