Introduction au machine learning avec un modèle de prédiction d’abandon de souscription

vous avez entendu parler de machine learning partout, mais avez vous vu comment cela fonctionnait? Nous allons voir avec un exemple comment ça marcce à quoi ça sert et vous comprendrez à la fin pourquoi il est très intéressant d’en faire dans les entreprises.

Le contexte : avec des données d’abandon de souscription à un forfait mobile (churn), nous allons établir un modèle mathématique pour prédire les futur churn. L’intérêt de faire confiance au machine learning est qu’il y a un grand nombre de paramètres qui influence le fait qu’un client part à la concurrence, il est quasi impossible de faire le diagnostic à la main pour savoir si le client va partir ou non, plus exactement il y a 18 paramètres.

Le principe du machine learning est de prendre ces données, les travailler et les faire passer dans un algorithme qui va générer un modèle mathématique qui servira aux futures données, pour prédire si le client va churner ou non. En gros se servir du passé pour prédire le futur.

Le dataset à télécharger

Allez sur Kaggle et téléchargez le dataset (il vous faudra créer un compte). On va explorer ce dataset, mais ce qu’il faut savoir c’est que dans ce dataset, 30% des lignes concernent les clients ayant churné, et 70% sont des clients qui n’ont pas churné. On n’a pas une distribution 50/50. En machine learning, entrainer sur ce dataset va produire un modèle mathématique qui va prédire de façon biaisé, quelquesoit l’entrée donnée, les probabilités que la prédiction soit en faveur d’un non churn sera anormalement haute.

Pour éviter ce déséquilibre dans les données (et ce déséquilibre importe car le type d’issue d’une prédiction est unique : le churn), on va créer artificiellement (oui artificiellement) des données pour compenser ce déséquilibre, donc on va générer des données ayant pour issue le churn, afin d’avoir un rapport 50/50 (churn/non-churn)

Installation des paquets

Pour faire du traitement de machine learning, il vous faudra installer les paquets suivants : numpy, pandas, matplotlib, seaborn,sklearn

Note: lors de l’installation de sklearn, vous aurez une erreur de paquet déprécié, en fait dans l’import de bibliothèque, c’est bien sklearn qu’il faut importer, mais lors que vous installez le paquet avec pip, c’est scikit-learn qu’il faut installer ! (Lien pour en savoir plus)

Utiliser l’éditeur de code Jupyter Notebook

Jupyter noteboopk est un éditeur de code progressif, très adapté à des non codeur pur et dur. Pour les datascientistes, c’est très pertinent car ils peuvent voir comment évolue leur traitement de données. Et pour tout le monde, c’est très visuel lors de l’apprentissage du machine learning, même pour un codeur pur et dur comme moi.

Vous pouvez décomposer le code en étape et refaire exécuter une étape à tout moment, pratique si vous vous trompez dans votre code et qu’il y a une erreur.

Pour installer Jupyter Notebook, faites la commande

# installation de jupyterlab
pip install jupyterlab
pour les macs
pip3 install jupyterlab


pour démarrer
jupyter lab



pip install notebook
ou pour les mac
pip3 install notebook

puis démarrer en faisant :
jupyter notebook

Note sur Jupyterlab : jupyterlab est la version moderne de jupyter notebook, et possède un système d eplugin plus simple à utiliser et un terminal et un éditeur de texte.

Commençons à explorer les caractéristiques de notre jeu de données

Notre objectif est d’entrainer un modèle sur le jeu de données, pour ensuite prédire la probabilité d’un client à passer à la concurrencer (churner). customerChurn.csv est le fichier de chez Kaggle.

import pandas as pd

df = pd.read_csv('customerChurn.csv')
df.head()

La commande df.head() affiche les 5 premières ligne du dataframe. Le dataframe est la version de Pandas d’un fichier Excel en gros.

Nous avons un fichier avec des colonnes, qu’on appelle de feature, puisque chaque colonne est une caractéristique d’un client. Pour voir le nom et le type de la donnée stockée dans chaque colonne ainsi que le nombre de colonnes, on fait

df.info()

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7043 entries, 0 to 7042
Data columns (total 21 columns):
 #   Column            Non-Null Count  Dtype  
