Dans ce post, nous allons découvrir comment adapter un jeux de données contenant des données textuelles, pour le rendre utilisable dans des modèles de classification, de regression ou de clustering. L’objectif est de convertir les informations contenues dans ces chaines de caractéres en une matrice numérique, sans en perdre le sens. Bref, le genre de savoir-faire indispensable en Machine/Deep Learning.

Nous utiliserons un dataset sur la consommation et l’autonomie de voitures électriques. Voici un extrait :

| marque   | modèle                  | batterie   | route     | conditions   |   consommation | autonomie   |
|:---------|:------------------------|:-----------|:----------|:-------------|---------------:|:------------|
| Aiways   | U5                      | 65 kWh     | autoroute | hiver        |           35.4 | 181 km      |
| Aiways   | U5                      | 65 kWh     | route     | hiver        |           24.2 | 256 km      |
| Aiways   | U5                      | 65 kWh     | ville     | hiver        |           19.3 | 335 km      |
| Aiways   | U5                      | 65 kWh     | autoroute | été          |           27.2 | 232 km      |
| Aiways   | U5                      | 65 kWh     | route     | été          |           18.5 | 341 km      |
| Aiways   | U5                      | 65 kWh     | ville     | été          |           14.6 | 430 km      |
| Audi     | Q4 Sportback 50 Quattro | 82 kWh     | autoroute | hiver        |           42.1 | 180 km      |
| Audi     | Q4 Sportback 50 Quattro | 82 kWh     | route     | hiver        |           24.2 | 314 km      |
| Audi     | Q4 Sportback 50 Quattro | 82 kWh     | ville     | hiver        |           19.3 | 391 km      |
| Audi     | Q4 Sportback 50 Quattro | 82 kWh     | autoroute | été          |           32.4 | 237 km      |
| Audi     | Q4 Sportback 50 Quattro | 82 kWh     | route     | été          |           18.5 | 419 km      |
| Audi     | Q4 Sportback 50 Quattro | 82 kWh     | ville     | été          |           14.6 | 528 km      |
| BMW      | i4 eDrive40             | 83.9 kWh   | autoroute | hiver        |           31.7 | 254 km      |
| BMW      | i4 eDrive40             | 83.9 kWh   | route     | hiver        |           20.6 | 400 km      |
| BMW      | i4 eDrive40             | 83.9 kWh   | ville     | hiver        |           16.4 | 467 km      |
| BMW      | i4 eDrive40             | 83.9 kWh   | autoroute | été          |           24.4 | 330 km      |
| BMW      | i4 eDrive40             | 83.9 kWh   | route     | été          |           15.7 | 513 km      |
| BMW      | i4 eDrive40             | 83.9 kWh   | ville     | été          |           12.4 | 631 km      |
| Citroën  | DS3 Crossback E-Tense   | 50 kWh     | autoroute | hiver        |           37.8 | 122 km      |
| Citroën  | DS3 Crossback E-Tense   | 50 kWh     | route     | hiver        |           23.2 | 199 km      |
| Citroën  | DS3 Crossback E-Tense   | 50 kWh     | ville     | hiver        |           18.5 | 253 km      |
| Citroën  | DS3 Crossback E-Tense   | 50 kWh     | autoroute | été          |           29.1 | 158 km      |
| Citroën  | DS3 Crossback E-Tense   | 50 kWh     | route     | été          |           17.7 | 259 km      |
| Citroën  | DS3 Crossback E-Tense   | 50 kWh     | ville     | été          |           14   | 324 km      |

Lien vers le dataset complet ‘datas_voitures_electriques’.

Revue du dataset

Classiquement, nous allons charger ce dataset dans un dataframe Pandas.

import pandas as pd

df = pd.read_csv('datas_voitures_electriques.csv',sep=';')

Le dataset contient 2 types de données :

  • des données numériques : consommation, batterie et autonomie,
  • des données de type catégories : marque, modèle, route et conditions.

Regardons les valeurs que prennent les catégories, colonne par colonne.

