Hashing et catégories

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

Le hashing est utilise lorsque le nombre de catégories est trop grand.

%matplotlib inline

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

import pandas
import numpy
prob = numpy.ones(100) / 100
rnd = numpy.random.binomial(100, 0.5, 10000)
df = pandas.DataFrame(dict(cat_int=rnd, cat_text=['cat%d' % i for i in rnd]))
df.head()
cat_int cat_text
0 51 cat51
1 51 cat51
2 53 cat53
3 49 cat49
4 56 cat56
dfc = df.drop('cat_int', axis=1).copy()
dfc['count'] = 1
gr = dfc.groupby('cat_text').sum().sort_values('count', ascending=False)
pandas.concat([gr.head(), gr.tail()])
count
cat_text
cat48 780
cat49 771
cat50 770
cat52 755
cat51 746
cat67 3
cat31 2
cat32 1
cat33 1
cat69 1
len(set(df.cat_text))
38

On utilise le module Category Encoders implémente la classe Hashing qui applique une fonction de hash pour retourner une séquence de nombre binaires. Il y a 100 catégories distinctes. Il faut au pire 8 colonnes binaires pour représenter l’information. On vérifie avec un encoder binaire qui encode chaque catégorie de façon binaire.

from category_encoders import BinaryEncoder
BinaryEncoder(cols=['cat_text']).fit_transform(df).head()
cat_text_0 cat_text_1 cat_text_2 cat_text_3 cat_text_4 cat_text_5 cat_int
0 0 0 0 0 0 0 51
1 0 0 0 0 0 0 51
2 0 0 0 0 0 1 53
3 0 0 0 0 1 0 49
4 0 0 0 0 1 1 56
from category_encoders import HashingEncoder
HashingEncoder(cols=['cat_text']).fit_transform(df).head()
col_0 col_1 col_2 col_3 col_4 col_5 col_6 col_7 cat_int
0 0 0 0 0 0 0 0 1 51
1 0 0 0 0 0 0 0 1 51
2 0 0 0 0 1 0 0 0 53
3 0 0 0 1 0 0 0 0 49
4 0 0 1 0 0 0 0 0 56

On peut réduire le nombre de colonnes. L’information est compressée mais pas de façon réversible. L’information est codée sur 4 colonnes.

res = HashingEncoder(cols=['cat_text'], n_components=4, hash_method="sha256").fit_transform(df)
res.head()
col_0 col_1 col_2 col_3 cat_int
0 0 1 0 0 51
1 0 1 0 0 51
2 0 0 0 1 53
3 0 0 0 1 49
4 0 1 0 0 56
res['col_int'] = res.col_0 + 2 * res.col_1 + 4 * res.col_2 + 8 * res.col_3
res.head()
col_0 col_1 col_2 col_3 cat_int col_int
0 0 1 0 0 51 2
1 0 1 0 0 51 2
2 0 0 0 1 53 8
3 0 0 0 1 49 8
4 0 1 0 0 56 2
res[['col_int', 'cat_int']].groupby('col_int').count()
cat_int
col_int
1 3913
2 2021
4 2
8 4064

Pas tout-à-fait ce à quoi je m’attendais. On peut aussi utiliser quelque chose comme ceci : hash + encoding binaire.

dfc = df.copy()
dfc['cat_hash'] = df["cat_text"].apply(lambda x: abs(hash(x)) % (2**4))
res = BinaryEncoder(cols=['cat_hash']).fit_transform(dfc)
res.head()
cat_hash_0 cat_hash_1 cat_hash_2 cat_hash_3 cat_int cat_text
0 0 0 0 0 51 cat51
1 0 0 0 0 51 cat51
2 0 0 0 1 53 cat53
3 0 0 1 0 49 cat49
4 0 0 1 1 56 cat56
res['col_int'] = res.cat_hash_0 + 2 * res.cat_hash_1 + 4 * res.cat_hash_2 + 8 * res.cat_hash_3
res.head()
cat_hash_0 cat_hash_1 cat_hash_2 cat_hash_3 cat_int cat_text col_int
0 0 0 0 0 51 cat51 0
1 0 0 0 0 51 cat51 0
2 0 0 0 1 53 cat53 8
3 0 0 1 0 49 cat49 4
4 0 0 1 1 56 cat56 12
res[['col_int', 'cat_int', 'cat_text']].groupby(['col_int', 'cat_text'], as_index=False) \
        .count().pivot('cat_text', 'col_int', 'cat_int').astype(str).replace("nan", "")
col_int 0 1 2 3 4 5 6 8 9 10 11 12 13 14
cat_text
cat31 2.0
cat32 1.0
cat33 1.0
cat34 6.0
cat35 9.0
cat36 13.0
cat37 23.0
cat38 45.0
cat39 65.0
cat40 101.0
cat41 168.0
cat42 196.0
cat43 314.0
cat44 373.0
cat45 486.0
cat46 583.0
cat47 691.0
cat48 780.0
cat49 771.0
cat50 770.0
cat51 746.0
cat52 755.0
cat53 668.0
cat54 598.0
cat55 473.0
cat56 404.0
cat57 303.0
cat58 220.0
cat59 162.0
cat60 105.0
cat61 68.0
cat62 43.0
cat63 23.0
cat64 17.0
cat65 9.0
cat66 4.0
cat67 3.0
cat69 1.0

Ce qu’on espère : que deux classes sur-représentées ne soient pas encodées par la même valeur, ce qui est le cas ici. Il faudra donc augmenter la taille du hash (16 ici).