Python – Valeurs de remap dans la colonne Pandas avec un dict, préservez les NANS

Sommaire :

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.

À LIRE  6 types de clementines: un aperçu (avec des images)

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+fillna fait des remplacements en temps presque constant, quelle que soit la taille du dictionnaire de remappage alors que replace Pire à 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 di l’emporte complètement sur tout l’impact qu’il a (complot en haut à droite).
  • Pour un dictionnaire de remappage donné, map+fillna fonctionne mieux que replace à mesure que la taille du dataframe augmente (tracé inférieur à gauche).
  • Encore, si di est grand, la taille du dataframe n’a pas d’importance; map+fillna est beaucoup plus rapide que replace (Tableau du bas à droite).

perflot

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);

Vous voulez suivre notre blog ?

Recevez nos conseils les plus précieux dans votre boîte de réception, une fois par mois !

Articles associés