Pandas Apply: DataFrames und Series mit benutzerdefinierten Funktionen transformieren
Updated on
Die Datentransformation bildet das Herzstück jedes Datenanalyse-Workflows. Während Pandas Hunderte von integrierten Methoden für gängige Operationen bietet, erfordern reale Daten oft benutzerdefinierte Logik, die Standardfunktionen nicht bewältigen können. Dies schafft ein Dilemma: Wie wenden Sie effizient komplexe, benutzerdefinierte Transformationen über Tausende oder Millionen von Zeilen an?
Die apply()-Methode löst dieses Problem, indem sie es Ihnen ermöglicht, jede Python-Funktion über DataFrame-Spalten, Zeilen oder Series-Elemente auszuführen. Ob Sie inkonsistente String-Formate bereinigen, bedingte Geschäftslogik implementieren oder Features für Machine-Learning-Modelle entwickeln müssen – apply() bietet die Flexibilität, Operationen zu handhaben, die außerhalb des integrierten Pandas-Toolkits liegen. Diese Leistung geht jedoch mit Performance-Kompromissen einher, die viele Data Scientists übersehen, was zu Code führt, der 10-100x langsamer läuft als optimierte Alternativen.
Dieser Leitfaden zeigt, wie Sie apply() effektiv einsetzen, wann Sie es vollständig vermeiden sollten und welche vektorisierten Alternativen dieselben Ergebnisse in einem Bruchteil der Zeit liefern.
Grundlagen von pandas apply() verstehen
Die apply()-Methode existiert in zwei Formen: DataFrame.apply() für Operationen über gesamte Spalten oder Zeilen und Series.apply() für elementweise Transformationen auf einer einzelnen Spalte.
Series.apply() Syntax
Bei der Arbeit mit einer einzelnen Spalte (Series) führt apply() eine Funktion auf jedem Element aus:
import pandas as pd
import numpy as np
# Beispieldaten erstellen
df = pd.DataFrame({
'price': [29.99, 45.50, 15.75, 89.00],
'quantity': [2, 1, 5, 3],
'product': ['widget', 'gadget', 'tool', 'device']
})
# Funktion auf Series anwenden
def add_tax(price):
return price * 1.08
df['price_with_tax'] = df['price'].apply(add_tax)
print(df)Ausgabe:
price quantity product price_with_tax
0 29.99 2 widget 32.38920
1 45.50 1 gadget 49.14000
2 15.75 5 tool 17.01000
3 89.00 3 device 96.12000DataFrame.apply() mit axis-Parameter
Der axis-Parameter steuert, ob apply() Spalten oder Zeilen verarbeitet:
axis=0(Standard): Funktion auf jede Spalte anwenden (vertikale Operation)axis=1: Funktion auf jede Zeile anwenden (horizontale Operation)
# axis=0: Jede Spalte verarbeiten
def get_range(column):
return column.max() - column.min()
ranges = df[['price', 'quantity']].apply(get_range, axis=0)
print(ranges)
# Ausgabe:
# price 73.25
# quantity 4.00# axis=1: Jede Zeile verarbeiten
def calculate_total(row):
return row['price'] * row['quantity']
df['total'] = df.apply(calculate_total, axis=1)
print(df)Ausgabe:
price quantity product total
0 29.99 2 widget 59.98
1 45.50 1 gadget 45.50
2 15.75 5 tool 78.75
3 89.00 3 device 267.00Lambda-Funktionen vs. benannte Funktionen
Lambda-Funktionen bieten prägnante Inline-Transformationen, während benannte Funktionen bessere Lesbarkeit für komplexe Logik bieten.
Lambda-Funktionen
Perfekt für einfache, einzeilige Operationen:
# Produktnamen in Großbuchstaben umwandeln
df['product_upper'] = df['product'].apply(lambda x: x.upper())
# Rabattpreis berechnen
df['discounted'] = df['price'].apply(lambda x: x * 0.9 if x > 30 else x)
# Mehrere Spalten kombinieren
df['description'] = df.apply(
lambda row: f"{row['quantity']}x {row['product']} @ ${row['price']}",
axis=1
)
print(df['description'])Ausgabe:
0 2x widget @ $29.99
1 1x gadget @ $45.5
2 5x tool @ $15.75
3 3x device @ $89.0Benannte Funktionen
Unverzichtbar für mehrstufige Transformationen und wiederverwendbare Logik:
def categorize_price(price):
"""Produkte nach Preisstufe kategorisieren"""
if price < 20:
return 'Budget'
elif price < 50:
return 'Standard'
else:
return 'Premium'
df['tier'] = df['price'].apply(categorize_price)
def validate_order(row):
"""Geschäftsregeln auf Bestelldaten anwenden"""
if row['quantity'] > 10:
return 'Bulk Order - Review Required'
elif row['price'] * row['quantity'] > 200:
return 'High Value - Priority Shipping'
else:
return 'Standard Processing'
df['order_status'] = df.apply(validate_order, axis=1)
print(df[['product', 'tier', 'order_status']])result_type Parameter verstehen
Der result_type-Parameter (nur DataFrame.apply()) steuert, wie apply() die Ausgabe formatiert, wenn Funktionen mehrere Werte zurückgeben:
| result_type | Verhalten | Anwendungsfall |
|---|---|---|
| None (Standard) | Leitet Ausgabeformat automatisch ab | Allgemeiner Zweck |
| 'expand' | Teilt listenartige Ergebnisse in separate Spalten | Mehrere Rückgabewerte |
| 'reduce' | Versucht, Series statt DataFrame zurückzugeben | Aggregationsoperationen |
| 'broadcast' | Gibt DataFrame mit ursprünglicher Form zurück | Elementweise Transformationen |
def get_stats(column):
"""Mehrere Statistiken zurückgeben"""
return [column.mean(), column.std(), column.max()]
# Standardverhalten (leitet Struktur ab)
stats = df[['price', 'quantity']].apply(get_stats)
print(stats)
# In separate Zeilen erweitern
stats_expanded = df[['price', 'quantity']].apply(get_stats, result_type='expand')
print(stats_expanded)Methodenvergleich: apply vs map vs applymap vs transform
Zu verstehen, wann welche Methode verwendet wird, verhindert Performance-Engpässe:
| Methode | Arbeitet auf | Eingabefunktion erhält | Ausgabetyp | Am besten für |
|---|---|---|---|---|
Series.apply() | Einzelne Spalte | Jedes Element einzeln | Series | Elementweise Transformationen auf einer Spalte |
Series.map() | Einzelne Spalte | Jedes Element (akzeptiert auch dict/Series) | Series | Ersetzungs-/Lookup-Operationen |
DataFrame.apply() | Gesamter DataFrame | Vollständige Spalte (axis=0) oder Zeile (axis=1) | Series/DataFrame | Spalten-/Zeilweise Operationen |
DataFrame.applymap() (veraltet) | Gesamter DataFrame | Jedes Element einzeln | DataFrame | Elementweise auf allen Spalten (verwenden Sie stattdessen map()) |
DataFrame.transform() | DataFrame/GroupBy | Vollständige Spalte/Gruppe | Gleiche Form wie Eingabe | Operationen, die DataFrame-Form beibehalten |
# Series.apply() - elementweise benutzerdefinierte Funktion
df['price_rounded'] = df['price'].apply(lambda x: round(x, 0))
# Series.map() - Ersetzungsmapping
tier_map = {'Budget': 1, 'Standard': 2, 'Premium': 3}
df['tier_code'] = df['tier'].map(tier_map)
# DataFrame.apply() - spaltenweise Aggregation
totals = df[['price', 'quantity']].apply(sum, axis=0)
# DataFrame.transform() - Form mit groupby beibehalten
df['price_norm'] = df.groupby('tier')['price'].transform(
lambda x: (x - x.mean()) / x.std()
)Performance: Warum apply() langsam ist
Die apply()-Methode verarbeitet Daten in einer Python-Schleife und umgeht dabei die optimierten C/Cython-Implementierungen von Pandas. Bei großen Datensätzen entstehen dadurch erhebliche Performance-Einbußen.
Performance-Vergleich
import time
# Großen Datensatz erstellen
large_df = pd.DataFrame({
'values': np.random.randn(100000)
})
# Methode 1: apply() mit Lambda
start = time.time()
result1 = large_df['values'].apply(lambda x: x ** 2)
apply_time = time.time() - start
# Methode 2: Vektorisierte Operation
start = time.time()
result2 = large_df['values'] ** 2
vectorized_time = time.time() - start
print(f"apply() Zeit: {apply_time:.4f}s")
print(f"Vektorisierte Zeit: {vectorized_time:.4f}s")
print(f"Beschleunigung: {apply_time/vectorized_time:.1f}x")Typische Ausgabe:
apply() Zeit: 0.0847s
Vektorisierte Zeit: 0.0012s
Beschleunigung: 70.6xWann apply() vermeiden
Verwenden Sie stattdessen vektorisierte Operationen, wenn:
# NICHT: apply für Arithmetik verwenden
df['total'] = df.apply(lambda row: row['price'] * row['quantity'], axis=1)
# DOCH: Vektorisierte Multiplikation verwenden
df['total'] = df['price'] * df['quantity']
# NICHT: apply für String-Methoden verwenden
df['upper'] = df['product'].apply(lambda x: x.upper())
# DOCH: Integrierte String-Accessor verwenden
df['upper'] = df['product'].str.upper()
# NICHT: apply für Bedingungen verwenden
df['expensive'] = df['price'].apply(lambda x: 'Yes' if x > 50 else 'No')
# DOCH: np.where oder direkte Vergleiche verwenden
df['expensive'] = np.where(df['price'] > 50, 'Yes', 'No')Vektorisierte Alternativen
| Operation | Langsam (apply) | Schnell (vektorisiert) |
|---|---|---|
| Arithmetik | .apply(lambda x: x * 2) | * 2 |
| Bedingungen | .apply(lambda x: 'A' if x > 10 else 'B') | np.where(condition, 'A', 'B') |
| String-Operationen | .apply(lambda x: x.lower()) | .str.lower() |
| Datumsoperationen | .apply(lambda x: x.year) | .dt.year |
| Mehrere Bedingungen | .apply(complex_if_elif_else) | np.select([cond1, cond2], [val1, val2], default) |
# Komplexe Bedingungen mit np.select
conditions = [
df['price'] < 20,
(df['price'] >= 20) & (df['price'] < 50),
df['price'] >= 50
]
choices = ['Budget', 'Standard', 'Premium']
df['tier_fast'] = np.select(conditions, choices, default='Unknown')Häufige Anwendungsfälle für pandas apply()
Trotz Performance-Einschränkungen bleibt apply() unverzichtbar für Operationen, die keine vektorisierten Äquivalente haben.
1. String-Bereinigung mit komplexer Logik
# Beispiel unordentliche Daten
messy_df = pd.DataFrame({
'email': ['John.Doe@COMPANY.com', ' jane_smith@test.co.uk ', 'ADMIN@Site.NET']
})
def clean_email(email):
"""E-Mail-Format standardisieren"""
email = email.strip().lower()
# Zusätzliche Punkte entfernen
username, domain = email.split('@')
username = username.replace('..', '.')
return f"{username}@{domain}"
messy_df['email_clean'] = messy_df['email'].apply(clean_email)
print(messy_df)2. Bedingte Logik mit externen Daten
# Preisanpassung basierend auf externem Lookup
discount_rules = {
'widget': 0.10,
'gadget': 0.15,
'device': 0.05
}
def apply_discount(row):
"""Produktspezifischen Rabatt mit Mindestlogik anwenden"""
base_price = row['price']
discount = discount_rules.get(row['product'], 0)
discounted = base_price * (1 - discount)
# Mindestpreisuntergrenze
return max(discounted, 9.99)
df['final_price'] = df.apply(apply_discount, axis=1)3. Feature Engineering für ML
# Interaktionsfeatures erstellen
def create_features(row):
"""Features für prädiktive Modellierung generieren"""
features = {}
features['price_per_unit'] = row['price'] / row['quantity']
features['is_bulk'] = 1 if row['quantity'] > 5 else 0
features['revenue_tier'] = pd.cut(
[row['price'] * row['quantity']],
bins=[0, 50, 150, 300],
labels=['Low', 'Medium', 'High']
)[0]
return pd.Series(features)
feature_df = df.apply(create_features, axis=1)
df = pd.concat([df, feature_df], axis=1)4. API-Aufrufe und externe Lookups
def geocode_address(address):
"""Externe API für Geocodierung aufrufen"""
# Platzhalter für tatsächlichen API-Aufruf
# In der Praxis: requests.get(f"api.geocode.com?q={address}")
return {'lat': 40.7128, 'lon': -74.0060}
# Mit Rate Limiting anwenden
import time
def safe_geocode(address):
time.sleep(0.1) # Rate Limit
return geocode_address(address)
# df['coords'] = df['address'].apply(safe_geocode)Fortgeschrittene Techniken
Zusätzliche Argumente übergeben
Funktionen können zusätzliche Parameter über args und kwargs erhalten:
def apply_markup(price, markup_pct, min_profit):
"""Prozentuale Aufschlag mit Mindestgewinn hinzufügen"""
markup_amount = price * (markup_pct / 100)
return price + max(markup_amount, min_profit)
# Zusätzliche Argumente übergeben
df['retail_price'] = df['price'].apply(
apply_markup,
args=(25,), # markup_pct=25
min_profit=5.0 # Schlüsselwortargument
)apply() mit groupby() verwenden
Kombinieren Sie groupby mit apply für komplexe Aggregationen:
# Transformationen auf Gruppenebene
def normalize_group(group):
"""Z-Score-Normalisierung innerhalb der Gruppe"""
return (group - group.mean()) / group.std()
df['price_normalized'] = df.groupby('tier')['price'].apply(normalize_group)
# Benutzerdefinierte Gruppenaggregationen
def group_summary(group):
"""Zusammenfassungsstatistiken für Gruppe erstellen"""
return pd.Series({
'total_revenue': (group['price'] * group['quantity']).sum(),
'avg_price': group['price'].mean(),
'item_count': len(group)
})
tier_summary = df.groupby('tier').apply(group_summary)
print(tier_summary)Fortschrittsbalken mit tqdm
Überwachen Sie langwierige apply-Operationen:
from tqdm import tqdm
tqdm.pandas()
# Verwenden Sie progress_apply statt apply
# df['result'] = df['column'].progress_apply(slow_function)Fehler elegant behandeln
def safe_transform(value):
"""Transformation mit Fehlerbehandlung anwenden"""
try:
return complex_operation(value)
except Exception as e:
return None # oder np.nan, oder Standardwert
df['result'] = df['column'].apply(safe_transform)Häufige Fehler und Debugging
Fehler 1: axis=1 verwenden, wenn Vektorisierung möglich ist
# FALSCH: Langsame zeilenweise Operation
df['total'] = df.apply(lambda row: row['price'] * row['quantity'], axis=1)
# RICHTIG: Schnelle vektorisierte Operation
df['total'] = df['price'] * df['quantity']Fehler 2: return-Anweisung vergessen
# FALSCH: Kein Rückgabewert
def broken_function(x):
x * 2 # Fehlendes return!
# RICHTIG: Explizites return
def working_function(x):
return x * 2Fehler 3: DataFrame innerhalb von apply() ändern
# FALSCH: Versuch, DataFrame während Iteration zu ändern
def bad_function(row):
df.loc[row.name, 'new_col'] = row['price'] * 2 # Unsicher!
return row['price']
# RICHTIG: Werte zurückgeben und danach zuweisen
def good_function(row):
return row['price'] * 2
df['new_col'] = df.apply(good_function, axis=1)apply()-Funktionen debuggen
# Funktion zuerst an einzelner Zeile/Element testen
test_row = df.iloc[0]
result = your_function(test_row)
print(f"Testergebnis: {result}")
# Debug-Ausgaben in Funktion hinzufügen
def debug_function(row):
print(f"Verarbeite Zeile {row.name}: {row.to_dict()}")
result = complex_logic(row)
print(f"Ergebnis: {result}")
return result
# An kleiner Teilmenge testen
df.head(3).apply(debug_function, axis=1)Ergebnisse mit PyGWalker visualisieren
Nach der Transformation Ihres DataFrames mit apply() hilft die Visualisierung der Ergebnisse, Transformationen zu validieren und Muster aufzudecken. PyGWalker verwandelt Ihren pandas DataFrame direkt in Jupyter-Notebooks in eine interaktive Tableau-ähnliche Benutzeroberfläche.
import pygwalker as pyg
# Transformierte Daten interaktiv visualisieren
walker = pyg.walk(df)PyGWalker ermöglicht Drag-and-Drop-Analysen Ihrer angewendeten Transformationen:
- Vergleichen Sie Original- vs. transformierte Spalten mit nebeneinander liegenden Diagrammen
- Validieren Sie bedingte Logik durch Filtern und Gruppieren
- Erkennen Sie Ausreißer in berechneten Feldern durch Verteilungsdiagramme
- Exportieren Sie Visualisierungen für Dokumentation
Erkunden Sie PyGWalker unter github.com/Kanaries/pygwalker (opens in a new tab), um die Datenexploration von statischen Plots in interaktive Analysen zu verwandeln.
FAQ
Wie wende ich eine Funktion auf mehrere Spalten gleichzeitig an?
Verwenden Sie DataFrame.apply() mit axis=0, um jede ausgewählte Spalte zu verarbeiten, oder verwenden Sie vektorisierte Operationen direkt auf mehreren Spalten:
# Auf mehrere Spalten anwenden
df[['price', 'quantity']] = df[['price', 'quantity']].apply(lambda x: x * 1.1)
# Oder vektorisiert (schneller)
df[['price', 'quantity']] = df[['price', 'quantity']] * 1.1Was ist der Unterschied zwischen apply() und map()?
apply() führt jede aufrufbare Funktion elementweise aus, während map() für Ersetzungen über Dictionaries, Series oder Funktionen optimiert ist. Verwenden Sie map() für Lookups und Ersetzungen (schneller) und apply() für benutzerdefinierte Transformationen, die komplexe Logik erfordern.
Warum ist meine apply()-Funktion so langsam?
apply() wird in einer Python-Schleife statt in kompiliertem C-Code ausgeführt, was es 10-100x langsamer macht als vektorisierte Operationen. Überprüfen Sie immer, ob Pandas eine integrierte Methode (.str, .dt, arithmetische Operatoren) hat oder ob np.where()/np.select() Ihre Logik ersetzen kann, bevor Sie apply() verwenden.
Kann ich apply() mit Lambda-Funktionen verwenden, die auf mehrere Spalten zugreifen?
Ja, mit axis=1 zum Verarbeiten von Zeilen:
df['result'] = df.apply(lambda row: row['col1'] + row['col2'] * 0.5, axis=1)Dies ist jedoch langsam für große DataFrames. Bevorzugen Sie: df['result'] = df['col1'] + df['col2'] * 0.5
Wie gebe ich mehrere Spalten aus einem einzelnen apply()-Aufruf zurück?
Geben Sie eine pd.Series mit benannten Indizes zurück, und Pandas expandiert sie automatisch in Spalten:
def multi_output(row):
return pd.Series({
'sum': row['a'] + row['b'],
'product': row['a'] * row['b']
})
df[['sum', 'product']] = df.apply(multi_output, axis=1)Fazit
Die pandas apply()-Methode bietet wesentliche Flexibilität für benutzerdefinierte Transformationen, die außerhalb der integrierten Pandas-Operationen liegen. Während ihre Python-Schleifen-Implementierung Performance-Kompromisse schafft, trennt das Verständnis, wann apply() versus vektorisierte Alternativen verwendet werden sollen, effiziente Daten-Workflows von solchen, die bei Produktionsdatensätzen zum Stillstand kommen.
Wichtigste Erkenntnisse: Verwenden Sie apply() nur, wenn vektorisierte Methoden, String-Accessoren oder NumPy-Funktionen Ihr Ziel nicht erreichen können. Testen Sie Funktionen an einzelnen Zeilen, bevor Sie sie auf gesamte DataFrames anwenden. Für zeilenweise Operationen auf großen Datensätzen untersuchen Sie Cython, Numba JIT-Kompilierung oder den Wechsel zu Polars für parallele Ausführung.
Meistern Sie sowohl die Leistung als auch die Einschränkungen von apply(), und Ihr Datentransformations-Toolkit wird jede Herausforderung bewältigen, die Pandas Ihnen in den Weg wirft.