categories_cols = ['marque','modèle','route','conditions']
{c: df[c].unique() for c in categories_cols}
  • .unique() extrait les différentes valeurs existantes dans une colonne,
  • for c in categories_cols itère sur la liste des noms de colonnes dans categories_cols,
  • et {c: df[c].unique() for …} génère un dictionnaire avec le nom de chaque colonne comme clé, et les valeurs uniques en valeur.
{'marque': array(['Aiways', 'Audi', 'BMW', 'Citroën', 'Fiat', 'Ford', 'Honda',
        'Hyundai', 'Kia', 'MG', 'Mazda', 'Nissan', 'Peugeot', 'Porsche',
        'Renault', 'Tesla', 'Volkswagen', 'Volvo'], dtype=object),
 'modèle': array(['U5', 'Q4 Sportback 50 Quattro', 'i4 eDrive40',
        'DS3 Crossback E-Tense', 'ë-C4', 'Fiat 500 long range',
        'Mustang Mach-E', 'e', 'Ioniq 5', 'Kona Electric', 'e-Niro',
        'Marvel R 4WD', 'MX-30', 'Leaf e+', 'e-2008', 'e-208',
        'Taycan  Perf Plus', 'Mégane Electrique', 'Zoé R110', 'Zoé R135',
        'Model 3  Standard Plus (2020)', 'Model 3 Grande Autonomie (2020)',
        'Model Y Grande Autonomie (conditions hiver)', 'ID.3', 'ID.4',
        'e-up!', 'XC40 P8'], dtype=object),
 'route': array(['autoroute', 'route', 'ville'], dtype=object),
 'conditions': array(['hiver', 'été'], dtype=object)}

Premiers constats :

  • conditions ne contient que deux valeurs possibles. Alors que marque, modèle et route en contiennent au moins trois,
  • batterie et autonomie sont des données numériques sous forme de texte. Je t’explique dans les Bonus comment régler ce problème.

Quelles sont les transformations possibles ?

Il y a trois genres de catégories dans ce dataset :

  • les catégories binaires (2 valeurs) : oui/non, vrai/faux, et été/hiver,
  • les catégories avec un ordre hierarchique : maternelle/primaire/college/lycée, froid/tiede/chaud, et ville/route/autoroute,
  • les catégories sans ordre : une liste de pays, une liste de films, et la liste des marques de véhicules.

La bonne transformation de ces types de catégories est celle qui donnera du sens aux données pour tes modèles à venir. Si tu traites la liste des marques de véhicules comme une catégorie avec un ordre hiérarchique, tu indiqueras à ton modèle de Machine Learning qu’une marque est “plus quelque-chose” qu’une autre. C’est pas toujours un bon plan …

Pour faire ces transformations, nous allons utiliser les méthodes de preprocessing de scikit-learn.

Pour les catégories binaires

Voici la transformation la plus simple, convertir les données de la colonne df.conditions en 0 et en 1.

avec LabelBinarizer

Première étape : instancier un preprocesseur avec la méthode LabelBinarizer, puis “l’ajuster” avec les données de la colonne.

from sklearn.preprocessing import LabelBinarizer

lb = LabelBinarizer()
lb.fit(df.conditions)

Le LabelBinarizer apprend les classes à transformer. Les classes déterminées ont la même valeur que celles retournées par df.conditions.unique().

lb.classes_
array(['hiver', 'été'], dtype='<U5')

df.conditions.unique()
array(['hiver', 'été'], dtype=object)

Ensuite, convertir le contenu de la colonne avec la méthode transform. Le vecteur final contient que des zéros et des uns, comme attendu.

df.conditions = lb.transform(df.conditions)
df.conditions.values
array([0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1,
       1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0,
       0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1,
       0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1,
       1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0,
       0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1,
       0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1,
       1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1])

L’intérêt des preprocesseurs scikit-learn est de pouvoir faire la transformation inverse. Et donc de retrouver la forme textuelle des résultats d’une prédiction.

lb.inverse_transform(df.conditions)
array(['hiver', 'hiver', 'hiver', 'été', 'été', 'été', 'hiver', 'hiver',
       'hiver', 'été', 'été', 'été', 'hiver', 'hiver', 'hiver', 'été',
       'été', 'été', 'hiver', 'hiver', 'hiver', 'été', 'été', 'été',
       'hiver', 'hiver', 'hiver', 'été', 'été', 'été', 'hiver', 'hiver',
       'hiver', 'été', 'été', 'été', 'hiver', 'hiver', 'hiver', 'été',
       'été', 'été', 'hiver', 'hiver', 'hiver', 'été', 'été', 'été',
       'hiver', 'hiver', 'hiver', 'été', 'été', 'été', 'hiver', 'hiver',
       'hiver', 'hiver', 'hiver', 'hiver', 'été', 'été', 'été', 'été',
       'été', 'été', 'hiver', 'hiver', 'hiver', 'été', 'été', 'été',
       'hiver', 'hiver', 'hiver', 'été', 'été', 'été', 'hiver', 'hiver',
       'hiver', 'été', 'été', 'été', 'hiver', 'hiver', 'hiver', 'été',
       'été', 'été', 'hiver', 'hiver', 'hiver', 'été', 'été', 'été',
       'hiver', 'hiver', 'hiver', 'été', 'été', 'été', 'hiver', 'hiver',
       'hiver', 'été', 'été', 'été', 'hiver', 'hiver', 'hiver', 'été',
       'été', 'été', 'hiver', 'hiver', 'hiver', 'été', 'été', 'été',
       'hiver', 'hiver', 'hiver', 'été', 'été', 'été', 'hiver', 'hiver',
       'hiver', 'été', 'été', 'été', 'hiver', 'hiver', 'hiver', 'été',
       'été', 'été', 'hiver', 'hiver', 'hiver', 'été', 'été', 'été',
       'hiver', 'hiver', 'hiver', 'été', 'été', 'été', 'hiver', 'hiver',
       'hiver', 'été', 'été', 'été', 'hiver', 'hiver', 'hiver', 'été',
       'été', 'été', 'hiver', 'hiver', 'hiver', 'été', 'été', 'été'],
      dtype='<U5')

