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 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 est proche de 1, la régression est quasi
parfaite. L’encodeur LeaveONeOutEncode
utilise la cible pour
maximiser le coefficient 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.