TL;DR: Verwenden Sie map
+fillna
für große di
und verwenden Sie replace
für kleine di
1. Eine Alternative: np.select()
Wenn das Umsetzungsverzeichnis nicht zu groß ist, ist eine weitere Option numpy.select
. Die Syntax von np.select
erfordert separate Arrays/Listen von Bedingungen und Ersatzwerten, sodass die Schlüssel und Werte von di
getrennt sein sollten.
import numpy as np
df['col1'] = np.select((df[['col1']].values == list(di)).T, di.values(), df['col1'])
N. B. Wenn das Umsetzungsverzeichnis di
sehr groß ist, kann es zu Speicherproblemen führen, da, wie Sie in der obigen Codezeile sehen können, ein boolesches Array der Form (len(df), len(di))
erforderlich ist, um die Bedingungen auszuwerten.
2. map
+fillna
vs replace
. Was ist besser?
Wenn wir uns den Quellcode anschauen, ist map
, wenn ein Wörterbuch übergeben wird, eine optimierte Methode, die eine Cython-optimierte take_nd()
-Funktion aufruft, um die Ersetzungen vorzunehmen, und fillna()
ruft where()
(eine weitere optimierte Methode) auf, um Werte zu füllen. Andererseits wird replace()
in Python implementiert und verwendet eine Schleife über das Wörterbuch. Wenn das Wörterbuch also groß ist, kann replace
potenziell tausendmal langsamer sein als map
+fillna
. Lassen Sie uns den Unterschied am folgenden Beispiel verdeutlichen, bei dem ein einzelner Wert (0
) in der Spalte ersetzt wird (eine unter Verwendung eines Wörterbuchs der Länge 1000 (di1
) und eine unter Verwendung eines Wörterbuchs der Länge 1 (di2
)).
df = pd.DataFrame({'col1': range(1000)})
di1 = {k: k+1 for k in range(-1000, 1)}
di2 = {0: 1}
%timeit df['col1'].map(di1).fillna(df['col1'])
# 1.19 ms ± 6.77 µs pro Schleife (Mittelwert ± Standardabweichung von 7 Schleifen, 1.000 Schleifen pro Schleife)
%timeit df['col1'].replace(di1)
# 41,4 ms ± 400 µs pro Schleife (Mittelwert ± Standardabweichung von 7 Schleifen, 100 Schleifen pro Schleife)
%timeit df['col1'].map(di2).fillna(df['col1'])
# 691 µs ± 27,9 µs pro Schleife (Mittelwert ± Standardabweichung von 7 Schleifen, 1.000 Schleifen pro Schleife)
%timeit df['col1'].replace(di2)
# 157 µs ± 3,34 µs pro Schleife (Mittelwert ± Standardabweichung von 7 Schleifen, 10.000 Schleifen pro Schleife)
Wie Sie sehen können, ist wenn len(di)==1000
, replace
35 Mal langsamer, aber wenn len(di)==1
, ist es 4.5 Mal schneller. Dieser Unterschied wird größer, wenn die Größe des Umsetzungsverzeichnisses di
zunimmt.
Tatsächlich können wir anhand der Leistungsplots folgende Beobachtungen machen. Die Plots wurden mit bestimmten Parametern in jedem Diagramm erstellt. Sie können den Code unten verwenden, um die Größe des Dataframes zu ändern, um unterschiedliche Parameter zu sehen, aber er wird sehr ähnliche Plots erzeugen.
- Für einen gegebenen DataFrame macht
map
+fillna
Ersetzungen in fast konstanter Zeit, unabhängig von der Größe des Umsetzungsverzeichnisses, während replace
schlechter wird, je größer das Umsetzungsverzeichnis wird (oben links).
- Der Prozentsatz der ersetzten Werte im DataFrame hat kaum Einfluss auf den Laufzeitunterschied. Der Einfluss der Länge von
di
übertrifft vollständig jeden Einfluss, den er hat (oben rechts).
- Für ein gegebenes Umsetzungsverzeichnis führt
map
+fillna
besser als replace
aus, wenn die Größe des Dataframes zunimmt (unten links).
- Wenn
di
groß ist, ist die Größe des Dataframes unerheblich; map
+fillna
ist viel schneller als replace
(unten rechts).
Verwendeter Code zur Erstellung der Plots:
import numpy as np
import pandas as pd
from perfplot import plot
import matplotlib.pyplot as plt
kernels = [lambda df,di: df['col1'].replace(di),
lambda df,di: df['col1'].map(di).fillna(df['col1'])]
labels = ["replace", "map+fillna"]
# Erstes Diagramm
N, m = 100000, 20
plot(
setup=lambda n: (pd.DataFrame({'col1': np.resize(np.arange(m*n), N)}),
{k: (k+1)/2 for k in range(n)}),
kernels=kernels, labels=labels,
n_range=range(1, 21),
xlabel='Länge des Ersatzverzeichnisses',
title=f'Werte in einer Spalte umsetzen (len(df)={N:,}, {100//m}% ersetzt)',
equality_check=pd.Series.equals)
_, xmax = plt.xlim()
plt.xlim((0.5, xmax+1))
plt.xticks(np.arange(1, xmax+1, 2));
# Zweites Diagramm
N, m = 100000, 1000
di = {k: (k+1)/2 for k in range(m)}
plot(
setup=lambda n: pd.DataFrame({'col1': np.resize(np.arange((n-100)*m//100, n*m//100), N)}),
kernels=kernels, labels=labels,
n_range=[1, 5, 10, 15, 25, 40, 55, 75, 100],
xlabel='Prozentsatz der ersetzten Werte',
title=f'Werte in einer Spalte umsetzen (len(df)={N:,}, len(di)={m})',
equality_check=pd.Series.equals);
# Drittes Diagramm
m, n = 10, 0.01
di = {k: (k+1)/2 for k in range(m)}
plot(
setup=lambda N: pd.DataFrame({'col1': np.resize(np.arange((n-1)*m, n*m), N)}),
kernels=kernels, labels=labels,
n_range=[2**k for k in range(6, 21)],
xlabel='Länge des Dataframes',
logy=False,
title=f'Werte in einer Spalte umsetzen (len(di)={m}, {int(n*100)}% ersetzt)',
equality_check=pd.Series.equals);
# Viertes Diagramm
m, n = 100, 0.01
di = {k: (k+1)/2 for k in range(m)}
plot(
setup=lambda N: pd.DataFrame({'col1': np.resize(np.arange((n-1)*m, n*m), N)}),
kernels=kernels, labels=labels,
n_range=[2**k for k in range(6, 21)],
xlabel='Länge des Dataframes',
title=f'Werte in einer Spalte umsetzen (len(di)={m}, {int(n*100)}% ersetzt)',
equality_check=pd.Series.equals);