avec MultiLabelBinarizer

Si le dataset contient plusieurs colonnes contenant des catégories binaires, MultiLabelBinarizer pourra transformer toutes les colonnes binaires en une seule fois. Et les décoder aussi bien sûr.

Pour les catégories ordonnées

Les “catégories ordonnées” sont des catégories où les données ont un sens hierarchique les unes par rapport aux autres. Dans ce cas, tu peux choisir le LabelEncoder ou le OrdinalEncoder.

Leur différence est la dimension de la matrice qu’ils traitent en entrée. LabelEncoder prend en charge un vecteur 1D, tandis que OrdinalEncoder gère une matrice 2D. LabelEncoder est utilisé pour transformer une “target”, OrdinalEncoder pour transformer des “features”.

avec LabelEncoder

LabelEncoder est aussi simple à utiliser que LabelBinarizer :

from sklearn.preprocessing import LabelEncoder

le = LabelEncoder()

le.fit(df.route)
le.classes_
array(['autoroute', 'route', 'ville'], dtype=object)

A savoir : les classes sont ordonnées alphabétiquement. C’est parfait pour notre exemple, moins pour ['froid', 'tiede', 'chaud']. Je t’explique ici comment forcer l’ordre de tes catégories.

le.transform(df.route)
array([0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0,
       1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1,
       2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 0, 1, 1, 2, 2, 0, 0, 1, 1, 2, 2,
       0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0,
       1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1,
       2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2,
       0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0,
       1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 2])

Les valeurs d’origine d’un vecteur transformé sont récupérées avec la méthode inverse_transform comme pour LabelBinarizer.

le.inverse_transform([2, 0, 1, 2, 0])
array(['ville', 'autoroute', 'route', 'ville', 'autoroute'], dtype=object)

L’énorme avantage de ces préprocesseurs est de pouvoir les sauvegarder après les avoir ajustés. Ils sont disponibles pour l’encodage et le decodage des jeux de données sans reparamétrer les classes / catégories à chaque fois.

avec OrdinalEncoder

OrdinalEncoder apprend des categories à partir d’une matrice 2D. Pour le reste, le fonctionnement est identique aux préprocesseurs précédents.

from sklearn.preprocessing import OrdinalEncoder

oe = OrdinalEncoder()
oe.fit(df[['route','conditions']])

Les catégories apprises sont restituées dans une matrice 2D.

oe.categories_
[array(['autoroute', 'route', 'ville'], dtype=object),
 array(['hiver', 'été'], dtype=object)]

La transformation de la matrice de features retourne une matrice de même dimension, entièrement numérique.

oe.transform(df.loc[:6,['route','conditions']])
array([[0., 0.],
       [1., 0.],
       [2., 0.],
       [0., 1.],
       [1., 1.],
       [2., 1.],
       [0., 0.]])

Et toujours la transformation inverse :D

oe.inverse_transform([[0., 1.],[1., 0.],[2., 1.]])
array([['autoroute', 'été'],
       ['route', 'hiver'],
       ['ville', 'été']], dtype=object)

Modifier l’ordre des catégories

Si l’ordre des catégories ne correspond pas à leur ordre alphabétique, tu peux forcer l’ordre dans le préprocesseur. Tout se joue lors de l’instantiation :

oe = OrdinalEncoder(categories=[['ville','route','autoroute'],['été','hiver']])
oe.fit(df[['route','conditions']])

