apply
, die Komfortfunktion, die Sie nie gebraucht haben
Wir beginnen damit, die Fragen im OP einzeln zu beantworten.
" Wenn bewerben so schlecht ist, warum ist es dann in der API? "
DataFrame.apply
und Series.apply
sind Komfortfunktionen, die für DataFrame- bzw. Serienobjekte definiert sind. apply
Akzeptiert alle benutzerdefinierten Funktionen, die eine Transformation / Aggregation auf einen DataFrame anwenden. apply
ist praktisch eine Silberkugel, die alles tut, was eine bestehende Pandas-Funktion nicht kann.
Einige der Dinge apply
können tun:
- Führen Sie eine benutzerdefinierte Funktion auf einem DataFrame oder einer Serie aus
- Wenden Sie eine Funktion zeilenweise (
axis=1
) oder spaltenweise ( axis=0
) auf einen DataFrame an
- Führen Sie die Indexausrichtung durch, während Sie die Funktion anwenden
- Führen Sie eine Aggregation mit benutzerdefinierten Funktionen durch (wir bevorzugen jedoch normalerweise
agg
oder transform
in diesen Fällen)
- Führen Sie elementweise Transformationen durch
- Übertragen Sie aggregierte Ergebnisse in die ursprünglichen Zeilen (siehe
result_type
Argument).
- Akzeptieren Sie Positions- / Schlüsselwortargumente, um sie an die benutzerdefinierten Funktionen zu übergeben.
...Unter anderen. Weitere Informationen finden Sie in der Dokumentation unter Zeilen- oder spaltenweise Funktionsanwendung .
Warum ist es bei all diesen Funktionen apply
schlecht? Es ist, weil apply
es langsam ist . Pandas macht keine Annahmen über die Art Ihrer Funktion und wendet Ihre Funktion daher bei Bedarf iterativ auf jede Zeile / Spalte an. Darüber hinaus bedeutet die Behandlung aller oben genannten Situationen, dass apply
bei jeder Iteration ein erheblicher Aufwand entsteht. Ferner apply
verbraucht Speicher viel mehr, was eine Herausforderung für Speicher begrenzt Anwendungen ist.
Es gibt nur sehr wenige Situationen, in denen apply
die Verwendung angemessen ist (mehr dazu weiter unten). Wenn Sie nicht sicher sind, ob Sie verwenden sollten apply
, sollten Sie wahrscheinlich nicht.
Lassen Sie uns die nächste Frage beantworten.
" Wie und wann soll ich meinen Code kostenlos anwenden lassen ? "
Um es neu zu formulieren, hier sind einige häufige Situationen, in denen Sie alle Anrufe an loswerden möchten apply
.
Numerische Daten
Wenn Sie mit numerischen Daten arbeiten, gibt es wahrscheinlich bereits eine vektorisierte Cython-Funktion, die genau das tut, was Sie versuchen (wenn nicht, stellen Sie entweder eine Frage zum Stapelüberlauf oder öffnen Sie eine Funktionsanforderung auf GitHub).
Vergleichen Sie die Leistung apply
für eine einfache Additionsoperation.
df = pd.DataFrame({"A": [9, 4, 2, 1], "B": [12, 7, 5, 4]})
df
A B
0 9 12
1 4 7
2 2 5
3 1 4
df.apply(np.sum)
A 16
B 28
dtype: int64
df.sum()
A 16
B 28
dtype: int64
In Bezug auf die Leistung gibt es keinen Vergleich, das cythonisierte Äquivalent ist viel schneller. Es ist kein Diagramm erforderlich, da der Unterschied selbst für Spielzeugdaten offensichtlich ist.
%timeit df.apply(np.sum)
%timeit df.sum()
2.22 ms ± 41.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
471 µs ± 8.16 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Selbst wenn Sie das Übergeben von Raw-Arrays mit dem raw
Argument aktivieren , ist es immer noch doppelt so langsam.
%timeit df.apply(np.sum, raw=True)
840 µs ± 691 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Ein anderes Beispiel:
df.apply(lambda x: x.max() - x.min())
A 8
B 8
dtype: int64
df.max() - df.min()
A 8
B 8
dtype: int64
%timeit df.apply(lambda x: x.max() - x.min())
%timeit df.max() - df.min()
2.43 ms ± 450 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
1.23 ms ± 14.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Suchen Sie im Allgemeinen nach vektorisierten Alternativen, wenn dies möglich ist.
String / Regex
Pandas bietet in den meisten Situationen "vektorisierte" Zeichenfolgenfunktionen, aber es gibt seltene Fälle, in denen diese Funktionen sozusagen nicht ... "zutreffen".
Ein häufiges Problem besteht darin, zu überprüfen, ob ein Wert in einer Spalte in einer anderen Spalte derselben Zeile vorhanden ist.
df = pd.DataFrame({
'Name': ['mickey', 'donald', 'minnie'],
'Title': ['wonderland', "welcome to donald's castle", 'Minnie mouse clubhouse'],
'Value': [20, 10, 86]})
df
Name Value Title
0 mickey 20 wonderland
1 donald 10 welcome to donald's castle
2 minnie 86 Minnie mouse clubhouse
Dies sollte die zweite und dritte Zeile der Zeile zurückgeben, da "Donald" und "Minnie" in ihren jeweiligen "Titel" -Spalten vorhanden sind.
Mit apply würde dies mit using erfolgen
df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)
0 False
1 True
2 True
dtype: bool
df[df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)]
Name Title Value
1 donald welcome to donald's castle 10
2 minnie Minnie mouse clubhouse 86
Es gibt jedoch eine bessere Lösung, wenn Listenverständnisse verwendet werden.
df[[y.lower() in x.lower() for x, y in zip(df['Title'], df['Name'])]]
Name Title Value
1 donald welcome to donald's castle 10
2 minnie Minnie mouse clubhouse 86
%timeit df[df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)]
%timeit df[[y.lower() in x.lower() for x, y in zip(df['Title'], df['Name'])]]
2.85 ms ± 38.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
788 µs ± 16.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Hierbei ist zu beachten, dass iterative Routinen apply
aufgrund des geringeren Overheads schneller sind als . Wenn Sie mit NaNs und ungültigen dtypes umgehen müssen, können Sie darauf mit einer benutzerdefinierten Funktion aufbauen, die Sie dann mit Argumenten innerhalb des Listenverständnisses aufrufen können.
Weitere Informationen darüber, wann Listenverständnisse als gute Option angesehen werden sollten, finden Sie in meinem Artikel: Für Schleifen mit Pandas - Wann sollte es mich interessieren? .
Hinweis
Datums- und Datums- / Uhrzeitoperationen haben auch vektorisierte Versionen. So zum Beispiel, sollten Sie es vorziehen pd.to_datetime(df['date'])
, über, sagen df['date'].apply(pd.to_datetime)
.
Lesen Sie mehr in den
Dokumenten .
Eine häufige Gefahr: Explodierende Spalten von Listen
s = pd.Series([[1, 2]] * 3)
s
0 [1, 2]
1 [1, 2]
2 [1, 2]
dtype: object
Menschen sind versucht zu benutzen apply(pd.Series)
. Das ist schrecklich in Bezug auf die Leistung.
s.apply(pd.Series)
0 1
0 1 2
1 1 2
2 1 2
Eine bessere Option besteht darin, die Spalte aufzulisten und an pd.DataFrame zu übergeben.
pd.DataFrame(s.tolist())
0 1
0 1 2
1 1 2
2 1 2
%timeit s.apply(pd.Series)
%timeit pd.DataFrame(s.tolist())
2.65 ms ± 294 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
816 µs ± 40.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Zuletzt,
" Gibt es Situationen, in denen apply
es gut ist? "
Anwenden ist eine praktische Funktion, daher gibt es Situationen, in denen der Overhead vernachlässigbar genug ist, um zu vergeben. Es hängt wirklich davon ab, wie oft die Funktion aufgerufen wird.
Funktionen, die für Serien vektorisiert sind, jedoch keine DataFrames
Was ist, wenn Sie eine Zeichenfolgenoperation auf mehrere Spalten anwenden möchten? Was ist, wenn Sie mehrere Spalten in datetime konvertieren möchten? Diese Funktionen sind nur für Serien vektorisiert, daher müssen sie auf jede Spalte angewendet werden, die Sie konvertieren / bearbeiten möchten.
df = pd.DataFrame(
pd.date_range('2018-12-31','2019-01-31', freq='2D').date.astype(str).reshape(-1, 2),
columns=['date1', 'date2'])
df
date1 date2
0 2018-12-31 2019-01-02
1 2019-01-04 2019-01-06
2 2019-01-08 2019-01-10
3 2019-01-12 2019-01-14
4 2019-01-16 2019-01-18
5 2019-01-20 2019-01-22
6 2019-01-24 2019-01-26
7 2019-01-28 2019-01-30
df.dtypes
date1 object
date2 object
dtype: object
Dies ist ein zulässiger Fall für apply
:
df.apply(pd.to_datetime, errors='coerce').dtypes
date1 datetime64[ns]
date2 datetime64[ns]
dtype: object
Beachten Sie, dass es auch sinnvoll wäre, stack
eine explizite Schleife zu verwenden oder einfach nur zu verwenden. Alle diese Optionen sind etwas schneller als die Verwendung apply
, aber der Unterschied ist klein genug, um zu vergeben.
%timeit df.apply(pd.to_datetime, errors='coerce')
%timeit pd.to_datetime(df.stack(), errors='coerce').unstack()
%timeit pd.concat([pd.to_datetime(df[c], errors='coerce') for c in df], axis=1)
%timeit for c in df.columns: df[c] = pd.to_datetime(df[c], errors='coerce')
5.49 ms ± 247 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
3.94 ms ± 48.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
3.16 ms ± 216 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
2.41 ms ± 1.71 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Sie können einen ähnlichen Fall für andere Operationen wie Zeichenfolgenoperationen oder die Konvertierung in eine Kategorie festlegen.
u = df.apply(lambda x: x.str.contains(...))
v = df.apply(lambda x: x.astype(category))
v / s
u = pd.concat([df[c].str.contains(...) for c in df], axis=1)
v = df.copy()
for c in df:
v[c] = df[c].astype(category)
Und so weiter...
Konvertieren von Serien in str
: astype
versusapply
Dies scheint eine Eigenart der API zu sein. Die Verwendung apply
zum Konvertieren von Ganzzahlen in einer Serie in eine Zeichenfolge ist vergleichbar (und manchmal schneller) als die Verwendung astype
.
Das Diagramm wurde unter Verwendung der perfplot
Bibliothek aufgezeichnet .
import perfplot
perfplot.show(
setup=lambda n: pd.Series(np.random.randint(0, n, n)),
kernels=[
lambda s: s.astype(str),
lambda s: s.apply(str)
],
labels=['astype', 'apply'],
n_range=[2**k for k in range(1, 20)],
xlabel='N',
logx=True,
logy=True,
equality_check=lambda x, y: (x == y).all())
Bei Schwimmern sehe ich, dass das astype
durchweg so schnell oder etwas schneller ist als apply
. Dies hat also damit zu tun, dass die Daten im Test vom Typ Integer sind.
GroupBy
Operationen mit verketteten Transformationen
GroupBy.apply
wurde bisher noch nicht besprochen, ist aber GroupBy.apply
auch eine iterative Komfortfunktion, um alles zu handhaben, was die vorhandenen GroupBy
Funktionen nicht tun.
Eine häufige Anforderung besteht darin, einen GroupBy und dann zwei Hauptoperationen durchzuführen, z. B. einen "verzögerten Cumsum":
df = pd.DataFrame({"A": list('aabcccddee'), "B": [12, 7, 5, 4, 5, 4, 3, 2, 1, 10]})
df
A B
0 a 12
1 a 7
2 b 5
3 c 4
4 c 5
5 c 4
6 d 3
7 d 2
8 e 1
9 e 10
Sie benötigen hier zwei aufeinanderfolgende Gruppenanrufe:
df.groupby('A').B.cumsum().groupby(df.A).shift()
0 NaN
1 12.0
2 NaN
3 NaN
4 4.0
5 9.0
6 NaN
7 3.0
8 NaN
9 1.0
Name: B, dtype: float64
Mit apply
können Sie dies auf einen einzelnen Anruf verkürzen.
df.groupby('A').B.apply(lambda x: x.cumsum().shift())
0 NaN
1 12.0
2 NaN
3 NaN
4 4.0
5 9.0
6 NaN
7 3.0
8 NaN
9 1.0
Name: B, dtype: float64
Es ist sehr schwierig, die Leistung zu quantifizieren, da dies von den Daten abhängt. Aber im Allgemeinen apply
ist eine akzeptable Lösung, wenn das Ziel darin besteht, einen groupby
Anruf zu reduzieren (weil groupby
es auch ziemlich teuer ist).
Andere Vorsichtsmaßnahmen
Abgesehen von den oben genannten Einschränkungen ist es auch erwähnenswert, dass apply
die erste Zeile (oder Spalte) zweimal ausgeführt wird. Dies wird durchgeführt, um festzustellen, ob die Funktion irgendwelche Nebenwirkungen hat. Wenn nicht, apply
kann möglicherweise ein schneller Pfad zur Auswertung des Ergebnisses verwendet werden, andernfalls wird auf eine langsame Implementierung zurückgegriffen.
df = pd.DataFrame({
'A': [1, 2],
'B': ['x', 'y']
})
def func(x):
print(x['A'])
return x
df.apply(func, axis=1)
# 1
# 1
# 2
A B
0 1 x
1 2 y
Dieses Verhalten tritt auch bei GroupBy.apply
Pandas-Versionen <0,25 auf (es wurde für 0,25 behoben, siehe hier für weitere Informationen .)
returns.add(1).apply(np.log)
vs.np.log(returns.add(1)
ist ein Fall, in dem dieapply
Geschwindigkeit im Allgemeinen geringfügig schneller ist. Dies ist das grüne Feld unten rechts im Diagramm von jpp unten.