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