Traitement amélioré des catégories

Links: notebook, html, PDF, python, slides, GitHub

Ce notebook présenté des encoding différents de ceux implémentées dans scikit-learn.

from jyquickhelper import add_notebook_menu
add_notebook_menu()
%matplotlib inline

On construit un jeu très simple avec deux catégories, une entière, une au format texte.

import pandas
import numpy
df = pandas.DataFrame(dict(cat_int=[10, 20, 10, 39, 10, 10, numpy.nan],
                          cat_text=['catA', 'catB', 'catA', 'catDD', 'catB', numpy.nan, 'catB']))
df
cat_int cat_text
0 10.0 catA
1 20.0 catB
2 10.0 catA
3 39.0 catDD
4 10.0 catB
5 10.0 NaN
6 NaN catB

Une API un peu différente

Le module Category Encoders implémente d’autres options avec une API un peu différente puisqu’il est possible de spécifier la colonne sur laquelle s’applique l’encoding.

from category_encoders import OneHotEncoder
OneHotEncoder(cols=['cat_text']).fit_transform(df)
cat_text_1 cat_text_2 cat_text_3 cat_text_4 cat_text_-1 cat_int
0 1 0 0 0 0 10.0
1 0 1 0 0 0 20.0
2 1 0 0 0 0 10.0
3 0 0 1 0 0 39.0
4 0 1 0 0 0 10.0
5 0 0 0 1 0 10.0
6 0 1 0 0 0 NaN

Autres options

import category_encoders
encoders = []
for k, enc in category_encoders.__dict__.items():
    if 'Encoder' in k:
        encoders.append(enc)
encoders
[category_encoders.backward_difference.BackwardDifferenceEncoder,
 category_encoders.binary.BinaryEncoder,
 category_encoders.hashing.HashingEncoder,
 category_encoders.helmert.HelmertEncoder,
 category_encoders.one_hot.OneHotEncoder,
 category_encoders.ordinal.OrdinalEncoder,
 category_encoders.sum_coding.SumEncoder,
 category_encoders.polynomial.PolynomialEncoder,
 category_encoders.basen.BaseNEncoder,
 category_encoders.leave_one_out.LeaveOneOutEncoder,
 category_encoders.target_encoder.TargetEncoder,
 category_encoders.woe.WOEEncoder]
dfi = df[['cat_text']].copy()
dfi["copy"] = dfi['cat_text']
for encoder in encoders:
    if 'Leave' in encoder.__name__ or \
       'Target' in encoder.__name__ or \
       'WOE' in encoder.__name__:
        continue
    enc = encoder(cols=['cat_text'])
    try:
        out = enc.fit_transform(dfi)
    except Exception as e:
        print("Issue with '{0}' due to {1}".format(encoder.__name__, e))
        continue
    print('-----', encoder.__name__)
    print(out)
    print('-----')
----- BackwardDifferenceEncoder
   intercept  cat_text_0  cat_text_1  cat_text_2   copy
0          1       -0.75        -0.5       -0.25   catA
1          1        0.25        -0.5       -0.25   catB
2          1       -0.75        -0.5       -0.25   catA
3          1        0.25         0.5       -0.25  catDD
4          1        0.25        -0.5       -0.25   catB
5          1        0.25         0.5        0.75    NaN
6          1        0.25        -0.5       -0.25   catB
-----
----- BinaryEncoder
   cat_text_0  cat_text_1  cat_text_2   copy
0           0           0           1   catA
1           0           1           0   catB
2           0           0           1   catA
3           0           1           1  catDD
4           0           1           0   catB
5           1           0           0    NaN
6           0           1           0   catB
-----
----- HashingEncoder
   col_0  col_1  col_2  col_3  col_4  col_5  col_6  col_7   copy
0      0      0      0      0      0      0      1      0   catA
1      0      0      0      0      0      0      0      1   catB
2      0      0      0      0      0      0      1      0   catA
3      1      0      0      0      0      0      0      0  catDD
4      0      0      0      0      0      0      0      1   catB
5      1      0      0      0      0      0      0      0    NaN
6      0      0      0      0      0      0      0      1   catB
-----
----- HelmertEncoder
   intercept  cat_text_0  cat_text_1  cat_text_2   copy
0          1        -1.0        -1.0        -1.0   catA
1          1         1.0        -1.0        -1.0   catB
2          1        -1.0        -1.0        -1.0   catA
3          1         0.0         2.0        -1.0  catDD
4          1         1.0        -1.0        -1.0   catB
5          1         0.0         0.0         3.0    NaN
6          1         1.0        -1.0        -1.0   catB
-----
----- OneHotEncoder
   cat_text_1  cat_text_2  cat_text_3  cat_text_4  cat_text_-1   copy
0           1           0           0           0            0   catA
1           0           1           0           0            0   catB
2           1           0           0           0            0   catA
3           0           0           1           0            0  catDD
4           0           1           0           0            0   catB
5           0           0           0           1            0    NaN
6           0           1           0           0            0   catB
-----
----- OrdinalEncoder
   cat_text   copy
0         1   catA
1         2   catB
2         1   catA
3         3  catDD
4         2   catB
5         4    NaN
6         2   catB
-----
----- SumEncoder
   intercept  cat_text_0  cat_text_1  cat_text_2   copy
0          1         1.0         0.0         0.0   catA
1          1         0.0         1.0         0.0   catB
2          1         1.0         0.0         0.0   catA
3          1         0.0         0.0         1.0  catDD
4          1         0.0         1.0         0.0   catB
5          1        -1.0        -1.0        -1.0    NaN
6          1         0.0         1.0         0.0   catB
-----
----- PolynomialEncoder
   intercept  cat_text_0  cat_text_1  cat_text_2   copy