oe.transform(df.loc[:6,['route','conditions']])
array([[2., 1.],
       [1., 1.],
       [0., 1.],
       [2., 0.],
       [1., 0.],
       [0., 0.],
       [2., 1.]])

‘autoroute’ est converti en ‘2’ au lieu de ‘0’, ‘route’ vaut toujours ‘1’, et ‘ville’ est converti en ‘0’ au lieu de ‘2’. Idem pour les conditions ‘été’ et ‘hiver’.

oe.inverse_transform([[0., 1.],[1., 0.],[2., 1.]])
array([['ville', 'hiver'],
       ['route', 'été'],
       ['autoroute', 'hiver']], dtype=object)

Gérer les catégories inconnues

Si des valeurs à transformer sont inconnues, soit le préprocessor lève une erreur :

oe.transform([['route','été'],['route','automne']])
ValueError: Found unknown categories ['automne'] in column 1 during transform

… soit il attribue une valeur par défaut, définie lors de l’instanciation :

oe = OrdinalEncoder(categories=[['ville','route','autoroute'],['été','hiver']],
                    handle_unknown = 'use_encoded_value', 
                    unknown_value = 9)
oe.fit(df[['route','conditions']])
  • handle_unknown = 'use_encoded_value' : modifie le comportement par défaut du préprocesseur,
  • unknown_value = 9 : defini la valeur par défaut pour toutes les valeurs inconnues.
oe.transform([['route','été'],['route','automne'],['ville','printemps']])
array([[1., 0.],
       [1., 9.],
       [0., 9.]])

La transformation inverse de la valeur par défaut retourne None.

oe.inverse_transform([[1., 0.],[1., 9.],[0., 9.]])
array([['route', 'été'],
       ['route', None],
       ['ville', None]], dtype=object)

Pour les catégories sans ordre

La dernière transformation de ce post est celle utilisée la plus fréquemment. C’est aussi celle qui m’a parue la moins intuitive pendant longtemps.

L’idée est de transformer une catégorie en un vecteur de dimension égale au nombre de valeurs possibles de la catégorie. Chaque colonne contient 0 ou 1 quand la colonne représente la valeur transformée. Par exemple : [['Mazda'],['Nissan'],['Porsche'],['Porsche']] devient [[1,0,0,],[0,1,0,],[0,0,1],[0,0,1]]. Cette transformation est le One Hot Encoding. Elle est très utilisée en Natural Langage Processing (c’est pour ça que je suis fan :D ).

avec One Hot Encoding 🔥

Comme pour les autres préprocesseurs, il faut l’instancier, puis l’ajuster avec les jeux de données.

from sklearn.preprocessing import OneHotEncoder

ohe = OneHotEncoder()
ohe.fit(df[['marque','modèle']])

Tu peux visualiser les catégories apprises,

ohe.categories_
[array(['Aiways', 'Audi', 'BMW', 'Citroën', 'Fiat', 'Ford', 'Honda',
        'Hyundai', 'Kia', 'MG', 'Mazda', 'Nissan', 'Peugeot', 'Porsche',
        'Renault', 'Tesla', 'Volkswagen', 'Volvo'], dtype=object),
 array(['DS3 Crossback E-Tense', 'Fiat 500 long range', 'ID.3', 'ID.4',
        'Ioniq 5', 'Kona Electric', 'Leaf e+', 'MX-30', 'Marvel R 4WD',
        'Model 3  Standard Plus (2020)', 'Model 3 Grande Autonomie (2020)',
        'Model Y Grande Autonomie (conditions hiver)', 'Mustang Mach-E',
        'Mégane Electrique', 'Q4 Sportback 50 Quattro',
        'Taycan  Perf Plus', 'U5', 'XC40 P8', 'Zoé R110', 'Zoé R135', 'e',
        'e-2008', 'e-208', 'e-Niro', 'e-up!', 'i4 eDrive40', 'ë-C4'],
       dtype=object)]

et visualiser la matrice obtenue après transformation. Ici, on affiche les lignes 4 à 7.

ohe.transform(df[['marque','modèle']]).toarray()[4:8,:]
array([[1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
        0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
        0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
        1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
        1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]])

Le résultat est converti avec .toarray() pour le visualiser facilement. Par défaut, la transformation retourne une ‘sparse matrix’.

ohe.transform(df[['marque','modèle']])
<168x45 sparse matrix of type '<class 'numpy.float64'>'
	with 336 stored elements in Compressed Sparse Row format>

Les matrices obtenues par ‘One Hot Encoding’ sont surtout composées de zéros. Retourner une ‘sparse matrix’ optimise la quantité de mémoire allouée.

