Ich verwende Pandas-Datenrahmen und möchte eine neue Spalte als Funktion vorhandener Spalten erstellen. Ich habe keine gute Diskussion über den Geschwindigkeitsunterschied zwischen df.apply()
und gesehen np.vectorize()
, also dachte ich, ich würde hier fragen.
Die Pandas- apply()
Funktion ist langsam. np.vectorize()
Nach dem apply()
, was ich gemessen habe (siehe unten in einigen Experimenten), ist die Verwendung 25x schneller (oder mehr) als die Verwendung der DataFrame-Funktion , zumindest auf meinem 2016 MacBook Pro. Ist das ein erwartetes Ergebnis und warum?
Angenommen, ich habe den folgenden Datenrahmen mit N
Zeilen:
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
Angenommen, ich möchte eine neue Spalte als Funktion der beiden Spalten A
und erstellen B
. Im folgenden Beispiel verwende ich eine einfache Funktion divide()
. Um die Funktion anzuwenden, kann ich entweder df.apply()
oder verwenden 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
Wenn ich mich N
auf reale Größen wie 1 Million oder mehr erhöhe , np.vectorize()
stelle ich fest, dass dies 25x schneller oder mehr ist als df.apply()
.
Unten finden Sie einen vollständigen Benchmarking-Code:
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']))
Die Ergebnisse sind unten gezeigt:
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
Wenn np.vectorize()
im Allgemeinen immer schneller ist als df.apply()
, warum wird dann np.vectorize()
nicht mehr erwähnt? Ich sehe immer nur StackOverflow-Beiträge, die sich auf Folgendes beziehen df.apply()
:
Pandas erstellen eine neue Spalte basierend auf Werten aus anderen Spalten
Wie verwende ich die Pandas-Funktion "Anwenden" auf mehrere Spalten?
So wenden Sie eine Funktion auf zwei Spalten des Pandas-Datenrahmens an
np.vectorize
ist im Grunde eine Python-for
Schleife (es ist eine bequeme Methode) undapply
mit einem Lambda auch in Python-Zeitapply
zeilenweise verwenden sollten, es sei denn, Sie müssen, und offensichtlich wird eine vektorisierte Funktion eine nicht vektorisierte Funktion übertreffen.np.vectorize
nicht vektorisiert. Es ist eine bekannte Fehlbezeichnung.str
Accessoren. Sie sind in vielen Fällen langsamer als das Listenverständnis. Wir nehmen zu viel an.Antworten:
Ich werde beginnen mit den Worten , dass die Macht der Pandas und NumPy Arrays von High-Performance abgeleitet wird vektorisiert Berechnungen auf numerische Arrays. 1 Der gesamte Sinn vektorisierter Berechnungen besteht darin, Schleifen auf Python-Ebene zu vermeiden, indem Berechnungen in hochoptimierten C-Code verschoben und zusammenhängende Speicherblöcke verwendet werden. 2
Python-Level-Schleifen
Jetzt können wir uns einige Timings ansehen. Im Folgenden sind alle Schleifen Python-Ebene , die entweder zu produzieren
pd.Series
,np.ndarray
oderlist
die gleichen Werte enthalten , Objekte. Für die Zuordnung zu einer Reihe innerhalb eines Datenrahmens sind die Ergebnisse vergleichbar.# 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
Einige Imbissbuden:
tuple
-basierten Methoden (die ersten 4) sind ein Faktor, der effizienter ist als diepd.Series
-basierten Methoden (die letzten 3).np.vectorize
, Listenverständnis +zip
undmap
Methoden, dh die Top 3, haben alle ungefähr die gleiche Leistung. Dies liegt daran, dass sie einige Pandas verwendentuple
und umgehenpd.DataFrame.itertuples
.raw=True
mit oderpd.DataFrame.apply
ohne. Diese Option führt NumPy-Arrays anstelle vonpd.Series
Objekten in die benutzerdefinierte Funktion ein .pd.DataFrame.apply
: nur eine weitere SchleifeUm genau die Objekte zu sehen , die Pandas herumgibt, können Sie Ihre Funktion trivial ändern:
def foo(row): print(type(row)) assert False # because you only need to see this once df.apply(lambda row: foo(row), axis=1)
Ausgabe :
<class 'pandas.core.series.Series'>
. Das Erstellen, Übergeben und Abfragen eines Objekts der Pandas-Serie ist im Vergleich zu NumPy-Arrays mit erheblichem Aufwand verbunden. Dies sollte nicht überraschen: Pandas-Serien enthalten eine angemessene Menge an Gerüsten, um einen Index, Werte, Attribute usw. aufzunehmen.Machen Sie die gleiche Übung noch einmal mit
raw=True
und Sie werden sehen<class 'numpy.ndarray'>
. All dies wird in den Dokumenten beschrieben, aber es ist überzeugender, es zu sehen.np.vectorize
: gefälschte VektorisierungDie Dokumentation für
np.vectorize
hat den folgenden Hinweis:Die "Rundfunkregeln" sind hier irrelevant, da die Eingabearrays die gleichen Abmessungen haben. Die Parallele zu
map
ist aufschlussreich, da diemap
obige Version eine nahezu identische Leistung aufweist. Der Quellcode zeigt, was passiert:np.vectorize
Konvertiert Ihre Eingabefunktion über in eine Universalfunktion ("ufunc")np.frompyfunc
. Es gibt einige Optimierungen, z. B. Caching, die zu einer Leistungsverbesserung führen können.Kurz gesagt,
np.vectorize
macht das , was eine Python-Level-Schleife tun sollte ,pd.DataFrame.apply
fügt aber einen klobigen Overhead hinzu. Es gibt keine JIT-Kompilierung, mit der Sie sehennumba
(siehe unten). Es ist nur eine Annehmlichkeit .Wahre Vektorisierung: Was Sie sollten verwenden
Warum werden die oben genannten Unterschiede nirgendwo erwähnt? Weil die Leistung wirklich vektorisierter Berechnungen sie irrelevant macht:
%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
Ja, das ist ~ 40x schneller als die schnellste der oben genannten Loop-Lösungen. Beides ist akzeptabel. Meiner Meinung nach ist die erste prägnant, lesbar und effizient. Schauen Sie sich andere Methoden nur an, z. B.
numba
unten, wenn die Leistung kritisch ist und dies Teil Ihres Engpasses ist.numba.njit
: grössere EffizienzWenn Schleifen werden als rentabel sie sind in der Regel über optimieren
numba
mit zugrunde liegenden NumPy Arrays so viel wie möglich zu C bewegennumba
Verbessert in der Tat die Leistung auf Mikrosekunden . Ohne umständliche Arbeit wird es schwierig sein, viel effizienter zu werden.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
Die Verwendung
@njit(parallel=True)
kann einen weiteren Schub für größere Arrays liefern.1 Numerische Typen umfassen:
int
,float
,datetime
,bool
,category
. Sie schließenobject
dtype aus und können in zusammenhängenden Speicherblöcken gespeichert werden.2 Es gibt mindestens zwei Gründe, warum NumPy-Operationen im Vergleich zu Python effizient sind:
quelle
parallel
Argument@njit(parallel=True)
gibt mir eine weitere Verbesserung gegenüber nur@njit
. Vielleicht können Sie das auch hinzufügen.Je komplexer Ihre Funktionen werden (dh je weniger
numpy
auf eigene Interna umgestellt werden können), desto mehr werden Sie feststellen, dass die Leistung nicht so unterschiedlich sein wird. Zum Beispiel: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)
Einige Timings machen:
Verwenden von Übernehmen
Ergebnisse:
76.2 ms ± 626 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Verwenden von
np.vectorize
Ergebnisse:
77.3 ms ± 216 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Numpy versucht, Python-Funktionen
ufunc
beim Aufruf in numpy- Objekte umzuwandelnnp.vectorize
. Wie das geht, weiß ich eigentlich nicht - man müsste sich mehr mit den Interna von Numpy beschäftigen, als ich bereit bin, Geldautomaten zu kaufen. Das heißt, es scheint bei einfach numerischen Funktionen eine bessere Arbeit zu leisten als bei dieser auf Zeichenfolgen basierenden Funktion.Anlassen der Größe auf 1.000.000:
name_series = pd.Series(np.random.choice(['adam', 'chang', 'eliza', 'odom'], replace=True, size=1000000))
apply
Ergebnisse:
769 ms ± 5.88 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
np.vectorize
Ergebnisse:
794 ms ± 4.85 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Ein besserer ( vektorisierter ) Weg mit
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()
Timings:
Ergebnisse:
67.2 ms ± 683 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
quelle
size=1000000
(1 Million) drehen?Ich bin neu in Python. Aber im folgenden Beispiel scheint "Anwenden" schneller zu funktionieren als "Vektorisieren", oder ich vermisse etwas.
import numpy as np import pandas as pd B = np.random.rand(1000,1000) fn = np.vectorize(lambda l: 1/(1-np.exp(-l))) print(fn(B)) B = pd.DataFrame(np.random.rand(1000,1000)) fn = lambda l: 1/(1-np.exp(-l)) print(B.apply(fn))
quelle