0          1   -0.670820         0.5   -0.223607   catA
1          1   -0.223607        -0.5    0.670820   catB
2          1   -0.670820         0.5   -0.223607   catA
3          1    0.223607        -0.5   -0.670820  catDD
4          1   -0.223607        -0.5    0.670820   catB
5          1    0.670820         0.5    0.223607    NaN
6          1   -0.223607        -0.5    0.670820   catB
-----
----- BaseNEncoder
   cat_text_0  cat_text_1  cat_text_2   copy
0           0           0           1   catA
1           0           1           0   catB
2           0           0           1   catA
3           0           1           1  catDD
4           0           1           0   catB
5           1           0           0    NaN
6           0           1           0   catB
-----

Utilisation de la cible

Certains encoding optimise l’encoding en fonction de la cible à prédire lors d’un apprentissage supervisé. Les deux encoders suivant prédisent la cible en fonction de la catégorie ou essayent d’optimiser l’encoding de la catégorie en fonction de la cible à prédire. En particulier, l’encoder LeaveOneOut associe à chaque modéalité la moyenne des valeurs observées sur une autre colonne pour chaque ligne associée à cette modalité.

dfy = df.sort_values('cat_text').reset_index(drop=True).copy()
dfy['cat_text_copy'] = dfy['cat_text']
dfy['y'] = dfy.index * dfy.index + 10
dfy['y_copy'] = dfy.y
dfy
cat_int cat_text cat_text_copy y y_copy
0 10.0 catA catA 10 10
1 10.0 catA catA 11 11
2 20.0 catB catB 14 14
3 10.0 catB catB 19 19
4 NaN catB catB 26 26
5 39.0 catDD catDD 35 35
6 10.0 NaN NaN 46 46
categories = dfy.drop('y', axis=1)
label = dfy.y
binary_label = label == 10  # dummy one

for encoder in encoders:
    enc = encoder(cols=['cat_text'])
    try:
        out = enc.fit_transform(categories)
    except Exception as e:
        out = pandas.DataFrame()
    try:
        outy = enc.fit_transform(categories, label)
    except ValueError as e:
        if "must be binary" not in str(e):
            continue
        outy = enc.fit_transform(categories, binary_label)
    if not out.equals(outy):
        print('-----', encoder.__name__)
        print(outy)
        print('-----')
----- LeaveOneOutEncoder
   cat_int  cat_text cat_text_copy  y_copy
0     10.0      11.0          catA      10
1     10.0      10.0          catA      11
2     20.0      22.5          catB      14
3     10.0      20.0          catB      19
4      NaN      16.5          catB      26
5     39.0      23.0         catDD      35
6     10.0      23.0           NaN      46
-----
----- TargetEncoder
   cat_int   cat_text cat_text_copy  y_copy
0     10.0  13.861768          catA      10
1     10.0  13.861768          catA      11
2     20.0  20.064010          catB      14
3     10.0  20.064010          catB      19
4      NaN  20.064010          catB      26
5     39.0  23.000000         catDD      35
6     10.0  23.000000           NaN      46
-----
----- WOEEncoder
   cat_int  cat_text cat_text_copy  y_copy
0     10.0  0.980829          catA      10
1     10.0  0.980829          catA      11
2     20.0 -0.405465          catB      14
3     10.0 -0.405465          catB      19
4      NaN -0.405465          catB      26
5     39.0  0.000000         catDD      35
6     10.0  0.000000           NaN      46
-----

A propos du LeaveOneOut

Cet encoder ne produit qu’une seule colonne. Dans le cas d’une régression linéaire, la valeur est la moyenne de la cible y sur l’ensemble des lignes associées à cette catégorie. On reprend un exemple déjà utilisé.

perm = numpy.random.permutation(list(range(10)))
n = 1000
X1 = numpy.random.randint(0, 10, (n,1))
X2 = numpy.array([perm[i] for i in X1])
eps = numpy.random.random((n, 1))
Y = X1 * (-10) - 7 + eps
data = pandas.DataFrame(dict(X1=X1.ravel(), X2=X2.ravel(), Y=Y.ravel()))
data.head()
X1 X2 Y
0 7 3 -76.045854
1 3 4 -36.468473
2 2 7 -26.752507
3 0 2 -6.294129
4 4 0 -46.812676
from sklearn.model_selection import train_test_split
data_train, data_test = train_test_split(data)
data_train = data_train.reset_index(drop=True)
from category_encoders import LeaveOneOutEncoder
le = LeaveOneOutEncoder(cols=['X2'])
X = data_train.drop('Y', axis=1)
le.fit(X, data_train.Y)
data_train2 = le.transform(X)
data_train2.head()
X1 X2
0 9 -96.513580
1 9 -96.513580
2 8 -86.548834
3 7 -76.475048
4 9 -96.513580
data_train2 = data_train2.reset_index(drop=True)
from sklearn.linear_model import LinearRegression
model = LinearRegression()
model.fit(data_train2[["X2"]], data_train['Y'])
LinearRegression(copy_X=True, fit_intercept=True, n_jobs=None,
         normalize=False)
data_test = data_test.reset_index(drop=True)
from sklearn.metrics import r2_score
data_test2 = le.transform(data_test.drop("Y", axis=1))
r2_score(data_test['Y'], model.predict(data_test2[['X2']]))
0.9999033020848935

Le coefficient R^2 est proche de 1, la régression est quasi parfaite. L’encodeur LeaveONeOutEncode utilise la cible pour maximiser le coefficient R^2 si le modèle utilisé pour prédire est une régression linéaire. Vous trouverez une idée de la démonstration dans cet énoncé : ENSAE TD noté 2016.