Pour savoir à quoi correspond l’un des vecteurs composant la matrice, tu peux obtenir le nom des colonnes transformées …

# avec scikit-learn 0.24
ohe.get_feature_names(['marque','modèle'])

# avec scikit-learn 1.0.2
ohe.get_feature_names_out(['marque','modèle'])

array(['marque_Aiways', 'marque_Audi', 'marque_BMW', 'marque_Citroën',
       'marque_Fiat', 'marque_Ford', 'marque_Honda', 'marque_Hyundai',
       'marque_Kia', 'marque_MG', 'marque_Mazda', 'marque_Nissan',
       'marque_Peugeot', 'marque_Porsche', 'marque_Renault',
       'marque_Tesla', 'marque_Volkswagen', 'marque_Volvo',
       'modèle_DS3 Crossback E-Tense', 'modèle_Fiat 500 long range',
       'modèle_ID.3', 'modèle_ID.4', 'modèle_Ioniq 5',
       'modèle_Kona Electric', 'modèle_Leaf e+', 'modèle_MX-30',
       'modèle_Marvel R 4WD', 'modèle_Model 3  Standard Plus (2020)',
       'modèle_Model 3 Grande Autonomie (2020)',
       'modèle_Model Y Grande Autonomie (conditions hiver)',
       'modèle_Mustang Mach-E', 'modèle_Mégane Electrique',
       'modèle_Q4 Sportback 50 Quattro', 'modèle_Taycan  Perf Plus',
       'modèle_U5', 'modèle_XC40 P8', 'modèle_Zoé R110',
       'modèle_Zoé R135', 'modèle_e', 'modèle_e-2008', 'modèle_e-208',
       'modèle_e-Niro', 'modèle_e-up!', 'modèle_i4 eDrive40',
       'modèle_ë-C4'], dtype=object)

… ou récupérer les valeurs d’origine par une transformation inverse du vecteur. Il faut évidemment remanier le vecteur 1D en matrice 2D.

vecteur = [0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0.,
           0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
           0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0.]
ohe.inverse_transform([vecteur])
array([['Hyundai', 'Zoé R135']], dtype=object)

Alors, le préprocesseur peut transformer n’importe quelles matrices contenant les features apprises.

ohe.transform([['Mazda','U5'],['Volvo','Marvel R 4WD']]).toarray()
array([[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0.,
        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
        0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
        0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0.,
        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]])

Enfin, le comportement du préprocesseur lorsqu’il rencontre une valeur inconnue est paramétrable.

Soit il lève une erreur,

ohe = OneHotEncoder(handle_unknown='error')
ohe.fit(df[['marque','modèle']])
ohe.transform([['Mazda','U2']]).toarray()
ValueError: Found unknown categories ['U2'] in column 1 during transform

Soit il ignore la nouvelle valeur. Ça fait sens pour un modèle utilisé les utilisateurs finaux.

ohe = OneHotEncoder(handle_unknown='ignore')
ohe.fit(df[['marque','modèle']])
ohe.transform([['Mazda','U2']]).toarray()
array([[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0.,
        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]])

Le résultat ne contient qu’une seule valeur à 1, celle qui correspond à la marque ‘Mazda’. Toutes les valeurs liées au modèle sont à zéro. Dans ce cas, la transformation inverse retourne une valeur à None.

ohe.inverse_transform([[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 1., 0., 0., 0., 0., 0.,
        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]])
array([['Mazda', None]], dtype=object)

Références

Bonus

Pourquoi laisser de côté pandas.get_dummies() ?

“pandas.get_dummies convert categorical variable into dummy/indicator variables.” (ref). Sans mentir, ça fait le job, non ?

pd.get_dummies(df,columns=['marque','modèle','route','conditions'])

