TL; DR: Utiliser map+fillna pour grand di et utiliser replace pour petit di
1. Une alternative: np.select()
Si le dictionnaire remappant n’est pas trop grand, une autre option est numpy.select. La syntaxe de np.select nécessite des tableaux / listes séparés des conditions et des valeurs de remplacement, donc les clés et les valeurs de di doit être séparé.
import numpy as np
df['col1'] = np.select((df[['col1']].values == list(di)).T, di.values(), df['col1'])
NB si le dictionnaire remappant di est très grand, cela peut rencontrer des problèmes de mémoire car comme vous pouvez le voir sur la ligne de code ci-dessus, un tableau de forme booléen (len(df), len(di)) est nécessaire pour évaluer les conditions.
2 map+fillna contre replace. Quel est le meilleur?
Si nous regardons le code source, si un dictionnaire y est transmis, map est une méthode optimisée qui appelle un cython optimisé take_nd() fonction pour effectuer des remplacements et fillna() appels where() (une autre méthode optimisée) pour remplir les valeurs. D’autre part, replace() est implémenté dans Python et utilise une boucle sur le dictionnaire. Donc, si le dictionnaire est grand, replace peut potentiellement être des milliers de fois plus lent que map+fillna. Illustrons la différence par l’exemple suivant où une seule valeur (0) est remplacé dans la colonne (une en utilisant un dictionnaire de longueur 1000 (di1) et une autre en utilisant un dictionnaire de longueur 1 (di2)).
df = pd.DataFrame({'col1': range(1000)})
di1 = {k: k+1 for k in range(-1000, 1)}
di2 = {0: 1}
%timeit df['col1'].map(di1).fillna(df['col1'])
# 1.19 ms ± 6.77 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
%timeit df['col1'].replace(di1)
# 41.4 ms ± 400 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit df['col1'].map(di2).fillna(df['col1'])
# 691 µs ± 27.9 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
%timeit df['col1'].replace(di2)
# 157 µs ± 3.34 µs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)
Comme vous pouvez le voir, si len(di)==1000, replace est 35 fois plus lent, mais si len(di)==1c’est 4,5 fois plus rapide. Cet écart s’aggrave à mesure que la taille du dictionnaire remappant di augmente.
En fait, si nous regardons les parcelles de performance, nous pouvons faire les observations suivantes. Les parcelles ont été dessinées avec des paramètres particuliers fixés dans chaque graphique. Vous pouvez utiliser le code ci-dessous pour modifier la taille de DataFrame pour voir pour différents paramètres, mais il produira des tracés très similaires.
- Pour une dataframe donnée,
map+fillnafait des remplacements en temps presque constant, quelle que soit la taille du dictionnaire de remappage alors quereplacePire à mesure que la taille du dictionnaire de remappage augmente (tracé supérieur gauche). - Le pourcentage de valeurs remplacées dans le dataframe a très peu d’impact sur la différence d’exécution. L’impact de la durée de
dil’emporte complètement sur tout l’impact qu’il a (complot en haut à droite). - Pour un dictionnaire de remappage donné,
map+fillnafonctionne mieux quereplaceà mesure que la taille du dataframe augmente (tracé inférieur à gauche). - Encore, si
diest grand, la taille du dataframe n’a pas d’importance;map+fillnaest beaucoup plus rapide quereplace(Tableau du bas à droite).

Code utilisé pour produire les parcelles:
import numpy as np
import pandas as pd
from perfplot import plot
import matplotlib.pyplot as plt
kernels = [lambda df,di: df['col1'].replace(di),
lambda df,di: df['col1'].map(di).fillna(df['col1'])]
labels = ["replace", "map+fillna"]
# first plot
N, m = 100000, 20
plot(
setup=lambda n: (pd.DataFrame({'col1': np.resize(np.arange(m*n), N)}),
{k: (k+1)/2 for k in range(n)}),
kernels=kernels, labels=labels,
n_range=range(1, 21),
xlabel="Length of replacement dictionary",
title=f'Remapping values in a column (len(df)={N:,}, {100//m}% replaced)',
equality_check=pd.Series.equals)
_, xmax = plt.xlim()
plt.xlim((0.5, xmax+1))
plt.xticks(np.arange(1, xmax+1, 2));
# second plot
N, m = 100000, 1000
di = {k: (k+1)/2 for k in range(m)}
plot(
setup=lambda n: pd.DataFrame({'col1': np.resize(np.arange((n-100)*m//100, n*m//100), N)}),
kernels=kernels, labels=labels,
n_range=[1, 5, 10, 15, 25, 40, 55, 75, 100],
xlabel="Percentage of values replaced",
title=f'Remapping values in a column (len(df)={N:,}, len(di)={m})',
equality_check=pd.Series.equals);
# third plot
m, n = 10, 0.01
di = {k: (k+1)/2 for k in range(m)}
plot(
setup=lambda N: pd.DataFrame({'col1': np.resize(np.arange((n-1)*m, n*m), N)}),
kernels=kernels, labels=labels,
n_range=[2**k for k in range(6, 21)],
xlabel="Length of dataframe",
logy=False,
title=f'Remapping values in a column (len(di)={m}, {int(n*100)}% replaced)',
equality_check=pd.Series.equals);
# fourth plot
m, n = 100, 0.01
di = {k: (k+1)/2 for k in range(m)}
plot(
setup=lambda N: pd.DataFrame({'col1': np.resize(np.arange((n-1)*m, n*m), N)}),
kernels=kernels, labels=labels,
n_range=[2**k for k in range(6, 21)],
xlabel="Length of dataframe",
title=f'Remapping values in a column (len(di)={m}, {int(n*100)}% replaced)',
equality_check=pd.Series.equals);



