Evitare iterrows e i cicli Python
Sostituisca i cicli riga per riga con operazioni vettoriali sulle colonne, np.where e pd.cut per ottenere accelerazioni da 10 a 100 volte.
Evitare iterrows e i cicli Python è una lezione Pandas & NumPy Academy gratuita su CoddyKit. Questa è la lezione 2 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento Pandas & NumPy Academy, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso Pandas & NumPy Academy include 4 lezioni in totale.
Il costo dell'iterazione riga per riga
Pandas è basato su NumPy, che elabora interi array in una sola volta utilizzando codice C compilato. Quando esegue un'iterazione riga per riga con for _, row in df.iterrows(), aggira questo meccanismo e ricade nel puro Python: ogni riga viene estratta come oggetto Series e il ciclo viene eseguito dall'interprete Python con un costo circa 100-1000 volte superiore a quello di un'operazione vettorializzata equivalente. Per un DataFrame di 1 milione di righe, questo può significare minuti anziché millisecondi.
import pandas as pd
import numpy as np
import timeit
np.random.seed(0)
df = pd.DataFrame({'a': np.random.randn(10000), 'b': np.random.randn(10000)})
# Slow: iterrows loop
def loop_version(df):
result = []
for _, row in df.iterrows():
result.append(row['a'] + row['b'])
return pd.Series(result)
# Fast: vectorised
t_loop = timeit.timeit(lambda: loop_version(df), number=5)
t_vec = timeit.timeit(lambda: df['a'] + df['b'], number=500)
print(f'Loop (5 runs): {t_loop:.3f}s total')
print(f'Vectorised (500 runs): {t_vec:.3f}s total')
print(f'Speedup: ~{(t_loop/5)/(t_vec/500):.0f}x')Sostituire i cicli condizionali con np.where
np.where(condition, value_if_true, value_if_false) è l'equivalente vettorializzato di un if/else elemento per elemento. Invece di iterare sulle righe e scrivere un'istruzione if, passi la condizione e i due possibili valori del risultato come array. np.where esegue il calcolo in C sull'intera colonna in una sola volta, risultando 50-200 volte più veloce di un ciclo Python equivalente per i DataFrame di grandi dimensioni.
import pandas as pd
import numpy as np
import timeit
np.random.seed(0)
df = pd.DataFrame({'price': np.random.uniform(10, 100, 100000)})
# Slow: iterrows
def label_loop(df):
labels = []
for _, row in df.iterrows():
if row['price'] > 50:
labels.append('expensive')
else:
labels.append('cheap')
return labels
# Fast: np.where
def label_vectorised(df):
return np.where(df['price'] > 50, 'expensive', 'cheap')
# Verify equivalence on a small subset
assert list(label_loop(df.head(100))) == list(label_vectorised(df.head(100)))
print('Results match!')
print('Fast version result sample:', label_vectorised(df)[:5])Più condizioni con np.select
Per tre o più condizioni, utilizzi np.select(condlist, choicelist, default=) invece di np.where annidati. Passi un elenco di array booleani e un elenco dei corrispondenti valori di output. La prima condizione verificata determina l'output; alle righe che non soddisfano alcuna condizione viene assegnato il valore default. In questo modo sostituisce complesse catene if/elif/else all'interno dei cicli con un'espressione pulita e vettorializzata.
import pandas as pd
import numpy as np
np.random.seed(0)
df = pd.DataFrame({'score': np.random.randint(0, 101, 20)})
conditions = [
df['score'] >= 90,
df['score'] >= 75,
df['score'] >= 60
]
choices = ['A', 'B', 'C']
df['grade'] = np.select(conditions, choices, default='F')
print(df.sort_values('score', ascending=False).head(10))Operazioni vettorializzate sulle stringhe tramite .str
La manipolazione delle stringhe è una causa comune di cicli lenti. L'accessor Pandas .str fornisce equivalenti vettorializzati di tutti i metodi per stringhe di Python: .str.lower(), .str.strip(), .str.replace(), .str.contains() e molti altri. Utilizzare i metodi .str è 10-50 volte più veloce che iterare sulle righe e chiamare manualmente i metodi per stringhe di Python.
import pandas as pd
import numpy as np
import timeit
np.random.seed(0)
names = ['Alice Smith', 'BOB JONES', ' carol ', 'Dave Brown'] * 25000
df = pd.DataFrame({'name': names})
# Slow: loop
def clean_loop(df):
return [n.strip().title() for n in df['name']]
# Fast: .str accessor
def clean_vectorised(df):
return df['name'].str.strip().str.title()
t1 = timeit.timeit(lambda: clean_loop(df), number=10)
t2 = timeit.timeit(lambda: clean_vectorised(df), number=50)
print(f'Loop (10 runs): {t1:.3f}s')
print(f'.str (50 runs): {t2:.3f}s')
print(f'Speedup: {(t1/10)/(t2/50):.0f}x')
print('Sample:', clean_vectorised(df).head(4).tolist())Utilizzare pd.cut e pd.qcut invece dei cicli
pd.cut() e pd.qcut() vettorializzano le operazioni di suddivisione in intervalli che spesso vengono implementate con cicli contenenti più condizioni if/elif. Se si sta per scrivere 'if value < 18: age_group = child elif value < 65: age_group = adult' all'interno di un ciclo sulle righe, lo si sostituisca con una singola chiamata a pd.cut(), che elabora l'intera colonna in una sola volta alla velocità del codice C.
import pandas as pd
import numpy as np
import timeit
np.random.seed(0)
df = pd.DataFrame({'age': np.random.randint(0, 90, 100000)})
# Slow: loop with conditionals
def categorise_loop(df):
result = []
for age in df['age']:
if age < 18:
result.append('child')
elif age < 65:
result.append('adult')
else:
result.append('senior')
return result
# Fast: pd.cut
def categorise_cut(df):
return pd.cut(df['age'], bins=[0, 18, 65, 100],
labels=['child', 'adult', 'senior'],
right=False)
assert list(categorise_loop(df.head(5))) == list(categorise_cut(df.head(5)).astype(str))
print('Fast version sample:', categorise_cut(df).head(5).tolist())apply() non è sempre la soluzione
Molti tutorial consigliano di sostituire i cicli con .apply(func), ma apply è comunque internamente un ciclo a livello Python: è solo leggermente più veloce di iterrows e molto più lento della vera vettorializzazione. Riservi apply ai casi in cui non esiste un'alternativa vettorializzata (ad esempio una logica complessa su più colonne). Se una funzione integrata di Pandas o NumPy è in grado di esprimere l'operazione, la preferisca sempre a apply.
import pandas as pd
import numpy as np
import timeit
np.random.seed(0)
df = pd.DataFrame({'x': np.random.randn(100000)})
# These all do the same thing — compare performance
t1 = timeit.timeit(lambda: df['x'].apply(lambda v: v**2), number=20)
t2 = timeit.timeit(lambda: df['x'] ** 2, number=200)
t3 = timeit.timeit(lambda: np.square(df['x']), number=200)
print(f'apply(v**2): {t1/20*1000:.2f} ms per run')
print(f'** operator: {t2/200*1000:.2f} ms per run')
print(f'np.square(): {t3/200*1000:.2f} ms per run')Sostituire i cicli groupby con agg e transform
Un modello comune consiste nell'iterare manualmente sui gruppi: for name, group in df.groupby('cat'): do_something(group). Questo è lento per gli stessi motivi per cui è lento iterrows. Lo sostituisca con groupby().agg() per produrre statistiche riepilogative oppure con groupby().transform() per riportare le statistiche a livello di gruppo nelle posizioni delle righe originali.
import pandas as pd
import numpy as np
import timeit
np.random.seed(0)
df = pd.DataFrame({
'cat': np.random.choice(['A','B','C','D'], 100000),
'val': np.random.randn(100000)
})
# Slow: manual loop to add group mean
def slow_group_mean(df):
means = {}
for name, group in df.groupby('cat'):
means[name] = group['val'].mean()
return df['cat'].map(means)
# Fast: transform
def fast_group_mean(df):
return df.groupby('cat')['val'].transform('mean')
t1 = timeit.timeit(lambda: slow_group_mean(df), number=10)
t2 = timeit.timeit(lambda: fast_group_mean(df), number=100)
print(f'Loop: {t1/10*1000:.1f} ms')
print(f'transform: {t2/100*1000:.1f} ms')
print(f'Speedup: {(t1/10)/(t2/100):.0f}x')Quando iterrows è accettabile
Nonostante la sua lentezza, esistono usi legittimi di iterrows e itertuples: stampare o registrare nei log informazioni sulle righe (le prestazioni sono irrilevanti), costruire payload per API in cui ogni riga determina una chiamata HTTP (la latenza di rete è predominante) e eseguire il debug di righe specifiche con condizioni complesse. Se si hanno meno di 1.000 righe e l'operazione viene eseguita una sola volta, la differenza tra un ciclo e la vettorializzazione è dell'ordine dei millisecondi: non vale la pena introdurre la complessità del codice.
import pandas as pd
import numpy as np
df = pd.DataFrame({
'order_id': [101, 102, 103],
'product': ['Widget', 'Gadget', 'Doohickey'],
'amount': [29.99, 49.99, 9.99]
})
# Acceptable use: building a structured log (small data, one-time)
for _, row in df.iterrows():
print(f'Order {row["order_id"]}: {row["product"]} — ${row["amount"]:.2f}')itertuples è più veloce di iterrows
Se deve necessariamente iterare sulle righe in Python (perché non esiste un equivalente vettorializzato), utilizzi itertuples() invece di iterrows(). Restituisce ogni riga come una tupla con nome anziché come una Series Pandas, evitando il sovraccarico della costruzione di Series. In genere è quindi 5-10 volte più veloce di iterrows. Acceda ai valori tramite la notazione con attributo: row.column_name. Eviti questo approccio se i nomi delle colonne contengono spazi (che non sono nomi validi per gli attributi).
import pandas as pd
import numpy as np
import timeit
np.random.seed(0)
df = pd.DataFrame({'a': np.random.randn(10000), 'b': np.random.randn(10000)})
t1 = timeit.timeit(
lambda: [row.a + row.b for row in df.itertuples()], number=10)
t2 = timeit.timeit(
lambda: [row['a'] + row['b'] for _, row in df.iterrows()], number=10)
print(f'itertuples: {t1/10*1000:.1f} ms')
print(f'iterrows: {t2/10*1000:.1f} ms')
print(f'itertuples speedup: {t2/t1:.1f}x')Checklist dei modelli di vettorializzazione
Prima di scrivere un ciclo, si ponga queste domande:
- L'operazione è elemento per elemento su una colonna? → Utilizzi un'espressione aritmetica sulla colonna o una ufunc di NumPy.
- Coinvolge una logica condizionale? → Utilizzi
np.where(2 condizioni) onp.select(3 o più). - Suddivide i valori in intervalli? → Utilizzi
pd.cutopd.qcut. - Applica operazioni sulle stringhe? → Utilizzi l'accessor
.str. - Aggrega i dati all'interno dei gruppi? → Utilizzi
groupby().agg()ogroupby().transform().
apply() o itertuples() solo se nessuna delle soluzioni precedenti è applicabile.Esempio di miglioramento reale: calcolo del prezzo
Ecco un refactoring completo, prima e dopo, di un comune calcolo aziendale: applicare sconti a scaglioni in base alla quantità ordinata. La versione con ciclo è leggibile, ma inaccettabilmente lenta per i DataFrame di grandi dimensioni. La versione vettorializzata che utilizza np.select ottiene lo stesso risultato in un decimo del tempo.
import pandas as pd
import numpy as np
import timeit
np.random.seed(0)
df = pd.DataFrame({
'price': np.random.uniform(10, 200, 100000),
'qty': np.random.randint(1, 500, 100000)
})
# BEFORE: loop with conditionals
def slow_discount(df):
result = []
for _, row in df.iterrows():
if row['qty'] >= 100:
disc = 0.2
elif row['qty'] >= 50:
disc = 0.1
elif row['qty'] >= 10:
disc = 0.05
else:
disc = 0.0
result.append(row['price'] * (1 - disc))
return pd.Series(result)
# AFTER: np.select
def fast_discount(df):
conditions = [df['qty'] >= 100, df['qty'] >= 50, df['qty'] >= 10]
discounts = [0.20, 0.10, 0.05]
disc = np.select(conditions, discounts, default=0.0)
return df['price'] * (1 - disc)
assert slow_discount(df.head(200)).round(4).equals(fast_discount(df.head(200)).reset_index(drop=True).round(4))
print('Both versions agree!')Verifica rapida
Verifichi la propria comprensione della vettorializzazione presentata in questa lezione.
Riepilogo della lezione
In questa lezione ha imparato che iterrows è 100-1000 volte più lento delle operazioni vettorializzate, np.where e np.select sostituiscono i cicli condizionali, l'accessor .str vettorializza le operazioni sulle stringhe e groupby().transform() sostituisce l'iterazione manuale sui gruppi. Quando deve iterare, preferisca itertuples a iterrows per ottenere un miglioramento di 5-10 volte. Nella prossima lezione vedremo i tipi di dati efficienti: il downcasting dei numeri e l'utilizzo di Categorical per ridurre la memoria fino al 70%.
Domande Frequenti
La lezione «Evitare iterrows e i cicli Python» è gratuita?
Sì — il testo completo di «Evitare iterrows e i cicli Python» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso Pandas & NumPy Academy, passa a CoddyKit PRO. Il corso Pandas & NumPy Academy include 4 lezioni in totale.
Cosa imparerò in «Evitare iterrows e i cicli Python»?
Sostituisca i cicli riga per riga con operazioni vettoriali sulle colonne, np.where e pd.cut per ottenere accelerazioni da 10 a 100 volte. Eserciti Pandas & NumPy Academy con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.
Ho bisogno di esperienza per iniziare Pandas & NumPy Academy?
Non è richiesta alcuna esperienza precedente. Pandas & NumPy Academy su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 2 di 4.
Quanto tempo richiede la lezione «Evitare iterrows e i cicli Python»?
La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.
Posso scrivere ed eseguire codice in questa lezione Pandas & NumPy Academy?
Sì. Ogni lezione Pandas & NumPy Academy include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.
Tutte le lezioni di questo corso
- Profiling con timeit e memory_profiler
- Evitare iterrows e i cicli Python
- Tipi di dati efficienti per ridurre la memoria
- Lettura a blocchi di file di grandi dimensioni