Pandas & NumPy Academy · Lezione

Profiling con timeit e memory_profiler

Misuri il tempo di esecuzione con %timeit e il picco di memoria con memory_profiler per individuare le parti più lente della pipeline.

Lezione 1 di 413 passaggi

Profiling con timeit e memory_profiler è una lezione Pandas & NumPy Academy gratuita su CoddyKit. Questa è la lezione 1 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.

Perché profilare prima di ottimizzare?

Un errore comune consiste nell'ottimizzare il codice basandosi sull'intuizione, riscrivendo una funzione che sembra lenta senza misurare se sia davvero il collo di bottiglia. Nelle pipeline di dati, spesso il 90% del tempo di esecuzione proviene dal 10% del codice. La profilazione identifica esattamente dove vengono consumati tempo e memoria, consentendoLe di concentrare gli sforzi di ottimizzazione dove sono più utili. La regola d'oro è: misurare prima, ottimizzare dopo. Strumenti come timeit misurano il tempo, mentre memory_profiler misura la RAM.

import pandas as pd
import numpy as np
import timeit

# Naive approach vs vectorised approach — which is faster?
np.random.seed(0)
df = pd.DataFrame({'a': np.random.randn(100000), 'b': np.random.randn(100000)})

t1 = timeit.timeit(lambda: df['a'] + df['b'], number=100)
t2 = timeit.timeit(lambda: [a + b for a, b in zip(df['a'], df['b'])], number=10)

print(f'Vectorised (100 runs): {t1:.3f}s')
print(f'List comprehension (10 runs): {t2:.3f}s')
print(f'Per-run speedup: ~{(t2/10)/(t1/100):.0f}x')

timeit: il timer integrato di Python

Il modulo timeit misura il tempo di esecuzione eseguendo un'istruzione molte volte e restituendo il tempo totale trascorso. timeit.timeit(stmt, number=n) esegue stmt n volte e restituisce il totale in secondi. Divida per number per ottenere il tempo per esecuzione. Per misurazioni accurate, scelga number in modo che l'esecuzione totale duri almeno 0,1 secondi: una singola esecuzione di un'operazione rapida non è rappresentativa a causa del rumore dovuto alla pianificazione del sistema.

import timeit
import pandas as pd
import numpy as np

np.random.seed(0)
df = pd.DataFrame({'x': np.random.randn(10000)})

# Compare three ways to square a column
time_a = timeit.timeit(lambda: df['x'] ** 2, number=1000)
time_b = timeit.timeit(lambda: df['x'].apply(lambda v: v**2), number=100)
time_c = timeit.timeit(lambda: np.square(df['x']), number=1000)

print(f'Operator **2  per run: {time_a/1000*1000:.3f} ms')
print(f'.apply(v**2) per run: {time_b/100*1000:.3f} ms')
print(f'np.square()  per run: {time_c/1000*1000:.3f} ms')

La magic %timeit in Jupyter

Nei notebook Jupyter, la magic %timeit seleziona automaticamente il numero di ripetizioni e di esecuzioni per fornire una stima statisticamente stabile, quindi riporta i tempi migliore e medio con la deviazione standard. È il modo più pratico per eseguire rapidamente il benchmark di una singola riga. Utilizzi %%timeit (doppio simbolo di percentuale) per misurare un'intera cella. Il formato dell'output è: X ns/μs/ms ± Y ns per ciclo (media ± dev. standard di 7 esecuzioni, N cicli ciascuna).

# In Jupyter notebook:
# %timeit df['x'] ** 2
# Output: 47.3 us +- 1.2 us per loop (mean +- std. dev. of 7 runs, 10000 loops each)

# Multi-line cell timing:
# %%timeit
# result = df['x'] ** 2
# total = result.sum()

# In a regular Python script, use timeit.timeit() instead:
import timeit
import pandas as pd, numpy as np
df = pd.DataFrame({'x': np.random.randn(10000)})
result = timeit.repeat(lambda: df['x']**2, number=1000, repeat=7)
print(f'Best run: {min(result)/1000*1000:.3f} ms per call')

cProfile per la profilazione a livello di funzione

Mentre timeit misura una singola espressione, cProfile analizza l'intero stack di chiamate: mostra quanto tempo viene impiegato in ogni funzione chiamata durante l'esecuzione. È essenziale per identificare il collo di bottiglia in una pipeline complessa. Esegua python -m cProfile -s cumulative script.py dalla riga di comando oppure utilizzi cProfile.run('function()') in uno script. In Jupyter, utilizzi %prun function().

import cProfile
import pandas as pd
import numpy as np