|     | batterie   |   consommation | autonomie   |   marque_Aiways |   marque_Audi |   marque_BMW |   marque_Citroën |   marque_Fiat |   marque_Ford |   marque_Honda |   marque_Hyundai |   marque_Kia |   marque_MG |   marque_Mazda |   marque_Nissan |   marque_Peugeot |   marque_Porsche |   marque_Renault |   marque_Tesla |   marque_Volkswagen |   marque_Volvo |   modèle_DS3 Crossback E-Tense |   modèle_Fiat 500 long range |   modèle_ID.3 |   modèle_ID.4 |   modèle_Ioniq 5 |   modèle_Kona Electric |   modèle_Leaf e+ |   modèle_MX-30 |   modèle_Marvel R 4WD |   modèle_Model 3  Standard Plus (2020) |   modèle_Model 3 Grande Autonomie (2020) |   modèle_Model Y Grande Autonomie (conditions hiver) |   modèle_Mustang Mach-E |   modèle_Mégane Electrique |   modèle_Q4 Sportback 50 Quattro |   modèle_Taycan  Perf Plus |   modèle_U5 |   modèle_XC40 P8 |   modèle_Zoé R110 |   modèle_Zoé R135 |   modèle_e |   modèle_e-2008 |   modèle_e-208 |   modèle_e-Niro |   modèle_e-up! |   modèle_i4 eDrive40 |   modèle_ë-C4 |   route_autoroute |   route_route |   route_ville |   conditions_hiver |   conditions_été |
|----:|:-----------|---------------:|:------------|----------------:|--------------:|-------------:|-----------------:|--------------:|--------------:|---------------:|-----------------:|-------------:|------------:|---------------:|----------------:|-----------------:|-----------------:|-----------------:|---------------:|--------------------:|---------------:|-------------------------------:|-----------------------------:|--------------:|--------------:|-----------------:|-----------------------:|-----------------:|---------------:|----------------------:|---------------------------------------:|-----------------------------------------:|-----------------------------------------------------:|------------------------:|---------------------------:|---------------------------------:|---------------------------:|------------:|-----------------:|------------------:|------------------:|-----------:|----------------:|---------------:|----------------:|---------------:|---------------------:|--------------:|------------------:|--------------:|--------------:|-------------------:|-----------------:|
|   0 | 65 kWh     |           35.4 | 181 km      |               1 |             0 |            0 |                0 |             0 |             0 |              0 |                0 |            0 |           0 |              0 |               0 |                0 |                0 |                0 |              0 |                   0 |              0 |                              0 |                            0 |             0 |             0 |                0 |                      0 |                0 |              0 |                     0 |                                      0 |                                        0 |                                                    0 |                       0 |                          0 |                                0 |                          0 |           1 |                0 |                 0 |                 0 |          0 |               0 |              0 |               0 |              0 |                    0 |             0 |                 1 |             0 |             0 |                  1 |                0 |
|   1 | 65 kWh     |           23.9 | 252 km      |               1 |             0 |            0 |                0 |             0 |             0 |              0 |                0 |            0 |           0 |              0 |               0 |                0 |                0 |                0 |              0 |                   0 |              0 |                              0 |                            0 |             0 |             0 |                0 |                      0 |                0 |              0 |                     0 |                                      0 |                                        0 |                                                    0 |                       0 |                          0 |                                0 |                          0 |           1 |                0 |                 0 |                 0 |          0 |               0 |              0 |               0 |              0 |                    0 |             0 |                 0 |             1 |             0 |                  1 |                0 |
|   2 | 65 kWh     |           19.9 | 335 km      |               1 |             0 |            0 |                0 |             0 |             0 |              0 |                0 |            0 |           0 |              0 |               0 |                0 |                0 |                0 |              0 |                   0 |              0 |                              0 |                            0 |             0 |             0 |                0 |                      0 |                0 |              0 |                     0 |                                      0 |                                        0 |                                                    0 |                       0 |                          0 |                                0 |                          0 |           1 |                0 |                 0 |                 0 |          0 |               0 |              0 |               0 |              0 |                    0 |             0 |                 0 |             0 |             1 |                  1 |                0 |
|   3 | 65 kWh     |           27.2 | 232 km      |               1 |             0 |            0 |                0 |             0 |             0 |              0 |                0 |            0 |           0 |              0 |               0 |                0 |                0 |                0 |              0 |                   0 |              0 |                              0 |                            0 |             0 |             0 |                0 |                      0 |                0 |              0 |                     0 |                                      0 |                                        0 |                                                    0 |                       0 |                          0 |                                0 |                          0 |           1 |                0 |                 0 |                 0 |          0 |               0 |              0 |               0 |              0 |                    0 |             0 |                 1 |             0 |             0 |                  0 |                1 |
|   4 | 65 kWh     |           18.5 | 341 km      |               1 |             0 |            0 |                0 |             0 |             0 |              0 |                0 |            0 |           0 |              0 |               0 |                0 |                0 |                0 |              0 |                   0 |              0 |                              0 |                            0 |             0 |             0 |                0 |                      0 |                0 |              0 |                     0 |                                      0 |                                        0 |                                                    0 |                       0 |                          0 |                                0 |                          0 |           1 |                0 |                 0 |                 0 |          0 |               0 |              0 |               0 |              0 |                    0 |             0 |                 0 |             1 |             0 |                  0 |                1 |
|   5 | 65 kWh     |           15.2 | 430 km      |               1 |             0 |            0 |                0 |             0 |             0 |              0 |                0 |            0 |           0 |              0 |               0 |                0 |                0 |                0 |              0 |                   0 |              0 |                              0 |                            0 |             0 |             0 |                0 |                      0 |                0 |              0 |                     0 |                                      0 |                                        0 |                                                    0 |                       0 |                          0 |                                0 |                          0 |           1 |                0 |                 0 |                 0 |          0 |               0 |              0 |               0 |              0 |                    0 |             0 |                 0 |             0 |             1 |                  0 |                1 |

