J'utilise souvent les méthodes Pandas mask
et where
pour une logique plus claire lors de la mise à jour des valeurs d'une série Cependant, pour le code relativement critique pour les performances, je constate une baisse significative des performances par rapport à numpy.where
.
Bien que je sois heureux d'accepter cela pour des cas spécifiques, je suis intéressé de savoir:
mask
/where
offrent-elles des fonctionnalités supplémentaires, à part inplace
/errors
/try-cast
paramètres? Je comprends ces 3 paramètres mais les utilise rarement. Par exemple, je n'ai aucune idée de ce à quoi fait référence le paramètre level
.mask
/where
surpasse numpy.where
? Si un tel exemple existe, il pourrait influencer la façon dont je choisirai les méthodes appropriées pour aller de l'avant.Pour référence, voici quelques analyses comparatives sur Pandas 0.19.2/Python 3.6.0:
np.random.seed(0)
n = 10000000
df = pd.DataFrame(np.random.random(n))
assert (df[0].mask(df[0] > 0.5, 1).values == np.where(df[0] > 0.5, 1, df[0])).all()
%timeit df[0].mask(df[0] > 0.5, 1) # 145 ms per loop
%timeit np.where(df[0] > 0.5, 1, df[0]) # 113 ms per loop
Les performances semblent diverger davantage pour les valeurs non scalaires:
%timeit df[0].mask(df[0] > 0.5, df[0]*2) # 338 ms per loop
%timeit np.where(df[0] > 0.5, df[0]*2, df[0]) # 153 ms per loop
J'utilise pandas 0.23.3 et Python 3.6, donc je peux voir une vraie différence de temps d'exécution uniquement pour votre deuxième exemple).
Mais examinons une version légèrement différente de votre deuxième exemple (afin que nous éliminions 2*df[0]
). Voici notre référence sur ma machine:
twice = df[0]*2
mask = df[0] > 0.5
%timeit np.where(mask, twice, df[0])
# 61.4 ms ± 1.51 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit df[0].mask(mask, twice)
# 143 ms ± 5.27 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
La version de Numpy est environ 2,3 fois plus rapide que les pandas.
Alors profilons les deux fonctions pour voir la différence - le profilage est un bon moyen d'avoir une vue d'ensemble quand on n'est pas très familier avec la base du code: c'est plus rapide que le débogage et moins sujet aux erreurs que d'essayer de comprendre ce qui se passe juste en lisant le code.
Je suis sous Linux et j'utilise perf
. Pour la version de numpy nous obtenons (pour la liste, voir l'annexe A):
>>> perf record python np_where.py
>>> perf report
Overhead Command Shared Object Symbol
68,50% python multiarray.cpython-36m-x86_64-linux-gnu.so [.] PyArray_Where
8,96% python [unknown] [k] 0xffffffff8140290c
1,57% python mtrand.cpython-36m-x86_64-linux-gnu.so [.] rk_random
Comme nous pouvons le voir, la part du lion du temps est consacrée à PyArray_Where
- environ 69%. Le symbole inconnu est une fonction du noyau (en fait clear_page
) - Je cours sans privilèges root donc le symbole n'est pas résolu.
Et pour pandas nous obtenons (voir l'annexe B pour le code):
>>> perf record python pd_mask.py
>>> perf report
Overhead Command Shared Object Symbol
37,12% python interpreter.cpython-36m-x86_64-linux-gnu.so [.] vm_engine_iter_task
23,36% python libc-2.23.so [.] __memmove_ssse3_back
19,78% python [unknown] [k] 0xffffffff8140290c
3,32% python umath.cpython-36m-x86_64-linux-gnu.so [.] DOUBLE_isnan
1,48% python umath.cpython-36m-x86_64-linux-gnu.so [.] BOOL_logical_not
Une situation assez différente:
PyArray_Where
sous le capot - le consommateur de temps le plus important est vm_engine_iter_task
, qui est fonctionnalité numexpr .__memmove_ssse3_back
utilise environ 25
% du temps! Probablement, certaines des fonctions du noyau sont également connectées à des accès à la mémoire.En fait, pandas-0.19 utilisait PyArray_Where
Sous le capot, pour l'ancienne version, le rapport de performance ressemblerait à:
Overhead Command Shared Object Symbol
32,42% python multiarray.so [.] PyArray_Where
30,25% python libc-2.23.so [.] __memmove_ssse3_back
21,31% python [kernel.kallsyms] [k] clear_page
1,72% python [kernel.kallsyms] [k] __schedule
Donc, fondamentalement, il utiliserait np.where
Sous le capot + quelques frais généraux (tous ci-dessus la copie de données, voir __memmove_ssse3_back
) À l'époque.
Je ne vois aucun scénario où pandas pourrait devenir plus rapide que numpy dans la version 0.19 de pandas - cela ne fait qu'ajouter des frais généraux à la fonctionnalité de numpy. La version 0.23.3 de Pandas est une toute autre histoire - ici module numexpr est utilisé, il est très possible qu'il existe des scénarios pour lesquels la version de pandas est (au moins légèrement) plus rapide.
Je ne suis pas sûr que cette copie de mémoire soit vraiment demandée/nécessaire - peut-être pourrait-on même appeler cela un bug de performance, mais je n'en sais pas assez pour en être certain.
Nous pourrions aider pandas à ne pas copier, en retirant quelques indirections (en passant np.array
Au lieu de pd.Series
). Par exemple:
%timeit df[0].mask(mask.values > 0.5, twice.values)
# 75.7 ms ± 1.5 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
Maintenant, pandas est seulement 25% plus lent. Le perf dit:
Overhead Command Shared Object Symbol
50,81% python interpreter.cpython-36m-x86_64-linux-gnu.so [.] vm_engine_iter_task
14,12% python [unknown] [k] 0xffffffff8140290c
9,93% python libc-2.23.so [.] __memmove_ssse3_back
4,61% python umath.cpython-36m-x86_64-linux-gnu.so [.] DOUBLE_isnan
2,01% python umath.cpython-36m-x86_64-linux-gnu.so [.] BOOL_logical_not
Beaucoup moins de copie de données, mais toujours plus que dans la version numphy qui est principalement responsable des frais généraux.
Mes principaux enseignements:
pandas a le potentiel d'être au moins légèrement plus rapide que numpy (car il est possible d'être plus rapide). Cependant, la gestion quelque peu opaque des pandas de la copie de données rend difficile de prédire quand ce potentiel est éclipsé par la copie de données (inutile).
lorsque les performances de where
/mask
sont le goulot d'étranglement, j'utiliserais numba/cython pour améliorer les performances - voir mes tentatives plutôt naïves d'utiliser numba et cython plus loin.
L'idée est de prendre
np.where(df[0] > 0.5, df[0]*2, df[0])
et pour éliminer la nécessité de créer une version temporaire, c'est-à-dire df[0]*2
.
Comme proposé par @ max9111, en utilisant numba:
import numba as nb
@nb.njit
def nb_where(df):
n = len(df)
output = np.empty(n, dtype=np.float64)
for i in range(n):
if df[i]>0.5:
output[i] = 2.0*df[i]
else:
output[i] = df[i]
return output
assert(np.where(df[0] > 0.5, twice, df[0])==nb_where(df[0].values)).all()
%timeit np.where(df[0] > 0.5, df[0]*2, df[0])
# 85.1 ms ± 1.61 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit nb_where(df[0].values)
# 17.4 ms ± 673 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Ce qui est environ 5 fois plus rapide que la version numpy!
Et voici mon essai beaucoup moins réussi d'améliorer les performances avec l'aide de Cython:
%%cython -a
cimport numpy as np
import numpy as np
cimport cython
@cython.boundscheck(False)
@cython.wraparound(False)
def cy_where(double[::1] df):
cdef int i
cdef int n = len(df)
cdef np.ndarray[np.float64_t] output = np.empty(n, dtype=np.float64)
for i in range(n):
if df[i]>0.5:
output[i] = 2.0*df[i]
else:
output[i] = df[i]
return output
assert (df[0].mask(df[0] > 0.5, 2*df[0]).values == cy_where(df[0].values)).all()
%timeit cy_where(df[0].values)
# 66.7± 753 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
accélère de 25%. Je ne sais pas pourquoi le cython est tellement plus lent que le numba.
Annonces:
A: np_where.py:
import pandas as pd
import numpy as np
np.random.seed(0)
n = 10000000
df = pd.DataFrame(np.random.random(n))
twice = df[0]*2
for _ in range(50):
np.where(df[0] > 0.5, twice, df[0])
B: pd_mask.py:
import pandas as pd
import numpy as np
np.random.seed(0)
n = 10000000
df = pd.DataFrame(np.random.random(n))
twice = df[0]*2
mask = df[0] > 0.5
for _ in range(50):
df[0].mask(mask, twice)