np.random.seed(0)
df = pd.DataFrame({'a': np.random.randn(50000), 'b': np.random.randn(50000)})

def slow_pipeline(df):
    result = []
    for _, row in df.iterrows():
        result.append(row['a'] * row['b'])
    return pd.Series(result)

# Profile the function
print('Profile output (top 5 functions by cumulative time):')
cProfile.run('slow_pipeline(df.head(1000))', sort='cumulative')

Misurazione dell'utilizzo della memoria

Utilizzi df.memory_usage(deep=True) per vedere quanta RAM consuma ogni colonna. L'argomento deep=True misura la memoria effettivamente utilizzata dalle colonne di tipo object (stringhe), che altrimenti riporterebbero soltanto la dimensione del puntatore. Utilizzi .sum() per ottenere la memoria totale del DataFrame in byte. Divida per 1e6 per ottenere i MB o per 1e9 per ottenere i GB. Questo è il punto di partenza per ottimizzare la memoria: non è possibile ridurre ciò che non è stato misurato.

import pandas as pd
import numpy as np

np.random.seed(0)
df = pd.DataFrame({
    'id': range(100000),
    'name': ['user_' + str(i) for i in range(100000)],
    'score': np.random.randn(100000),
    'category': np.random.choice(['A', 'B', 'C', 'D'], 100000)
})

mem = df.memory_usage(deep=True)
print('Memory per column (bytes):')
print(mem)
print(f'\nTotal: {mem.sum() / 1e6:.2f} MB')

memory_profiler per la memoria riga per riga

Il pacchetto memory_profiler (pip install memory-profiler) misura l'utilizzo della RAM riga per riga all'interno di una funzione. Decori una funzione con @profile ed esegua lo script con python -m memory_profiler script.py. In Jupyter, utilizzi la magic %memit (dall'estensione ipython) per misurare la memoria di picco di una singola espressione. In questo modo è possibile identificare quali strutture dati causano improvvisi picchi di memoria, spesso dovuti alla concatenazione all'interno di un ciclo invece che alla raccolta dei dati seguita da un'unica concatenazione.

# Install: pip install memory-profiler
# Usage as a decorator:

# from memory_profiler import profile
# @profile
# def process_data():
#     import pandas as pd, numpy as np
#     df = pd.DataFrame({'x': np.random.randn(1000000)})
#     result = df['x'].rolling(100).mean()    # <-- sees if this spikes RAM
#     return result

# Run: python -m memory_profiler script.py

# In Jupyter (after %load_ext memory_profiler):
# %memit pd.DataFrame({'x': np.random.randn(1000000)}).rolling(100).mean()

print('memory_profiler decorates functions for line-by-line RAM measurement.')
print('Typical output: Line # Mem usage Increment Line Contents')

sys.getsizeof e memory_usage per gli oggetti

sys.getsizeof(obj) restituisce la dimensione in memoria di un oggetto Python, ma per i DataFrame pandas riporta soltanto l'overhead del contenitore (alcune centinaia di byte), non i dati effettivi. Per i DataFrame utilizzi sempre df.memory_usage(deep=True).sum(). Per singoli oggetti Python come liste e dizionari, sys.getsizeof è accurato, ma non analizza ricorsivamente gli oggetti annidati: utilizzi la libreria pympler (asizeof) per ottenere le dimensioni complete degli oggetti.

import sys
import pandas as pd
import numpy as np

df = pd.DataFrame({'x': np.random.randn(100000)})

# sys.getsizeof reports only the container metadata — misleading!
print('sys.getsizeof(df):', sys.getsizeof(df), 'bytes (misleading!)')

# memory_usage gives the true memory
true_mem = df.memory_usage(deep=True).sum()
print('df.memory_usage(deep=True).sum():', true_mem, 'bytes')
print(f'True memory: {true_mem/1e6:.2f} MB')

Pattern del context manager per la misurazione dei tempi

Per i passaggi più lunghi di una pipeline, in cui timeit non è adatto (ad esempio query al database o operazioni di I/O su file), utilizzi un semplice context manager con time.perf_counter(). Racchiuda ogni passaggio della pipeline in un contesto di temporizzazione per registrarne la durata. È più pratico di timeit nel codice di produzione perché registra il tempo reale trascorso, inclusa l'attesa per l'I/O, che timeit misura a sua volta ma che è più difficile da integrare con i framework di logging.

import time
import contextlib
import pandas as pd
import numpy as np

@contextlib.contextmanager
def timer(name):
    start = time.perf_counter()
    yield
    elapsed = time.perf_counter() - start
    print(f'[{name}] {elapsed*1000:.1f} ms')

np.random.seed(0)
df = pd.DataFrame({'x': np.random.randn(500000), 'y': np.random.randn(500000)})

with timer('rolling mean'):
    result1 = df['x'].rolling(30).mean()

with timer('groupby mean'):
    df['group'] = df['x'].round(0)
    result2 = df.groupby('group')['y'].mean()

Profilazione sistematica delle operazioni Pandas

Un flusso di lavoro sistematico per la profilazione: 1) inizi con un campione rappresentativo dei dati (10.000 righe sono sufficienti per confrontare i tempi). 2) identifichi i passaggi più lenti usando un contesto timer o cProfile. 3) riscriva il passaggio più lento utilizzando un approccio più veloce (vettorializzazione, dtype Categorical, estensione C). 4) verifichi la correttezza con istruzioni assert. 5) misuri nuovamente i tempi per confermare il miglioramento. 6) ripeta il processo per il collo di bottiglia successivo finché la pipeline non soddisfa il proprio SLA.