---  ------            --------------  -----  
 0   customerID        7043 non-null   object 
 1   gender            7043 non-null   object 
 2   SeniorCitizen     7043 non-null   int64  
 3   Partner           7043 non-null   object 
 4   Dependents        7043 non-null   object 
 5   tenure            7043 non-null   int64  
 6   PhoneService      7043 non-null   object 
 7   MultipleLines     7043 non-null   object 
 8   InternetService   7043 non-null   object 
 9   OnlineSecurity    7043 non-null   object 
 10  OnlineBackup      7043 non-null   object 
 11  DeviceProtection  7043 non-null   object 
 12  TechSupport       7043 non-null   object 
 13  StreamingTV       7043 non-null   object 
 14  StreamingMovies   7043 non-null   object 
 15  Contract          7043 non-null   object 
 16  PaperlessBilling  7043 non-null   object 
 17  PaymentMethod     7043 non-null   object 
 18  MonthlyCharges    7043 non-null   float64
 19  TotalCharges      7043 non-null   object 
 20  Churn             7043 non-null   object 
dtypes: float64(1), int64(2), object(18)
memory usage: 1.1+ MB

Et pour savoir combien il y a d’enregistrements on fait:

df["Churn"].value_counts()

Churn
No     5174
Yes    1869
Name: count, dtype: int64

donc on a 19 colonnes utile et une colonne inutile d’un point de vue machine learning, c’est la colonne customerID, elle ne comporte aucune information métier. La dernière colonne appelée Churn, prend deux valeuer, elle indique si le client est parti chez le concurrent ou non. Elle prend les valeur No ou Yes. Pour afficher le compte on fait:

df["Churn"].values_count()

Churn
No     5174
Yes    1869
Name: count, dtype: int64

il y a 1869 personnes partie, on note ici que le nombre de Yes et de No est différent dans un rapport 1 à 3 environ. Cela on le verra plus tard aura son importance dans l'entrainement du modèle. Il y a inégalité dans les 2 camps, cette disparité va avoir une incidence sur la qualité du modèle entrainé. En effet, le modèle résultant va être biaisé avec le No, car ce dernier est majoritaire. On devra équilibrer artificiellement ce dataset pour avoir un bon modèle de prédiction.

Analyse exploratoire des données

Maintenant explorons les relations entre les différentes catégorie et la colonne Churn

import matplotlib.pyplot as plt
import seaborn as sns 
import numpy as np

cols = ['gender','SeniorCitizen',"Partner","Dependents"]
numerical = cols

plt.figure(figsize=(20,4))

for i, col in enumerate(numerical):
    ax = plt.subplot(1, len(numerical), i+1)
    unique_vals = df[col].nunique()
    sns.countplot(x=col, hue=col, data=df, legend=False, 
                  palette=sns.color_palette("husl", df[col].nunique()))
    #sns.countplot(x=str(col), data=df, palette=sns.color_palette("husl", unique_vals))
    ax.set_title(f"{col}")

L’idée est ici de graphiquement regarder pour chaque colonne la distribution des valeurs. Ici on voit la colonne Genre, SeniorCitizen, Partner, Dependents, des varaible catégorielle relative à la démographie.

On voit qu’il y a autant d’homme que de femmes, mais que les seniors sont minoritaires, qu’il y a autant de célibataires que de mariés, et il y a 1 tier de la population qui est dépendantes.

Maintenant regardons la relation entre le MonthlyCharges et le Churn, on peut se dire que plus le forfait est élevé plus les gens ont tendance à partir

sns.boxplot(x='Churn', y='MonthlyCharges', hue='Churn', data=df, palette='Set2', dodge=False, legend=False)

Effectivement c’est ce que nous constatons, les Yes sont entre 50 et 100 dollars / mois.

Pour comprendre le boxplot je vous joins ce petit schéma pris sur datatab.net

Maintenant analysons la relation entre quelques autres variable catégorielle avec le Churn

cols = ['InternetService',"TechSupport","OnlineBackup","Contract"]

plt.figure(figsize=(14,4))

for i, col in enumerate(cols):
    ax = plt.subplot(1, len(cols), i+1)
    sns.countplot(x ="Churn", hue = str(col), data = df)
    ax.set_title(f"{col}")

Cette fois-ci les catégories ont plusieurs valeurs (3). On peut voir que pour InternetService, donc le forfait box de l’abonnement, les churners n’ont pas de forfait box ou très peu, idem pour le service ADSL, par contre ils sont nombreux à être sur fibre optique. Concernant les non churners, ils sont plus équitablement distribué.