Presque. Pour rejouer cette transformation avec une partie du jeux de données d’origine, sans code supplémentaire, il manquera des features en sortie. Essayons avec un jeu de données ne contenant que les voitures de marque ‘Volkswagen’.

df_VW = df.loc[df.marque == 'Volkswagen']
df_VW.shape
(18, 7)

18 échantillons (lignes), 1 marque, 3 modèles, 3 routes et 2 conditions.

df_VW
|     | marque     | modèle   | batterie   | route     | conditions   |   consommation | autonomie   |
|----:|:-----------|:---------|:-----------|:----------|:-------------|---------------:|:------------|
| 144 | Volkswagen | ID.3     | 58 kWh     | autoroute | hiver        |           27.7 | 205 km      |
| 145 | Volkswagen | ID.3     | 58 kWh     | route     | hiver        |           17.2 | 333 km      |
| 146 | Volkswagen | ID.3     | 58 kWh     | ville     | hiver        |           14.3 | 411 km      |
| 147 | Volkswagen | ID.3     | 58 kWh     | autoroute | été          |           21.3 | 273 km      |
| 148 | Volkswagen | ID.3     | 58 kWh     | route     | été          |           13.3 | 438 km      |
| 149 | Volkswagen | ID.3     | 58 kWh     | ville     | été          |           10.9 | 548 km      |
| 150 | Volkswagen | ID.4     | 77 kWh     | autoroute | hiver        |           32.1 | 243 km      |
| 151 | Volkswagen | ID.4     | 77 kWh     | route     | hiver        |           17.8 | 420 km      |
| 152 | Volkswagen | ID.4     | 77 kWh     | ville     | hiver        |           14.8 | 555 km      |
| 153 | Volkswagen | ID.4     | 77 kWh     | autoroute | été          |           24.7 | 311 km      |
| 154 | Volkswagen | ID.4     | 77 kWh     | route     | été          |           13.8 | 560 km      |
| 155 | Volkswagen | ID.4     | 77 kWh     | ville     | été          |           11.3 | 711 km      |
| 156 | Volkswagen | e-up!    | 36.8 kWh   | autoroute | hiver        |           27.3 | 119 km      |
| 157 | Volkswagen | e-up!    | 36.8 kWh   | route     | hiver        |           14.8 | 207 km      |
| 158 | Volkswagen | e-up!    | 36.8 kWh   | ville     | hiver        |           12.3 | 259 km      |
| 159 | Volkswagen | e-up!    | 36.8 kWh   | autoroute | été          |           21   | 154 km      |
| 160 | Volkswagen | e-up!    | 36.8 kWh   | route     | été          |           11.5 | 280 km      |
| 161 | Volkswagen | e-up!    | 36.8 kWh   | ville     | été          |            9.4 | 350 km      |

Le One Hot Encoder retourne une matrice de dimension 18 x 45. Soit autant de lignes que le jeux de données des Volkswagen, et autant de colonnes que les matrices transformées plus haut.

ohe.transform(df_VW[['marque','modèle']]).toarray()[:3,:]
array([[0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
        1., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
        1., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
        1., 0., 0., 0., 1., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.,
        0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]])

Que se passe-t-il avec .get_dummies() ?

| batterie   | route     | conditions   |   consommation | autonomie   |   marque_Volkswagen |   modèle_ID.3 |   modèle_ID.4 |   modèle_e-up! |
|:-----------|:----------|:-------------|---------------:|:------------|--------------------:|--------------:|--------------:|---------------:|
| 58 kWh     | autoroute | hiver        |           27.7 | 205 km      |                   1 |             1 |             0 |              0 |
| 58 kWh     | route     | hiver        |           17.2 | 333 km      |                   1 |             1 |             0 |              0 |
| 58 kWh     | ville     | hiver        |           14.3 | 411 km      |                   1 |             1 |             0 |              0 |

