J'utilise Pandas dataframes et je veux créer une nouvelle colonne en fonction des colonnes existantes. Je n'ai pas vu une bonne discussion sur la différence de vitesse entre df.apply()
et np.vectorize()
, alors j'ai pensé poser la question ici.
La fonction Pandas apply()
] est lente. D'après ce que j'ai mesuré (illustré ci-dessous dans certaines expériences), utiliser np.vectorize()
est 25 fois plus rapide (ou plus) que Utilisation de la fonction DataFrame apply()
, au moins sur mon MacBook Pro 2016 S'agit-il d'un résultat attendu et pourquoi?
Par exemple, supposons que j'ai le dataframe suivant avec N
rows:
N = 10
A_list = np.random.randint(1, 100, N)
B_list = np.random.randint(1, 100, N)
df = pd.DataFrame({'A': A_list, 'B': B_list})
df.head()
# A B
# 0 78 50
# 1 23 91
# 2 55 62
# 3 82 64
# 4 99 80
Supposons en outre que je veuille créer une nouvelle colonne en fonction des deux colonnes A
et B
. Dans l'exemple ci-dessous, j'utiliserai une fonction simple divide()
. Pour appliquer la fonction, je peux utiliser soit df.apply()
ou np.vectorize()
:
def divide(a, b):
if b == 0:
return 0.0
return float(a)/b
df['result'] = df.apply(lambda row: divide(row['A'], row['B']), axis=1)
df['result2'] = np.vectorize(divide)(df['A'], df['B'])
df.head()
# A B result result2
# 0 78 50 1.560000 1.560000
# 1 23 91 0.252747 0.252747
# 2 55 62 0.887097 0.887097
# 3 82 64 1.281250 1.281250
# 4 99 80 1.237500 1.237500
Si j'augmente N
aux tailles réelles, telles que 1 million ou plus, j'observe que np.vectorize()
est 25 fois plus rapide ou plus que df.apply()
.
Vous trouverez ci-dessous un code de référence complet:
import pandas as pd
import numpy as np
import time
def divide(a, b):
if b == 0:
return 0.0
return float(a)/b
for N in [1000, 10000, 100000, 1000000, 10000000]:
print ''
A_list = np.random.randint(1, 100, N)
B_list = np.random.randint(1, 100, N)
df = pd.DataFrame({'A': A_list, 'B': B_list})
start_Epoch_sec = int(time.time())
df['result'] = df.apply(lambda row: divide(row['A'], row['B']), axis=1)
end_Epoch_sec = int(time.time())
result_apply = end_Epoch_sec - start_Epoch_sec
start_Epoch_sec = int(time.time())
df['result2'] = np.vectorize(divide)(df['A'], df['B'])
end_Epoch_sec = int(time.time())
result_vectorize = end_Epoch_sec - start_Epoch_sec
print 'N=%d, df.apply: %d sec, np.vectorize: %d sec' % \
(N, result_apply, result_vectorize)
# Make sure results from df.apply and np.vectorize match.
assert(df['result'].equals(df['result2']))
Les résultats sont montrés plus bas:
N=1000, df.apply: 0 sec, np.vectorize: 0 sec
N=10000, df.apply: 1 sec, np.vectorize: 0 sec
N=100000, df.apply: 2 sec, np.vectorize: 0 sec
N=1000000, df.apply: 24 sec, np.vectorize: 1 sec
N=10000000, df.apply: 262 sec, np.vectorize: 4 sec
Si np.vectorize()
est en général toujours plus rapide que df.apply()
, alors pourquoi np.vectorize()
n'est-il pas mentionné davantage? Je ne vois jamais que les publications StackOverflow liées à df.apply()
, telles que:
Les pandas créent une nouvelle colonne basée sur les valeurs des autres colonnes
Comment utiliser la fonction Pandas 'apply' sur plusieurs colonnes?
Comment appliquer une fonction à deux colonnes de Pandas dataframe
Je vais commencer en disant que la puissance des tableaux Pandas et NumPy est dérivée de la haute performance vectorisée calculs sur des tableaux numériques.1 Le but des calculs vectorisés est d'éviter les boucles au niveau Python en déplaçant les calculs vers un code C hautement optimisé et en utilisant des blocs de mémoire contigus.2
Maintenant, nous pouvons regarder quelques timings. Vous trouverez ci-dessous toutes les boucles de niveau Python qui produisent des objets pd.Series
, np.ndarray
Ou list
contenant les mêmes valeurs. Aux fins de l'affectation à une série dans une base de données, les résultats sont comparables.
# Python 3.6.5, NumPy 1.14.3, Pandas 0.23.0
np.random.seed(0)
N = 10**5
%timeit list(map(divide, df['A'], df['B'])) # 43.9 ms
%timeit np.vectorize(divide)(df['A'], df['B']) # 48.1 ms
%timeit [divide(a, b) for a, b in Zip(df['A'], df['B'])] # 49.4 ms
%timeit [divide(a, b) for a, b in df[['A', 'B']].itertuples(index=False)] # 112 ms
%timeit df.apply(lambda row: divide(*row), axis=1, raw=True) # 760 ms
%timeit df.apply(lambda row: divide(row['A'], row['B']), axis=1) # 4.83 s
%timeit [divide(row['A'], row['B']) for _, row in df[['A', 'B']].iterrows()] # 11.6 s
Quelques plats à emporter:
Tuple
(les 4 premières) sont un facteur plus efficace que les méthodes basées sur pd.Series
(Les 3 dernières).np.vectorize
, La compréhension de la liste + Zip
et map
méthodes, c’est-à-dire les 3 meilleurs, ont à peu près les mêmes performances. C'est parce qu'ils utilisent Tuple
et en contournent certains Pandas overhead de pd.DataFrame.itertuples
.raw=True
Avec pd.DataFrame.apply
Par rapport à sans amélioration est très rapide. Cette option alimente les tableaux NumPy avec la fonction personnalisée à la place des objets pd.Series
.pd.DataFrame.apply
: Juste une autre bouclePour voir exactement les objets Pandas circule, vous pouvez modifier votre fonction de manière triviale:
def foo(row):
print(type(row))
assert False # because you only need to see this once
df.apply(lambda row: foo(row), axis=1)
Sortie: <class 'pandas.core.series.Series'>
. Créer, transmettre et interroger un objet de la série Pandas entraîne des frais généraux importants par rapport aux tableaux NumPy. Cela ne devrait pas vous surprendre: Pandas incluent une quantité décente d'échafaudages contenir un index, des valeurs, des attributs, etc.
Répétez le même exercice avec raw=True
Et vous verrez <class 'numpy.ndarray'>
. Tout cela est décrit dans la documentation, mais le voir est plus convaincant.
np.vectorize
: Fausse vectorisationLa documentation pour np.vectorize
a la note suivante:
La fonction vectorisée évalue
pyfunc
sur des nuplets successifs des tableaux d'entrée tels que la fonction python map), sauf qu'elle utilise les règles de diffusion de numpy.
Les "règles de diffusion" sont sans importance ici, car les tableaux d'entrée ont les mêmes dimensions. Le parallèle avec map
est instructif, car la version map
ci-dessus a des performances presque identiques. Le code source montre ce qui se passe: np.vectorize
Convertit votre fonction d'entrée en un fonction universelle ("ufunc") via np.frompyfunc
. Il y a une optimisation, par exemple. la mise en cache, ce qui peut conduire à une amélioration des performances.
En bref, np.vectorize
Fait ce qu’une boucle de niveau Python devrait faire, mais pd.DataFrame.apply
Ajoute une surcharge épaisse. Il n'y a pas de compilation JIT que vous voyez avec numba
(voir ci-dessous). C'est juste une commodité .
Pourquoi les différences ci-dessus ne sont-elles mentionnées nulle part? Parce que les performances des calculs réellement vectorisés les rendent non pertinents:
%timeit np.where(df['B'] == 0, 0, df['A'] / df['B']) # 1.17 ms
%timeit (df['A'] / df['B']).replace([np.inf, -np.inf], 0) # 1.96 ms
Oui, c'est environ 40 fois plus rapide que la plus rapide des solutions ci-dessus. Les deux sont acceptables. À mon avis, le premier est succinct, lisible et efficace. Ne regardez que les autres méthodes, par exemple numba
ci-dessous, si les performances sont critiques et que cela fait partie de votre goulot d'étranglement.
numba.njit
: Une plus grande efficacitéLorsque les boucles are sont considérées comme viables, elles sont généralement optimisées via numba
avec les tableaux NumPy sous-jacents de manière à ce que le maximum possible soit déplacé vers C.
En effet, numba
améliore les performances de microsecondes. Sans travail fastidieux, il sera difficile d’être beaucoup plus efficace que cela.
from numba import njit
@njit
def divide(a, b):
res = np.empty(a.shape)
for i in range(len(a)):
if b[i] != 0:
res[i] = a[i] / b[i]
else:
res[i] = 0
return res
%timeit divide(df['A'].values, df['B'].values) # 717 µs
Utiliser @njit(parallel=True)
peut donner un nouvel élan aux tableaux de grande taille.
1 Les types numériques incluent: int
, float
, datetime
, bool
, category
. Ils excludeobject
dtype et peuvent être conservés dans des blocs de mémoire contigus.
2 Il existe au moins deux raisons pour lesquelles les opérations NumPy sont efficaces par rapport à Python:
Plus vos fonctions sont complexes (c'est-à-dire que moins numpy
peut se déplacer vers ses propres internes), plus vous verrez que la performance ne sera pas si différente. Par exemple:
name_series = pd.Series(np.random.choice(['adam', 'chang', 'eliza', 'odom'], replace=True, size=100000))
def parse_name(name):
if name.lower().startswith('a'):
return 'A'
Elif name.lower().startswith('e'):
return 'E'
Elif name.lower().startswith('i'):
return 'I'
Elif name.lower().startswith('o'):
return 'O'
Elif name.lower().startswith('u'):
return 'U'
return name
parse_name_vec = np.vectorize(parse_name)
Faire des timings:
tiliser Apply
%timeit name_series.apply(parse_name)
Résultats:
76.2 ms ± 626 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
En utilisant np.vectorize
%timeit parse_name_vec(name_series)
Résultats:
77.3 ms ± 216 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Numpy essaie de transformer python fonctions en numpy ufunc
objets lorsque vous appelez np.vectorize
. Comment cela se fait-il, je ne le sais pas vraiment - il faudrait creuser davantage dans les éléments internes de numpy que ce que je suis disposé à utiliser dans un guichet automatique. Cela dit, il semble faire ici un meilleur travail sur des fonctions simplement numériques que cette fonction basée sur des chaînes.
Cranking la taille jusqu'à 1.000.000:
name_series = pd.Series(np.random.choice(['adam', 'chang', 'eliza', 'odom'], replace=True, size=1000000))
apply
%timeit name_series.apply(parse_name)
Résultats:
769 ms ± 5.88 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
np.vectorize
%timeit parse_name_vec(name_series)
Résultats:
794 ms ± 4.85 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Un meilleur moyen ( vectorisé ) avec np.select
:
cases = [
name_series.str.lower().str.startswith('a'), name_series.str.lower().str.startswith('e'),
name_series.str.lower().str.startswith('i'), name_series.str.lower().str.startswith('o'),
name_series.str.lower().str.startswith('u')
]
replacements = 'A E I O U'.split()
Horaires:
%timeit np.select(cases, replacements, default=name_series)
Résultats:
67.2 ms ± 683 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)