import pandas as pd
import numpy as np
import timeit

np.random.seed(0)
df = pd.DataFrame({
    'value': np.random.randn(100000),
    'category': np.random.choice(['A', 'B', 'C', 'D', 'E'], 100000)
})

# Benchmark two groupby implementations
t1 = timeit.timeit(lambda: df.groupby('category')['value'].mean(), number=100)
df2 = df.copy()
df2['category'] = df2['category'].astype('category')
t2 = timeit.timeit(lambda: df2.groupby('category')['value'].mean(), number=100)

print(f'Object dtype groupby: {t1/100*1000:.2f} ms per call')
print(f'Categorical dtype:    {t2/100*1000:.2f} ms per call')
print(f'Speedup: {t1/t2:.1f}x')

Quando eseguire la profilazione: soglie pratiche

Non tutti gli script richiedono la profilazione. Investire nella profilazione è opportuno quando: una pipeline impiega più di 30 secondi per essere eseguita e viene eseguita frequentemente (ogni giorno o più), una funzione elabora più di 100.000 righe oppure l'utilizzo della memoria supera il 50% della RAM disponibile. Per gli script eseguiti una sola volta o in pochi secondi, la leggibilità è più importante delle ottimizzazioni al microsecondo. Il costo di manutenzione di codice molto ottimizzato ma poco chiaro spesso supera il tempo risparmiato dall'ottimizzazione.

Istantanea: riepilogo degli strumenti di profilazione

Un riferimento rapido:

  • timeit.timeit — misura il tempo di una singola istruzione (script)
  • %timeit / %%timeit — misura il tempo in Jupyter (ripetendo automaticamente)
  • %prun / cProfile — profilazione dello stack delle chiamate a livello di funzione
  • df.memory_usage(deep=True) — misurazione della RAM per colonna
  • memory_profiler @profile — misurazione della RAM riga per riga
  • time.perf_counter() — timer manuale per i passaggi della pipeline
Esegua sempre la profilazione su un campione rappresentativo dei dati e confermi le ottimizzazioni con controlli assert prima di passare alla produzione.

Verifica rapida

Verifichi la propria comprensione degli strumenti di profilazione presentati in questa lezione.

Riepilogo della lezione

In questa lezione ha imparato che timeit.timeit e %timeit misurano il tempo di esecuzione di operazioni specifiche, df.memory_usage(deep=True) misura accuratamente la RAM per colonna e cProfile / memory_profiler forniscono una profilazione più approfondita per le pipeline complesse. Esegua sempre la profilazione prima di ottimizzare e verifichi la correttezza dopo ogni modifica. Nella prossima lezione vedremo come evitare i lenti cicli iterrows e sostituirli con operazioni vettorializzate.

Gratis per iniziare

Impara Python con un tutor IA — gratis

Scrivi ed esegui vero codice nel tuo browser, ricevi aiuto istantaneo da un tutor IA disponibile 24/7, e riprendi da dove hai lasciato sul web o nell'app.

Corsi
30
Lezioni
120

Domande Frequenti

La lezione «Profiling con timeit e memory_profiler» è gratuita?

Sì — il testo completo di «Profiling con timeit e memory_profiler» è 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 «Profiling con timeit e memory_profiler»?

Misuri il tempo di esecuzione con %timeit e il picco di memoria con memory_profiler per individuare le parti più lente della pipeline. 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 1 di 4.

Quanto tempo richiede la lezione «Profiling con timeit e memory_profiler»?

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

  1. Profiling con timeit e memory_profiler
  2. Evitare iterrows e i cicli Python
  3. Tipi di dati efficienti per ridurre la memoria
  4. Lettura a blocchi di file di grandi dimensioni
← Torna a Pandas & NumPy Academy