Les autres marques et les autres modèles sont absents du jeu de données. La fonction .get_dummies() ne peux pas en tenir compte. La dimension de la matrice transformée diffère de celle attendue par les modèles de predictions. C’est mort 💣

Et puis, implémenter le code pour la transformation inverse est un peu long … 😅

Transformer les données numériques textuelles

Les colonnes batterie et autonomie ne nécessitent pas de transformation complexe. Juste extraire le nombre contenu dans la colonne.

| batterie   | autonomie   |
|:-----------|:------------|
| 58 kWh     | 205 km      |
| 58 kWh     | 333 km      |
| 58 kWh     | 411 km      |

Pour extraire les données, cette simple ligne suffit :

df.autonomie = df.autonomie.str.extract(r'(\d+)').astype(int)

Décryptons-la :

  • .str est une méthode qui permet d’accéder à la Serie df.autonomie comme une liste de textes,
  • .extract(r'(\d+)') recherche l’expression régulière r'(\d+)' et capture le groupe correspondant (ici les chiffres consécutifs),
  • .astype(int) finit de convertir la série en données numériques.

Pour l’appliquer à plusieurs colonnes à la fois, la méthode apply est magique.

def extract_num_in_serie(serie):
    return serie.str.extract(r'(\d+)',expand=False).fillna(0).astype(int)

df[['batterie','autonomie']] = df[['batterie','autonomie']].apply(extract_num_in_serie)

Décryptons-la :

  • .fillna(0) remplace toutes les valeurs Nan de la série par zéro. Il y a d’autres comportements possibles, mais ce n’est pas l’objet de ce post 😜,
  • serie.str convertit la série en texte, avant de la passer à l’expression régulière,
  • .apply(extract_num_in_serie) execute la fonction extract_num_in_serie à chaque série du dataframe df[['batterie','autonomie']], donc batterie et autonomie.

Gagner du temps avec Pipeline et ColumnTransformer

Il existe quelques méthodes qui permettent de transformer entièrement un jeux de données en quelques lignes, de sauvegarder ce nouveau préprocesseur et de l’utiliser pour encoder et décoder à la volée. Voici un petit aperçu que je détaillerai bientôt :

from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder
from sklearn.impute import SimpleImputer

numeric_features = df.select_dtypes(exclude=['object']).columns
numeric_transformer = Pipeline(steps=[('imputer', SimpleImputer())])

categorical_features = df.select_dtypes(include=['object']).columns
categorical_transformer = Pipeline(steps=[('onehotencoder', OneHotEncoder(categories = "auto",sparse = False, drop = None, handle_unknown='ignore'))])

preprocessor = ColumnTransformer(n_jobs=-1,
      transformers=[
            ('num', numeric_transformer, numeric_features),
            ('cat', categorical_transformer, categorical_features)])
    
preprocessor.fit(df)

Puis

dataset = preprocessor.transform(df)
dataset[:5]

array([[65. , 35.4,  1. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,
         0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,
         0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,
         0. ,  0. ,  0. ,  1. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,
         0. ,  0. ,  0. ,  1. ,  0. ,  0. ,  1. ,  0. ],
       [65. , 23.9,  1. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,
         0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,
         0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,
         0. ,  0. ,  0. ,  1. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,
         0. ,  0. ,  0. ,  0. ,  1. ,  0. ,  1. ,  0. ],
       [65. , 19.9,  1. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,
         0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,
         0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,
         0. ,  0. ,  0. ,  1. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,
         0. ,  0. ,  0. ,  0. ,  0. ,  1. ,  1. ,  0. ],
       [65. , 27.2,  1. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,
         0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,
         0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,
         0. ,  0. ,  0. ,  1. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,
         0. ,  0. ,  0. ,  1. ,  0. ,  0. ,  0. ,  1. ],
       [65. , 18.5,  1. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,
         0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,
         0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,
         0. ,  0. ,  0. ,  1. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,  0. ,
         0. ,  0. ,  0. ,  0. ,  1. ,  0. ,  0. ,  1. ]])

“Et voilà!” (en anglais US dans le texte)

Dans ce post, tu as appris à reconnaitre les types de données présents dans ton jeux de données. Tu as appris à transformer ton jeux de données pour le rendre compatible avec les méthodes de classification, de regression et de clustering de scikit-learn. Tu as aussi appris à changer l’ordre des catégories ordonnées et à gérer le comportement des préprocesseurs en cas de données inconnues.

Dans le prochain, nous transformerons ce dataset pour entrainer un modèle de regression.

Congrats 🤩