InternetService : Il ressort clairement du graphique ci-dessus que les clients utilisant une connexion Internet par fibre optique résilient leur abonnement plus souvent que les autres. Cela pourrait s’expliquer par le fait que la fibre est un service plus coûteux, ou bien que ce fournisseur n’offre pas une bonne couverture.

TechSupport : De nombreux utilisateurs ayant résilié leur abonnement ne s’étaient pas inscrits au service d’assistance technique. Cela pourrait signifier qu’ils n’ont reçu aucune aide pour résoudre leurs problèmes techniques et ont décidé d’arrêter d’utiliser le service.

OnlineBackup : Beaucoup de clients ayant résilié leur abonnement ne s’étaient pas inscrits au service de sauvegarde en ligne pour le stockage de données.

Contract : Les utilisateurs qui ont résilié leur abonnement étaient presque toujours sous contrat mensuel. Cela semble logique, car ces clients paient au mois et peuvent facilement annuler leur abonnement avant le prochain cycle de paiement.

Même sans construire un modèle d’apprentissage automatique sophistiqué, une simple analyse basée sur les données comme celle-ci peut aider les organisations à comprendre pourquoi elles perdent des clients et ce qu’elles peuvent faire pour y remédier.

Par exemple, si l’entreprise se rend compte que la majorité des clients qui résilient leur abonnement ne se sont pas inscrits au service d’assistance technique, elle pourrait inclure ce service gratuitement dans certaines de ses futures offres afin d’éviter que d’autres clients ne partent.

Préparation des données pour la modélisation

Rappel
df.info()

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7043 entries, 0 to 7042
Data columns (total 21 columns):
 #   Column            Non-Null Count  Dtype  
---  ------            --------------  -----  
 0   customerID        7043 non-null   object 
 1   gender            7043 non-null   object 
 2   SeniorCitizen     7043 non-null   int64  
 3   Partner           7043 non-null   object 
 4   Dependents        7043 non-null   object 
 5   tenure            7043 non-null   int64  
 6   PhoneService      7043 non-null   object 
 7   MultipleLines     7043 non-null   object 
 8   InternetService   7043 non-null   object 
 9   OnlineSecurity    7043 non-null   object 
 10  OnlineBackup      7043 non-null   object 
 11  DeviceProtection  7043 non-null   object 
 12  TechSupport       7043 non-null   object 
 13  StreamingTV       7043 non-null   object 
 14  StreamingMovies   7043 non-null   object 
 15  Contract          7043 non-null   object 
 16  PaperlessBilling  7043 non-null   object 
 17  PaymentMethod     7043 non-null   object 
 18  MonthlyCharges    7043 non-null   float64
 19  TotalCharges      7043 non-null   object 
 20  Churn             7043 non-null   object 
dtypes: float64(1), int64(2), object(18)
memory usage: 1.1+ MB

Beaucoup de données sont de type Object, des valeurs non numériques, « TotalCharges » qui est un montant est un objet par exemple, on va le convertir en valeur numérique

df['TotalCharges'] = df['TotalCharges'].apply(lambda x: pd.to_numeric(x, errors='coerce')).dropna()

Encodage des variables catégorielles

Les valeurs catégorielles (non numérique) doivent être convertie en valeur numérique pour être modélisée, des colonnes avec pour valeur Yes/No, on peut rendre le Yes en 1 et le No en 0. Avec Sckitlearn on va pouvoir transformer ces valeurs en nombres. Si on regarde le dataframe sans les valeur numériques

cat_features = df.drop(['customerID','TotalCharges','MonthlyCharges','SeniorCitizen','tenure'],axis=1)

cat_features.head()

La commande ci-dessus nous permet de voir les valeurs catégorielles des features. Transformaons les en valuer numérique avec ScikitLearn

from sklearn import preprocessing

le = preprocessing.LabelEncoder()
df_cat = cat_features.apply(le.fit_transform)
df_cat.head(1)

Scikitlearn convertit automatiquement les valeurs catégorielle en valeur numérique. Il n’y a pas de règle particulière, la valeur 0 correspond à la première valeur catégorielle ordonnées dans l’ordre alphabétique. No est avant Yes donc No vaut zéro et Yes vaut 1.

Regardons maintenant les types

df_cat.info()

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7043 entries, 0 to 7042
Data columns (total 16 columns):
 #   Column            Non-Null Count  Dtype
