Il ne vous aura pas échappé que les modèles n'acceptent comme données d'entrée que des chiffres. Si votre dataset d'entraînement ou vos échantillons de prédictions contiennent du texte, vous aurez une erreur de Python.
Le traitement apporté aux données catégoriques textuelles va dépendre du nombre de catégories prises par la variable. On distinguera :
le cas binaire où l'on a 2 catégories ;
et pour être très précis, les cas "un peu plus que 2" ou "carrément beaucoup".
Chargeons à nouveau les données et limitons-nous aux platanes :
dataset_url = "https://raw.githubusercontent.com/OpenClassrooms-Student-Center/8063076-Initiez-vous-au-Machine-Learning/master/data/paris-arbres-clean-2023-09-10.csv"
data = pd.read_csv(dataset_url)
df = data[data.libelle_francais == 'Platane'].copy()
Le cas binaire
La variable prend 2 valeurs distinctes et exclusives de type oui/non, vrai/faux, mort/vivant, homme/femme, positif/négatif au test, etc.
Sur le dataset des arbres, la variable remarquable
est binaire.
df.remarquable.value_counts()
remarquable
NON 185232
OUI 179
Il est facile de transformer ces valeurs en chiffres en choisissant de manière arbitraire une valeur pour chaque catégorie :
df.loc[df.remarquable == NON, 'remarquable'] = 0
df.loc[df.remarquable == OUI, 'remarquable'] = 1
On obtient alors :
df.remarquable.value_counts()
remarquable
0 185232
1 179
Cas non binaire mais peu de catégories
Les choses se compliquent un peu quand on a plus de 2 catégories à encoder. On distingue :
les catégories ordonnées (ordinales) ;
des catégories non ordonnées.
Exemple :
Catégories ordonnées (ou ordinales) | Catégories non ordonnées |
|
|
L'encodage ordonné
Prenons l'exemple du stade de développement des arbres et supposons que les valeurs de cette variable sont ordonnées de la manière suivante :
Jeune (arbre) < Jeune (arbre)Adulte < Adulte < Mature
Voici une méthode simple pour numériser ces catégories :
categories = ['Adulte', 'Jeune (arbre)Adulte', 'Jeune (arbre)', 'Mature']
for n, categorie in zip(range(1,len(categories)+1), categories):
df.loc[
df.stade_de_developpement == categorie,
'stade_de_developpement'
] = n
Et on obtiendra :
stade_de_developpement
3 21620
2 8356
1 5916
4 3346
avec une équivalence (mapping) entre les catégories et les nombres
1 => Jeune (arbre)
...
4 => Mature
Vous pouvez installer cette librairie avec :
!pip install category_encoders
Pour encoder des catégories ordinales, nous utilisons la classe OrdinalEncoder
.
Pour que l'ordre des catégories soit respecté dans l'encodage, il faut donner un mapping explicite à la fonction fit()
comme ceci :
from category_encoders.ordinal import OrdinalEncoder
mapping =[ {'col': 'stade_de_developpement',
'mapping': {
np.nan: 0,
'Jeune (arbre)': 1,
'Jeune (arbre)Adulte': 2,
'Adulte': 3,
'Mature': 4
}
} ]
encoder = OrdinalEncoder(mapping=mapping)
stade = encoder.fit_transform(df.stade_de_developpement)
stade.value_counts()
On remplace ensuite les catégories originales par leur équivalents numériques :
df['stade_de_developpement'] = stade.copy()
Et les stades de développement sont maintenant numérisés :
df['stade_de_developpement'].value_counts(dropna = False)
stade_de_developpement
3 21620
2 8356
1 5916
0 3350
4 3346
Notez que la forme du mapping est un peu spéciale. Elle permet d'encoder plusieurs colonnes à la fois en fournissant pour chaque colonne un dictionnaire d'équivalence.
Notez aussi que l'on peut spécifier l'encodage des valeurs manquantes. Ici np.nan => 0
.
Cas non ordonné : Si l'on remplace de la même façon les valeurs d'une catégorie non ordonnée par une suite de nombres, on introduit un ordre fictif dans les catégories. Cette information supplémentaire d'ordre pourrait être utilisée par le modèle alors qu'elle ne correspond à rien de concret.
On cherche donc une technique d'encodage qui n'introduit pas cette information inutile.
L'encodage one-hot
Le principe est d'associer pour chaque valeur de la catégorie une nouvelle variable binaire qui indique si la variable originale avait la valeur en question. Si la variable prend N valeurs distinctes, on introduira donc N-1 nouvelles variables, la Nième étant redondante.
Prenons un exemple avec nos arbres et la variable domanialité
qui prend les 9 valeurs suivantes :
domanialite
Alignement 35747
CIMETIERE 3121
Jardin 2038
DASCO 816
PERIPHERIQUE 603
DJS 182
DFPE 71
DAC 7
DASES 3
Pour que l'exemple soit plus clair, ne gardons que les arbres des 3 domanialités les plus fréquentes :
df = df [df.domanialite.isin(['Alignement', 'Jardin', 'CIMETIERE'])].copy()
df.reset_index(drop = True, inplace = True)
On va créer 2 variables booléennes : is_alignement
et is_jardin
, telles que :
is_alignement = 1 if (domanialite = 'Alignement') else 0
is_jardin = 1 if (domanialite = 'Jardin') else 0
Comme les catégories sont exclusives, un arbre ne peut pas être à la fois dans un alignement ou un jardin et dans un cimetière, la valeur cimetière n'a pas besoin d'être explicitée par une variable is_cimetiere. On a logiquement :
domanialite = CIMETIERE si is_alignement =0 et is_jardin = 0
Tout cela est un peu lourd. Imaginez faire cela à la main pour une variable à 5, 10 ou plus de valeurs.
from sklearn import preprocessing
enc = preprocessing.OneHotEncoder()
labels = enc.fit_transform(df.domanialite.values.reshape(-1, 1)).toarray()
labels.shape
(40906, 3)
On a donc 3 colonnes correspondant aux 3 valeurs de domanialité.
Il faut ensuite intégrer 2 de ces 3 colonnes (la 3e est redondante) et supprimer la colonne originale avec un peu de manipulation de pandas.dataframe.
df = pd.concat([df,
pd.DataFrame(columns = ['is_alignement','is_jardin'],
data = labels[:, :2])],
axis = 1)
De plus, la plupart de ces nouvelles variables ( is_peripherique
, is_alignement
, etc.) sont principalement constituées de 0. Par exemple, la domanialité PERIPHERIQUE
a seulement 5 225 arbres sur un total de 207 641 arbres, soit 2,5 % des arbres. La colonne binaire is_peripherique
correspondant à cette valeur va être à 97,5 % des zéros, donc extrêmement peu informative.
De même si on considère les variables comme espece
qui a 559 valeurs différentes, l'encodage one hot
ajoutera 558 nouvelles variables au dataset, la plupart truffées de 0 sans grande valeur.
On va donc noyer les informations contenues dans les 7 variables restantes dans la masse des variables dédiées aux catégories de l'espèce.
En conclusion, le one hot encoding n'est réellement utilisable que lorsque le nombre de valeurs de la variable à numériser est faible par rapport aux nombre de variables utiles du dataset.
Passons maintenant aux choses sérieuses !
L'encodage binaire
Prenons un exemple de la variable domanialité qui a 9 variables.
Le chiffre 9 en binaire s'écrit 1001
soit avec 4 digits. En associant à chaque digit une variable booléenne 0/1
on peut encoder les valeurs de cette variable à 9 valeurs sur 4 dimensions au lieu des 8 du one hot encoding.
On aura par exemple :
Alignement => 1 => 0000
Jardin => 2 => 0001
CIMETIERE => 3 => 0010
DASES => 9 => 1001
etc.
De même, la variable espece
qui a 559 valeurs distinctes pour tout le dataset (pas seulement les platanes) ne nécessite alors plus que 10 variables au lieu de 558 ( ).
Avec category_encoders
, le codage binaire de la variable espece
suit :
from category_encoders.binary import BinaryEncoder
encoder = BinaryEncoder()
espece_bin = encoder.fit_transform(data.espece)
espece_bin.shape
(207641, 10)
espece_bin.head()
espece_0 espece_1 espece_2 espece_3 espece_4 espece_5 espece_6 espece_7 espece_8 espece_9
0 0 0 0 0 0 0 0 0 0 1
1 0 0 0 0 0 0 0 0 1 0
2 0 0 0 0 0 0 0 0 1 1
3 0 0 0 0 0 0 0 1 0 0
4 0 0 0 0 0 0 0 1 0 1
Le Feature Engineering
Revenons vers des choses plus créatives que sont le feature engineering (ou en français : la création de nouvelles variables).
On peut néanmoins mentionner comme techniques répandues :
la mise en intervalle (bucket) des données continues ;
la transformation de variables existantes : puissance, inverse, racine carrée ou log, etc. ;
la création d’une variable
flag
qui indique si une autre variable est renseignée ou manquante, ou tout autre caractéristique notable ;et la compilation de plusieurs variables ensemble : par opérations sur les variables numériques, agrégation de textes, filtres sur les images, etc.
En fait, l'amélioration du dataset par le feature engineering revient à faire à un véritable travail de détective à partir :
de l'analyse statistique des variables ;
des connaissances du domaine ;
de l'intégration de datasets externes ;
de l'étude précise des erreurs de prédiction du modèle.
Dans notre contexte urbain forestier, nous pourrions augmenter notre dataset avec des données sur l'environnement de chaque arbre en intégrant les images de Google Maps obtenues à partir des coordonnées géographiques contenues dans le dataset.
Autre exemple de feature engineering. Dans le chapitre 2 de la partie 2, nous avons construit une régression linéaire sur le dataset advertising. Pour rappel, le revenu dépend du budget de publicité dans 3 médias : télévision, journaux et radio. La régression est de la forme ventes ~ tv + radio + journaux
.
Après analyse visuelle de la relation entre les ventes et la télé, nous avons vérifié qu'ajouter le carré de la variable tv
améliorerait significativement le score du modèle :
ventes ~ tv^2 + tv + radio + journaux
En résumé
Les variables catégoriques textuelles doivent être numérisées pour être compatibles avec le modèle.
La méthode la plus simple est l'encodage ordinal qui associe un entier à chaque catégorie. Il peut cependant induire un ordre qui n'est pas présent dans la variable originale.
L'encodage One Hot crée une nouvelle variable booléenne par valeur de la variable catégorique originale.
Lorsque la variable catégorique originale prend beaucoup de valeurs, l'encodage One Hot va créer trop de nouvelles variables qui vont noyer l'information contenue dans les autres variables du dataset.
Une alternative consiste à appliquer l'encodage binaire, qui réduit fortement le nombre de variables supplémentaires qui sont nécessaires à l'encodage.
Dans le prochain chapitre, nous allons revenir sur la partie modèle du binôme ‘données - modèle’ avec un concept essentiel au Machine Learning : l'overfit !