Die Leistung von Pandas gilt im Vergleich zu np.vectorize, um aus vorhandenen Spalten eine neue Spalte zu erstellen

80

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 NZeilen:

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 Aund 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 Nauf 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

stackoverflowuser2010
quelle
Ich habe mich nicht mit den Details Ihrer Frage befasst, sondern np.vectorizeist im Grunde eine Python- forSchleife (es ist eine bequeme Methode) und applymit einem Lambda auch in Python-Zeit
Roganjosh
"Wenn np.vectorize () im Allgemeinen immer schneller als df.apply () ist, warum wird np.vectorize () dann nicht mehr erwähnt?" Weil Sie nicht applyzeilenweise verwenden sollten, es sei denn, Sie müssen, und offensichtlich wird eine vektorisierte Funktion eine nicht vektorisierte Funktion übertreffen.
PMende
1
@ PMende wird aber np.vectorizenicht vektorisiert. Es ist eine bekannte Fehlbezeichnung
Roganjosh
1
@ PMende, klar, ich habe nichts anderes impliziert. Sie sollten Ihre Meinung zur Implementierung nicht aus Timings ableiten. Ja, sie sind aufschlussreich. Aber sie können Sie dazu bringen, Dinge anzunehmen, die nicht wahr sind.
Jpp
3
@ PMende spielen mit Pandas .strAccessoren. Sie sind in vielen Fällen langsamer als das Listenverständnis. Wir nehmen zu viel an.
Roganjosh

Antworten:

111

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.ndarrayoder listdie 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:

  1. Die tuple-basierten Methoden (die ersten 4) sind ein Faktor, der effizienter ist als die pd.Series-basierten Methoden (die letzten 3).
  2. np.vectorize, Listenverständnis + zipund mapMethoden, dh die Top 3, haben alle ungefähr die gleiche Leistung. Dies liegt daran, dass sie einige Pandas verwenden tuple und umgehen pd.DataFrame.itertuples.
  3. Es gibt eine signifikante Geschwindigkeitsverbesserung gegenüber der Verwendung raw=Truemit oder pd.DataFrame.applyohne. Diese Option führt NumPy-Arrays anstelle von pd.SeriesObjekten in die benutzerdefinierte Funktion ein .

pd.DataFrame.apply: nur eine weitere Schleife

Um 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=Trueund Sie werden sehen <class 'numpy.ndarray'>. All dies wird in den Dokumenten beschrieben, aber es ist überzeugender, es zu sehen.

np.vectorize: gefälschte Vektorisierung

Die Dokumentation für np.vectorizehat den folgenden Hinweis:

Die vektorisierte Funktion wertet pyfuncüber aufeinanderfolgende Tupel der Eingabearrays wie die Python-Map-Funktion aus, verwendet jedoch die Broadcast-Regeln von numpy.

Die "Rundfunkregeln" sind hier irrelevant, da die Eingabearrays die gleichen Abmessungen haben. Die Parallele zu mapist aufschlussreich, da die mapobige Version eine nahezu identische Leistung aufweist. Der Quellcode zeigt, was passiert: np.vectorizeKonvertiert 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.vectorizemacht das , was eine Python-Level-Schleife tun sollte , pd.DataFrame.applyfügt aber einen klobigen Overhead hinzu. Es gibt keine JIT-Kompilierung, mit der Sie sehen numba(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. numbaunten, wenn die Leistung kritisch ist und dies Teil Ihres Engpasses ist.

numba.njit: grössere Effizienz

Wenn Schleifen werden als rentabel sie sind in der Regel über optimieren numbamit zugrunde liegenden NumPy Arrays so viel wie möglich zu C bewegen

numbaVerbessert 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ßen object 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:

  • Alles in Python ist ein Objekt. Dies schließt im Gegensatz zu C Zahlen ein. Python-Typen haben daher einen Overhead, der bei nativen C-Typen nicht vorhanden ist.
  • NumPy-Methoden basieren normalerweise auf C. Darüber hinaus werden nach Möglichkeit optimierte Algorithmen verwendet.
jpp
quelle
1
@jpp: Die Verwendung des Dekorators mit parallelArgument @njit(parallel=True)gibt mir eine weitere Verbesserung gegenüber nur @njit. Vielleicht können Sie das auch hinzufügen.
Sheldore
1
Sie haben eine doppelte Überprüfung auf b [i]! = 0. Normales Python- und Numba-Verhalten besteht darin, nach 0 zu suchen und einen Fehler auszulösen. Dies unterbricht wahrscheinlich jede SIMD-Vektorisierung und hat normalerweise einen hohen Einfluss auf die Ausführungsgeschwindigkeit. Sie können dies jedoch in Numba in @njit (error_model = 'numpy') ändern, um diese doppelte Überprüfung auf eine Division durch 0 zu vermeiden. Es wird auch empfohlen, Speicher mit np.empty zuzuweisen und das Ergebnis in einer else-Anweisung auf 0 zu setzen.
Max9111
1
error_model numpy verwendet das, was der Prozessor in einer Division durch 0 -> NaN angibt. Zumindest in Numba 0.41dev verwenden beide Versionen die SIMD-Vektorisierung. Sie können dies wie hier beschrieben überprüfen. Numba.pydata.org/numba-doc/dev/user/faq.html (1.16.2.3. Warum ist meine Schleife nicht vektorisiert?) Ich würde Ihrer Funktion einfach eine else-Anweisung hinzufügen (res [ i] = 0.) und ordne Speicher mit np.empty zu. Dies sollte in Kombination mit error_model = 'numpy' die Leistung um ca. 20% verbessern. Bei älteren Numba-Versionen gab es einen höheren Einfluss auf die Leistung ...
max9111
2
@ stackoverflowuser2010, Es gibt keine universelle Antwort "für beliebige Funktionen". Sie müssen das richtige Werkzeug für den richtigen Job auswählen, was Teil des Verständnisses von Programmierung / Algorithmen ist.
jpp
1
Schöne Ferien!
CS95
4

Je komplexer Ihre Funktionen werden (dh je weniger numpyauf 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

%timeit name_series.apply(parse_name)

Ergebnisse:

76.2 ms ± 626 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Verwenden von np.vectorize

%timeit parse_name_vec(name_series)

Ergebnisse:

77.3 ms ± 216 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Numpy versucht, Python-Funktionen ufuncbeim Aufruf in numpy- Objekte umzuwandeln np.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

%timeit name_series.apply(parse_name)

Ergebnisse:

769 ms ± 5.88 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

np.vectorize

%timeit parse_name_vec(name_series)

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:

%timeit np.select(cases, replacements, default=name_series)

Ergebnisse:

67.2 ms ± 683 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
PMende
quelle
Was ist, wenn Sie das auf size=1000000(1 Million) drehen?
stackoverflowuser2010
2
Ich bin mir ziemlich sicher, dass Ihre Behauptungen hier falsch sind. Ich kann diese Aussage
vorerst
@ stackoverflowuser2010 Ich habe es zusammen mit einem tatsächlichen vektorisierten Ansatz aktualisiert .
PMende
0

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))
fordlab22
quelle