---  ------            --------------  -----
 0   gender            7043 non-null   int64
 1   Partner           7043 non-null   int64
 2   Dependents        7043 non-null   int64
 3   PhoneService      7043 non-null   int64
 4   MultipleLines     7043 non-null   int64
 5   InternetService   7043 non-null   int64
 6   OnlineSecurity    7043 non-null   int64
 7   OnlineBackup      7043 non-null   int64
 8   DeviceProtection  7043 non-null   int64
 9   TechSupport       7043 non-null   int64
 10  StreamingTV       7043 non-null   int64
 11  StreamingMovies   7043 non-null   int64
 12  Contract          7043 non-null   int64
 13  PaperlessBilling  7043 non-null   int64
 14  PaymentMethod     7043 non-null   int64
 15  Churn             7043 non-null   int64
dtypes: int64(16)
memory usage: 880.5 KB

On va maintenant fusionner avec le dataframe qui était déjà numérique

num_features = df[['customerID','TotalCharges','MonthlyCharges','SeniorCitizen','tenure']]
finaldf = pd.merge(num_features, df_cat, left_index=True, right_index=True)

Oversampling

On avait dit que le Churn Yes était minoritaire dans les données, alors qu’il devait être à 50/50 avec le churn No.

Comme mentionné précédemment, le jeu de données est déséquilibré, ce qui signifie que la majorité des valeurs de la variable cible appartiennent à une seule classe. La plupart des clients du jeu de données n’ont pas résilié leur abonnement — seulement 27 % l’ont fait.

Ce problème de déséquilibre des classes peut entraîner de mauvaises performances d’un modèle d’apprentissage automatique. Certains algorithmes, lorsqu’ils sont entraînés sur un jeu de données déséquilibré, finissent toujours par prédire la classe majoritaire. Dans notre cas, par exemple, le modèle pourrait prédire qu’aucun client n’a résilié. Bien qu’un tel modèle semble très précis (il serait correct dans 73 % des cas), il ne nous est d’aucune utilité puisqu’il prédit toujours le même résultat.

Il existe différentes techniques pour résoudre le problème de déséquilibre des classes en apprentissage automatique. Dans ce tutoriel, nous allons utiliser une technique appelée suréchantillonnage (oversampling). Ce processus consiste à sélectionner aléatoirement des échantillons de la classe minoritaire et à les ajouter au jeu de données d’entraînement. Nous allons suréchantillonner la classe minoritaire jusqu’à ce que le nombre d’exemples soit égal à celui de la classe majoritaire.

Avant de procéder au suréchantillonnage, effectuons une séparation entre le jeu d’entraînement et le jeu de test. Nous appliquerons le suréchantillonnage uniquement sur le jeu d’entraînement, car le jeu de test doit rester représentatif de la population réelle.

On va constituer un dataset pour le training du modèle, et un dataset de test qu’on va soumettre qu modèle entrainé.

from sklearn.model_selection import train_test_split

finaldf = finaldf.dropna()
finaldf = finaldf.drop(['customerID'],axis=1)

X = finaldf.drop(['Churn'],axis=1)
y = finaldf['Churn']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33, random_state=42)

Chaque dataset est en deux parties X (majuscule) et y (minuscule), y représente la dernière colonne qui est le churn. Ainsi Dans le dataset de training, on a X_train et y_train. Et pour le dataset de test on a X_test et y_test.

#y_train
X_test.info()

<class 'pandas.core.frame.DataFrame'>
Index: 2321 entries, 2481 to 5585
Data columns (total 19 columns):
 #   Column            Non-Null Count  Dtype  
---  ------            --------------  -----  
 0   TotalCharges      2321 non-null   float64
 1   MonthlyCharges    2321 non-null   float64
 2   SeniorCitizen     2321 non-null   int64  
 3   tenure            2321 non-null   int64  
 4   gender            2321 non-null   int64  
 5   Partner           2321 non-null   int64  
 6   Dependents        2321 non-null   int64  
 7   PhoneService      2321 non-null   int64  
 8   MultipleLines     2321 non-null   int64  
 9   InternetService   2321 non-null   int64  
 10  OnlineSecurity    2321 non-null   int64  
 11  OnlineBackup      2321 non-null   int64  
 12  DeviceProtection  2321 non-null   int64  
 13  TechSupport       2321 non-null   int64  
 14  StreamingTV       2321 non-null   int64  
 15  StreamingMovies   2321 non-null   int64  
 16  Contract          2321 non-null   int64  
 17  PaperlessBilling  2321 non-null   int64  
 18  PaymentMethod     2321 non-null   int64  
