Ti è mai capitato? Hai un archivio CSV che sembra innocuo, magari un log di eventi o un export di dati storici. Pensi di caricarlo con un semplice pd.read_csv, ma dopo qualche minuto di attesa, il kernel del tuo notebook si pianta. MemoryError. Il portatile freeza e la ventola inizia a girare all’impazzata.
È un classico: il file è “solo un po’ troppo grande” per la RAM a disposizione.
Non sei il solo, e no, non devi subito comprare un server più potente. Spesso, la soluzione è cambiare approccio, non hardware. Che tu sia un data analyst che deve pulire un export, un ricercatore che analizza log o uno sviluppatore che prepara un dataset, la gestione efficiente dei file di grandi dimensioni è un’abilità fondamentale.
In questa guida, partiamo dal problema più comune – un file troppo grosso per Pandas – e vediamo la prima, fondamentale, tecnica per domarlo: lo streaming.
Introduzione: l’archivio che vogliamo analizzare
Per rendere l’esempio concreto, prendiamo come caso di studio il bulk CSV di OpenPowerlifting — un dataset pubblico che raccoglie migliaia o milioni di record su gare, atleti e prestazioni (il pacchetto “bulk CSV” contiene decine/centinaia di MB fino a qualche GB a seconda della versione). Anche se il caso è specifico, le tecniche che descriveremo si applicano a qualsiasi archivio CSV molto grande (log di eventi, dati finanziari storici, clickstream, tabelle di produttività industriale, ecc.).
Problemi tipici quando si lavora con questi archivi:
-
memoria RAM insufficiente per caricare tutto in un
DataFrame; -
bisogno di operazioni incrementali (aggiornare, aggiungere, eliminare dati nel tempo — il cosiddetto churn);
-
necessità di avere ID stabili e relazioni tra tabelle;
-
efficienza I/O e CPU (leggibilità vs velocità).
Nel seguito esaminiamo tre strategie pratiche, ordinate dal più semplice al più performante/robusto:
-
Pandas streaming → CSV di output (approccio rapido, “file-centric”)
-
SQLite con UPSERT (persistenza relazionale, gestione churn)
-
Polars + SQLite + UPSERT (performance, efficienza memoria, pipeline moderna)
Per ciascuna versione troverai codice esempio, comportamento in termini di RAM/IO/CPU, punti deboli e quando preferirla.
Versione 1 — Pandas in streaming, output CSV (semplice e portabile)
Quando usarla
-
dataset “solo un po’ troppo grande” per la memoria ma ancora gestibile con chunking;
-
pipeline ad uso one-shot o proof-of-concept;
-
preferisci file CSV come artefatto finale.
Idea chiave
Leggere il CSV a blocchi (chunksize), trasformare ogni blocco e scriverlo incrementalmente su disco (append). Mantieni in RAM solo il chunk corrente.
Snippet essenziale
Spiegazione approfondita
1) Scopo generale
Lo script legge un CSV,FILE_CSV = "openpowerlifting-latest.csv", che è molto grande a blocchi (chunks), estrae alcune colonne, aggiunge una colonna id incrementale per ogni riga e scrive i blocchi su un file di output in modalità append.
È pensato per costruire un file tabella_performance.csv senza caricare l’intero CSV in memoria.
2) Blocchi di codice — spiegazione riga per riga
Import:
-
pandasper I/O e trasformazioni tabellari; -
tqdmper la progress bar (utile su dataset grandi); -
osper controlli sul filesystem.
Configurazione:
-
FILE_CSV: percorso del file sorgente. -
OUTPUT_CSV: file CSV risultante che conterrà la tabella finale. -
CHUNKSIZE: numero di righe che Pandas leggerà per volta. Qui 100k è un valore bilanciato per macchine con ~8–16 GB RAM; va adattato al tuo hardware.
Lista esplicita delle colonne da leggere dal CSV.
Importante perché:
-
usecolsriduce I/O e memoria leggendo solo le colonne necessarie. -
Evita costi inutili se il CSV ha molte colonne non usate.
Crea il file di output solo la prima volta e scrive l’header.
Dettagli:
-
os.path.existsevita di riscrivere l’intestazione se il file già esiste (utile per ripartenze). -
newline=""è buona pratica per evitare problemi di newline doppi su Windows. -
encoding="utf-8"per compatibilità con caratteri internazionali.
Variabile che terrà il prossimo valore numerico da assegnare agli id.
Qui si parte da 0; se vuoi riprendere da uno stato precedente, dovresti leggere l’ultimo id presente nel file di output (vedi suggerimenti più avanti).
Ciclo principale:
-
pd.read_csv(..., chunksize=...)restituisce un iterator che carica unDataFramealla volta (ognuno con ~CHUNKSIZErighe). -
tqdm(...)avvolge l’iterator con una progress bar, molto utile su job lunghi.
Comportamento: non carica tutto in RAM, solo il chunk corrente.
Pulizia dei nomi colonna: rimuove eventuali spazi precedenti o successivi alle intestazioni.
Calcola il numero di righe effettive nel chunk (l’ultimo chunk potrebbe essere più piccolo).
Creazione della chiave univoca:
-
chunk.insert(0, "id", range(...))aggiunge una colonnaidin prima posizione con valorinext_id, next_id+1, ..., next_id+rows-1. -
next_idviene incrementato per mantenere continuità col chunk successivo.
Nota importantissima: questi id sono sequenziali basati sulla posizione nel flusso di lettura — dipendono dall’ordine delle righe nel CSV e dal numero di righe lette prima.
Non sono stabili se:
-
il CSV viene riorganizzato;
-
rimuovi/aggiungi righe rispetto a una precedente esecuzione.
Scrive il chunk nel file di output in append:
-
mode="a": aggiunta; non sovrascrive. -
header=False: l’intestazione è già stata scritta prima del loop. -
index=False: evitiamo la colonna indice di Pandas.
3) Cosa ottieni esattamente (output)
Un file tabella_performance.csv con formato:
-
id: chiave numerica univoca per ogni riga nel file generato. -
Le colonne numeriche restano in formato testuale nel CSV ma rappresentano
float64quando lette con Pandas.
4) Peculiarità, punti di attenzione e rischi
A. ID non stabile nel tempo
Come detto, gli id dipendono dall’ordine e dal numero di righe. Se ti serve stabilità (per join con altre tabelle o per successivi upsert), non usare questa strategia così com’è. Alternative:
-
creare
idbasati su hash stabile di colonne chiave; -
mantenere una mappa persistente
name->idsalvata su disco (pickle, DB).
B. Ripartenza / checkpoint
Se il processo si interrompe (power loss, crash), ripartire da zero genererà nuovi id. Strategie consigliate:
-
leggere
next_iddall’ultimoidpresente nel file di output all’avvio; -
salvare
next_idperiodicamente in un file di checkpoint (es.next_id.txtocheckpoint.pkl).
C. Dtypes e consistenza
-
Pandas inferisce i tipi per ogni chunk: se la stessa colonna ha tipi differenti tra chunk (es. numeri + stringhe), potresti avere incoerenze. Se conosci i tipi, passali con
dtype={...}aread_csv. -
Valori mancanti (NaN) rimangono: verifica se servono filling o cast.
D. Performance I/O e CHUNKSIZE
-
CHUNKSIZE troppo piccolo → overhead I/O e più commit/append (più lento).
-
CHUNKSIZE troppo grande → uso RAM elevato, rischio swap. Regola in base alla RAM disponibile (vedi playbook che abbiamo già preparato).
E. Locking / concorrenza
-
Questo script scrive su un singolo file in append: non è thread-safe se esegui più processi in parallelo che scrivono lo stesso file.
-
Evita scritture concorrenti su OUTPUT_CSV o usa un DB SQLite/Parquet per accesso concorrente più robusto.
F. Dimensione del file di output
Output in CSV non compresso può essere molto grande. Per analisi successive, preferisci Parquet (colonnare, compresso) o comunque gzip compressione (to_csv(..., compression="gzip")).
Pro e contro
Pro
-
Semplice da implementare.
-
Non serve un DB; il risultato è un file CSV portabile.
-
Controllo completo sul formato di uscita.
Contro
-
L’
idcosì generato dipende dall’ordine dei record e dal numero di righe processate prima — non è stabile al riordino o a estrazioni successive (problema in presenza di churn). -
Difficile gestire aggiornamenti/merge: se reinserisci lo stesso CSV ottieni duplicati.
-
Operazioni di ricerca/aggregazione successive sul CSV possono essere lente.
Consigli pratici
-
Se ti serve stabilità degli ID, usa hashing su colonne chiave (es.
AthleteID,MeetIDo concatenazione di colonne significative). -
Mantieni un piccolo file di checkpoint (
next_id) per ripartire senza ricominciare da zero. -
Salva il risultato finale in Parquet se pianifichi analisi future: Parquet è compresso e columnar — più veloce da leggere per colonne selezionate.
Perché questo esercizio è interessante?
Questo snippet non è un semplice “leggi e scrivi”, ma un eccellente caso di studio per diverse ragioni:
- Trasforma un’operazione “Batch” in “Streaming”: Il punto focale è il parametro
chunksize. Insegna al lettore chepd.read_csvnon è monolitico, ma può diventare un iteratore. Questo cambia radicalmente il modello mentale di utilizzo di Pandas, da “tutto in memoria” a “un pezzo alla volta”. - Gestione dello Stato (Stateful Streaming): L’esercizio non è stateless (come un semplice
.applysu ogni riga). L’introduzione dinext_idlo rende stateful. Deve ricordare un valore (l’ID massimo) e passarlo all’iterazione successiva. Questo è un assaggio di sfide molto più complesse tipiche dell’elaborazione di flussi di dati. - Costruzione manuale dell’Output: L’esercizio costringe a pensare a come è fatto un file CSV. Non si usa un semplice
df.to_csv(FILE_OUTPUT), ma si deve gestire la scrittura dell’intestazione (una sola volta) e l’accodamento dei dati (molte volte), prestando attenzione a parametri comemode,headereindex. - Introduce il concetto di “ID Fragile”: Il punto più importante. L’articolo usa saggiamente questo esercizio per mostrare una soluzione che sembra funzionare (crea ID univoci) ma che è estremamente fragile nel mondo reale. L’ID generato è posizionale e dipende dall’ordine del file sorgente. Questo crea il “problema” (la necessità di ID stabili) che le Parti 2 e 3 della guida andranno a risolvere.
🔹 Approfondisci: Database & Pandas
Importante Progettare database relazionali: la guida pratica dall’ERD all’integrità dei dati
Importante Normalizzazione database: guida pratica per sviluppatori con esempio reale
👉 Guida pratica: creare una Primary Key (PK) univoca con pandas.factorize, anche da colonne multiple
👉 Pandas: importare i dati da un file di tipo CSV
Prossimo passo: Quando il CSV non basta più
L’approccio streaming su CSV è un salvavita per operazioni one-shot e per dataset “appena un po’ troppo grandi”. Ma cosa succede quando i requisiti cambiano?
- E se avessimo bisogno di aggiornare solo 100 righe nel mezzo del file senza riscrivere tutto?
- E se volessimo garantire ID davvero stabili che sopravvivano a riordini e aggiunte (churn)?
- E se più processi dovessero leggere o scrivere i dati contemporaneamente?
È qui che il nostro fidato CSV mostra i suoi limiti. Nella Parte 2, faremo un salto di qualità: abbandoneremo il “file-centric” per abbracciare il “database-centric”, introducendo SQLite per gestire persistenza, relazioni e aggiornamenti (UPSERT) in modo robusto.



