Corso
Molti ricorrono a .apply() per “evitare i loop” e si aspettano che sia veloce e lineare. In pratica, l’.apply(axis=1) riga per riga è comunque un loop a livello Python. Può essere lento su dati grandi e a volte restituisce forme che non ti aspettavi. La soluzione è semplice: usa operazioni vettorializzate di pandas/NumPy per i casi comuni e riserva .apply() alla logica che ha davvero bisogno di più colonne.
Questa guida mostra come funziona oggi .apply(), evidenzia le insidie più comuni e fornisce schemi pronti all’uso più veloci e chiari.
Che cos’è DataFrame.apply() e Series.apply()?
DataFrame.apply(func, axis=0) chiama func su ogni colonna per impostazione predefinita. Con axis=1, chiama func su ogni riga. Series.apply(func) chiama func su ogni elemento (o sull’intera Series, a seconda delle firme nelle versioni recenti).
- Riga per riga vs. colonna per colonna:
axis=1significa “per riga”.axis=0(predefinito) significa “per colonna”.
- Regole sulla forma di ritorno (pandas ≥0.23): Se la tua funzione restituisce uno scalare per riga/colonna, ottieni una
Series. Se restituisce unaSerieso undict, pandas espande quelle chiavi in unDataFrame. Se restituisce una lista/array, otterrai una Series di liste a meno che tu non impostiresult_type='expand'.
- Errori più rigorosi (pandas ≥2.0): Le lunghezze non corrispondenti per oggetti simili a liste ora generano errori invece di produrre silenziosamente risultati incoerenti.
Prima scelta consigliata: operazioni vettorializzate
La maggior parte dei compiti che combinano o trasformano colonne sono più veloci e chiari con espressioni vettorializzate o metodi integrati. L’esempio seguente calcola il differenziale di punti di una squadra di baseball senza .apply().
import pandas as pd
team_stats = pd.DataFrame({
"Team": ["ARI", "ATL", "BAL"],
"League": ["NL", "NL", "AL"],
"Year": [2012, 2012, 2012],
"RunsScored": [734, 700, 712],
"RunsAllowed": [688, 600, 705],
"Wins": [81, 94, 93],
"Games": [162, 162, 162],
"Playoffs": [0, 1, 1],
})
# Vectorized: fast and idiomatic
team_stats["RunDiff"] = team_stats["RunsScored"] - team_stats["RunsAllowed"]
print(team_stats[["Team", "RunsScored", "RunsAllowed", "RunDiff"]])
.apply() riga per riga quando ti serve davvero
Usa .apply(axis=1) quando la tua logica coinvolge davvero più colonne e non è facilmente vettorializzabile (per esempio, regole condizionali che dipendono da diversi campi).
calcolare un valore derivato da più colonne
Questo schema calcola un valore per riga usando più input. L’approccio vettorializzato visto sopra è comunque preferibile, ma qui vedi l’uso corretto riga per riga e le opzioni che incidono su velocità e output.
def compute_run_diff(row):
# Treat the row as read-only; return a scalar
return row["RunsScored"] - row["RunsAllowed"]
# Row-wise apply (Python-level loop; can be slow on large data)
team_stats["RunDiff_apply"] = team_stats.apply(compute_run_diff, axis=1)
Quando la funzione è numerica e ha bisogno solo di array grezzi, passando raw=True salti parte dell’overhead di pandas fornendo un array NumPy alla tua funzione.
import numpy as np
def compute_run_diff_raw(values):
# values is a NumPy array when raw=True
# Order matches the column order we select
rs, ra = values
return rs - ra
team_stats["RunDiff_raw"] = team_stats[["RunsScored", "RunsAllowed"]].apply(
compute_run_diff_raw, axis=1, raw=True
)
verificare una volta l’input della funzione
Quando collego una nuova funzione per riga, stampo una riga una sola volta per confermare cosa viene passato.
def debug_row(row):
# Print the first row only
if row.name == team_stats.index[0]:
print("Example row:", row.to_dict())
return 0
_ = team_stats.apply(debug_row, axis=1)
Caso d’uso: somme, totali di stagione e flag testuali
Dato un dataset dei Rays per anno, preferisci metodi integrati e strumenti elemento per elemento rispetto a .apply() quando possibile.
rays_by_year = pd.DataFrame(
{
"Year": [2012, 2011, 2010, 2009, 2008],
"RunsScored": [697, 707, 802, 803, 774],
"RunsAllowed": [577, 614, 649, 754, 671],
"Wins": [90, 91, 96, 84, 97],
"Playoffs": [0, 1, 1, 0, 1],
}
).set_index("Year")
ottenere le somme di colonna in modo efficiente
Usa DataFrame.sum() invece di .apply(sum). È più veloce e più chiaro.
# Preferred
totals = rays_by_year.sum(axis=0)
print(totals)
# If you must use apply (not recommended here)
totals_apply = rays_by_year.apply(sum, axis=0)
calcolare il totale dei punti segnati in una stagione
Usa aritmetica vettoriale tra colonne; .apply() non serve.
rays_by_year["TotalRuns"] = rays_by_year["RunsScored"] + rays_by_year["RunsAllowed"]
print(rays_by_year[["RunsScored", "RunsAllowed", "TotalRuns"]].head())
convertire un flag 0/1 in testo
Preferisci Series.map() o replace() per trasformazioni elemento per elemento su una singola colonna.
# Using map with a dict
rays_by_year["PlayoffsText"] = rays_by_year["Playoffs"].map({0: "No", 1: "Yes"})
# Equivalent with replace
rays_by_year["PlayoffsText2"] = rays_by_year["Playoffs"].replace({0: "No", 1: "Yes"})
Controllare la forma dell’output di .apply()
Quando la tua funzione restituisce più di un valore per riga, rendi esplicita la forma. Eviti sorprese e rotture con versioni future.
restituire una lista ed espanderla in colonne
Usa result_type='expand' per suddividere una lista/array in più colonne.
def wins_losses(row):
# Derive wins and losses as two outputs
wins = row["Wins"]
losses = row["Games"] - row["Wins"] if "Games" in row else np.nan
return [wins, losses]
# Example using team_stats, which has "Wins" and "Games"
expanded = team_stats.apply(wins_losses, axis=1, result_type="expand")
expanded.columns = ["WinsOut", "LossesOut"]
team_stats = pd.concat([team_stats, expanded], axis=1)
restituire una series o un dict per assegnare i nomi alle colonne automaticamente
Restituire una Series o un dict da ogni riga produce un DataFrame con etichette di colonna corrispondenti.
def summary_row(row):
return pd.Series(
{
"IsWinningSeason": row["Wins"] >= 90,
"RunRatio": (row["RunsScored"] / row["RunsAllowed"]),
}
)
summary = team_stats.apply(summary_row, axis=1)
team_stats = pd.concat([team_stats, summary], axis=1)
Consigli sulle prestazioni che contano
L’.apply(axis=1) riga per riga è comodo ma lento su DataFrame grandi perché chiama la tua funzione Python una volta per riga. Questi schemi evitano quel collo di bottiglia.
- Preferisci operazioni vettorializzate di pandas/NumPy e metodi integrati come
sum,mean,clip,where,astypee i metodi per stringhe/accessor.
- Per operazioni numeriche su righe/colonne che accettano array, passa
raw=Trueper ottenere un array NumPy e ridurre l’overhead di pandas.
- Per lavoro elemento per elemento su una singola colonna, usa
Series.map()o metodi vettorializzati invece diDataFrame.apply(axis=1).
- Evita di mutare la riga/colonna fornita all’interno della tua funzione; restituisci invece un nuovo valore.
Note di versione da conoscere (pandas 2.x)
Le versioni recenti di pandas hanno reso più rigoroso il comportamento di .apply() così che i risultati siano più prevedibili.
- Ritorni tipo lista: dalla 2.0 in poi, lunghezze di lista non coerenti generano un errore. Usa lunghezze consistenti e imposta
result_type='expand'quando vuoi più colonne.
- Espansione dell’output: restituire una
Serieso undictsi espande in un DataFrame con quelle chiavi come colonne (stabile dalla 0.23).
Series.apply()modifiche: l’argomentoconvert_dtypeè deprecato. Se ti servono tipi misti, effettua il cast aobjectprima (per esempio,s.astype("object").apply(fn)). Le firme più recenti permettono di controllare sefnriceve scalari o una Series in più contesti (per esempio,by_row).
.apply() vs. .map() vs. .applymap() vs. .agg()
Scegli l’API che corrisponde alla forma della tua trasformazione.
Series.map(func_or_dict): trasformazione elemento per elemento su una colonna. Ottimo per lookup o funzioni semplici.
DataFrame.apply(func, axis=1): logica riga per riga che necessita di più colonne.
DataFrame.apply(func, axis=0): logica per colonna (ogni input è una Series che rappresenta una colonna).
DataFrame.applymap(func): elemento per elemento su ogni cella. Usalo con parsimonia; i metodi vettorializzati sono più veloci.
.agg()/.transform(): aggregazioni e trasformazioni per gruppo; preferiscile nelle pipeline con groupby.
Errori comuni e correzioni rapide
Questi sono i problemi che più spesso causano risultati errati o rallentamenti, con i modi per correggerli.
- Dimenticare axis=1 per logica per riga: se la tua funzione riceve all’improvviso colonne invece di righe, aggiungi
axis=1.
- Series di liste inattesa: quando restituisci una lista/array da ogni riga, imposta
result_type='expand'per suddividerla in colonne.
- Codice riga per riga lento: sostituiscilo con espressioni vettorializzate o metodi integrati; se non è possibile, valuta
raw=Trueper ridurre l’overhead.
- Mutare la riga in input: tratta gli input come di sola lettura e restituisci nuovi valori.
- Deriva di dtype: se la tua funzione a volte restituisce non interi, le colonne intere possono essere promosse a float o object. Esegui il cast dopo con
astypese necessario.
Conclusione
.apply() è uno strumento flessibile, ma non è una scorciatoia per le prestazioni. Usa operazioni vettorializzate per aritmetica, aggregazioni e trasformazioni elemento per elemento su una singola colonna. Ricorri a DataFrame.apply(axis=1) solo quando la tua logica ha davvero bisogno di più colonne per riga. Quando la usi, controlla la forma dell’output con result_type, valuta raw=True per funzioni numeriche e tieni d’occhio i dtypes. Questi schemi producono risultati prevedibili con le versioni moderne di pandas e scalano meglio all’aumentare dei dati.