dtypes: float64(2), int64(17)
memory usage: 362.7 KB

C’est avec la librairie imblearn qu’on va construire le suréchantillon sur les données de training

from imblearn.over_sampling import SMOTE

oversample = SMOTE(k_neighbors=5)
X_smote, y_smote = oversample.fit_resample(X_train, y_train)
X_train, y_train = X_smote, y_smote

Ensuite consmptez le nombre de rangées : 

y_train.value_counts()
Churn
0    3452
1    3452
Name: count, dtype: int64

On est à 50/50

Construction du modèle de prédiction du churn

Maintenant la partie vraiment importante ! On a passé beaucoup de temps pour nettoyer, changé la valeur catégorielle en valeur numérique, rééquilibré les population, maintenant on va entrainer le modèle !

from sklearn.ensemble import RandomForestClassifier

rf = RandomForestClassifier(random_state=46)
rf.fit(X_train,y_train)

Evaluation du modèle

On va évaluer l aprécision du modèle avec les données de test (dont on connait l’issue car ce sont les données d’origine)

#customer churn model evaluation
from sklearn.metrics import accuracy_score

preds = rf.predict(X_test)
print(accuracy_score(preds,y_test))


0.7733735458853942

On a une précision de 77%

Sauvegarder le modèle

On en va pas à chaque fois réentrainer le modèle quand on va faire une prédiction, donc on va le sauvegarder pour pouvoir le réutiliser.

#save the model
import joblib
# After training your model  (format Pickle)
joblib.dump(rf, 'random_forest_model.pkl')


Sauvergarder les données de test et de training

#save training and testing data to file
import pickle

# Save X data
with open('X_train.pkl', 'wb') as f:
    pickle.dump(X_train, f)
with open('X_test.pkl', 'wb') as f:
    pickle.dump(X_test, f)

# Save y data
with open('y_train.pkl', 'wb') as f:
    pickle.dump(y_train, f)
with open('y_test.pkl', 'wb') as f:
    pickle.dump(y_test, f)

Chargement du modèle et des données sauvegardées

On va ouvrir un autre fichier Notebook vierge et on va juste charge le modèle et les données pour passer au crible. En effet le modèle que vous avez calculé va être réutilisé, et il n’est pas question de le calculer à chaque fois que vous avez besoin de scorer la probabilité d’un client de patir.

Chargement du modèle

import joblib
#load the model
loaded_rf = joblib.load('random_forest_model.pkl')
print(loaded_rf)

# Now you can use the loaded model to make predictions
#prediction = loaded_rf.predict(new_customer)
#print(prediction)

Chargement des données

#load the datas
import pickle

with open('X_test.pkl', 'rb') as f:
    X_test = pickle.load(f)
with open('y_test.pkl', 'rb') as f:
    y_test = pickle.load(f)

X_test.head()

On rejoue le test, X_test contient les clients, et on passe en argument de la fonction predict qui va nous retourner la variable prediction, qui est une Serie, contenant des 0 et 1, 0 correspondant à « No » et 1 à « Yes ».

# Now you can use the loaded model to make predictions
from sklearn.metrics import accuracy_score
prediction = loaded_rf.predict(X_test)
#la série qui contient les outcome (churn si 1 , 0 si no churn)
print(prediction)

[0 0 1 ... 0 1 1]

Ce qu'on voit c'est la colonne churn

Test avec un sous ensemble

Ici je simule le fait que j’ai un nouveau client à scorer, c’est donc une ligne (un seul client), je passe cette ligne en argument de la fonction predict, qui va nous retourner un Serie avec un seul élément (on n’a scoré qu’un client), ici on obtient un « Yes ».

# test on row
# subset = X_test[X_test['TotalCharges']<100]
# extract a line from X_test
oneline = X_test.loc[[6125]]
#convert to dataframe
#oneline = oneline.to_frame()


import pandas as pd
isinstance(oneline,pd.DataFrame) # test if it's DataFrame
print(oneline)
oneline.head()


prediction = loaded_rf.predict(oneline)
print(prediction)

[1] //  Churn

Retour en haut