
L’utilisation des « category » avec Pandas
On a tendance à dire que le prix de la mémoire est tellement faible qu’il n’est pas toujours nécessaire d’optimiser ses programmes Python. Sur les différents posts Linkedin qui traitaient de ce sujet, on m’a fait remarquer, et à juste titre, que les data-analysts (DA) et data-scientist (DS) ne sont pas des développeurs. Il faut le reconnaître, c’est tout à fait vrai ! Ce qui peut être une obsession pour le développeur peut n’être qu’un contrainte pour une DA/DS.
Mais il faut admettre que l’optimisation, sur de gros dataframe, permet :
- d’avoir des temps de réponse plus court
- donc de conserver plus facilement sa concentration
- et donc de rentrer plus tôt le soir à la maison !
Quel gain représente une category ?
En général, ce qui coûte du temps, c’est le volume d’information à traiter. Que ce soit dans un dataframe, une base de donnée ou même un post-it, il est plus long de lire 10 fois un même mot que de le lire une fois et de savoir qu’il est écrit 10 fois.
L’utilisation des category est fortement recommandée quand il y a peu de cardinalité. Par exemple, pour le sexe, nous avons homme/femme. Pour le type de consommateur nous avons client/prospect. Soit très de valeurs différentes.
Pour aider à la compréhension, je vais faire le parallèle avec le SQL. Nous allons créé 2 dataframes :
- Le premier contient le prénom et un code représentant la civilité
- Le second est une table de référence code <-> civilité
La category ne fonctionne pas ainsi, mais plutôt comme un index SQL mais cette représentation aide plus à la compréhension.
import pandas as pd
data = {
"salutation": [1, 1, 2],
"firstname": ["Jean", "Pierre", "Julie"]
}
ref = {
"salutation": [ 1, 2],
"value": ["Monsieur", "Madame"]
}
df_data = pd.DataFrame(data)
df_ref = pd.DataFrame(ref)

Si nous utilisons un merge pour joindre ces 2 dataframes, nous obtenons le même résultat qui si nous n’en avions qu’un seul. Par contre, notre version à 2 dataframes et beaucoup plus compacte, surtout si nous avons des milliers d’enregistrements.
pd.merge(df_ref, df_data, on="salutation")

Vous commencez à comprendre la logique ? Tant mieux car Pandas fonctionne à peu de chose près de la même façon. La seule différence, c’est qu’il fait le travail à votre place. En effet, il suffit de typer la colonne en category et Pandas organisera les cardinalités des façons optimales.
Les chaînes de caractères sont stockées dans des objects, gourmands par nature. Plus vos dataframes sont grands, plus l’empreinte mémoire augmente. Plus l’empreinte mémoire augmente, plus il y a d’informations à traiter. La category est, quant à elle optimisée, pour occuper moins de mémoire grâce à son système de pointeurs/références qui s’appuie sur des l’integer beaucoup moins gourmand en mémoire. Moins de mémoire occupée, mois d’informations à traiter.
J’ai piqué votre curiosité ? Tant mieux, vous pouvez aller consulter la documentation Pandas sur ce sujet.
C’est bien joli tout ça, mais est ce vraiment si significatif ?
Le test !
Création du dataframe
Nous allons créé un dataframe de 10 millions lignes avec les 3 colonnes suivantes :
- mot1 contiendra une valeur parmi titi, toto, tutu, tata (on ne rigole pas dans le fond svp)
- bool1 contiendra True or False
- val1 contiendra un integer entre 0 et 1000
import pandas as pd
import random
from time import perf_counter
mot1=[]; bool1=[]; val1=[]
for i in range(10_000_000):
mot1.append(random.choice(["titi","toto","tutu","tata"]))
bool1.append(random.choice([True, False]))
val1.append(random.randint(1,1000))
df=pd.DataFrame({"mot1": mot1, "bool1": bool1, "val1": val1})
df.info(verbose=True, memory_usage=True, max_cols=True, show_counts=True)

Le dataframe occupe 162M en mémoire. Pandas type de façon relativement logique :
- mot1 ne contient que des mots. Il utilise dont le type object;
- bool1 ne contenant que True et False, quoi de mieux qu’un boolean ?
- val1 est toujours un entier : int64
Si nous regardons les cardinalités de mot1 et bool1, nous avons :


Soit 4 valeurs possibles pour mott1 et 2 valeurs possibles pour bool1. Tout a l’air au mieux.
Les tests sans typage particulier
Les value_counts
t_start = perf_counter()
print(df.mot1.value_counts())
t_finish = perf_counter()
print("Temps de calcul : ",t_finish-t_start )

t_start = perf_counter()
print(df.bool1.value_counts())
t_finish = perf_counter()
print("Temps de calcul : ",t_finish-t_start )

Le pivot
t_start = perf_counter()
print(df.pivot_table(index="bool1",
columns="mot1",
values="val1",
aggfunc="sum"))
t_finish = perf_counter()
print("Temps de calcul:",t_finish-t_start)

La sélection simple
t_start = perf_counter()
copy = df.loc[df["mot1"]=="toto"]
t_finish = perf_counter()
print("Temps de calcul : ",t_finish-t_start )

Les temps de calcul sont plus qu’honorable, surtout pour 10 millions d’enregistrements. Mais que se passe t’il si nous typons mot1 en category ? Quel sera l’impact sur la taille du dataframe et sur le temps des calculs ?
Je type, tu types, nous typons
df = df.astype({"mot1": "category"})
print(df.dtypes)
Nous ne changeons que le type de mot1 via la fonction astype(). La propriété dtypes nous confirme le changement de type.

Voyant ce qu’il en est pour la taille du dataframe :

On passe de 162 à 95 Mo, ce qui semble plus que normal lorsqu’on a compris la mécanique. Voyons maintenant si la cure d’amaigrissement permet d’être plus rapide.

Les résultats sont sans appel !
En conclusion
Le bénéfice est incontestable. Plus rapide, empreinte mémoire plus faible. Cela peut paraître peu mais notre dataframe est simple et nous avons réalisé peu de calculs. Lors de la lecture du code, le type category est une information en soit. Elle indique un nombre de cardinalité faible sur la colonne.
Les ressources
La documentation Pandas qui traite des category