Analizzare CSV Giganti in Python (Parte 1): La Guida Pratica a Pandas e chunksize

Cerca:

Generic selectors
Exact matches only
Search in title
Search in content
Post Type Selectors
come evitare MemoryError pandas

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:

  1. Pandas streaming → CSV di output (approccio rapido, “file-centric”)

  2. SQLite con UPSERT (persistenza relazionale, gestione churn)

  3. 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.

 

Pubblicità

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

import pandas as pd
from tqdm import tqdm
import os

FILE_CSV = "openpowerlifting-latest.csv"
OUTPUT_CSV = "tabella_performance.csv"
CHUNKSIZE = 100_000

COLONNE = [
    "Equipment", "Division", "BodyweightKg", "WeightClassKg",
    "Squat1Kg", "Squat2Kg", "Squat3Kg", "Squat4Kg", "Best3SquatKg",
    "Bench1Kg", "Bench2Kg", "Bench3Kg", "Bench4Kg", "Best3BenchKg",
    "Deadlift1Kg", "Deadlift2Kg", "Deadlift3Kg", "Deadlift4Kg", "Best3DeadliftKg",
    "TotalKg", "Place", "Dots"
]

if not os.path.exists(OUTPUT_CSV):
    with open(OUTPUT_CSV, "w", encoding="utf-8", newline="") as f:
        f.write("id," + ",".join(COLONNE) + "\n")

next_id = 0

for chunk in tqdm(pd.read_csv(FILE_CSV, usecols=COLONNE, chunksize=CHUNKSIZE)):
    chunk.columns = [c.strip() for c in chunk.columns]
    rows = len(chunk)
    chunk.insert(0, "id", range(next_id, next_id + rows))
    next_id += rows
    chunk.to_csv(OUTPUT_CSV, mode="a", header=False, index=False, encoding="utf-8")

Pubblicità

Spiegazione approfondita

1) Scopo generale

Forse potrebbe interessarti anche:  Pandas in Azienda: 3 Esercizi Pratici per Pulire Dati e Creare Report

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 pandas as pd
from tqdm import tqdm
import os

Import:

  • pandas per I/O e trasformazioni tabellari;

  • tqdm per la progress bar (utile su dataset grandi);

  • os per controlli sul filesystem.


FILE_CSV = "openpowerlifting-latest.csv"
OUTPUT_CSV = "tabella_performance.csv"
CHUNKSIZE = 100_000

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.


COLONNE = [
    "Equipment", "Division", "BodyweightKg", "WeightClassKg",
    ...
    "TotalKg", "Place", "Dots"
]

Lista esplicita delle colonne da leggere dal CSV.

Importante perché:

  • usecols riduce I/O e memoria leggendo solo le colonne necessarie.

  • Evita costi inutili se il CSV ha molte colonne non usate.


if not os.path.exists(OUTPUT_CSV):
    with open(OUTPUT_CSV, "w", encoding="utf-8", newline="") as f:
        f.write("id," + ",".join(COLONNE) + "\n")

Crea il file di output solo la prima volta e scrive l’header.

Dettagli:

  • os.path.exists evita 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.


next_id = 0

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).


for chunk in tqdm(pd.read_csv(FILE_CSV, usecols=COLONNE, chunksize=CHUNKSIZE)):

Ciclo principale:

  • pd.read_csv(..., chunksize=...) restituisce un iterator che carica un DataFrame alla volta (ognuno con ~CHUNKSIZE righe).

  • tqdm(...) avvolge l’iterator con una progress bar, molto utile su job lunghi.

Comportamento: non carica tutto in RAM, solo il chunk corrente.


    chunk.columns = [c.strip() for c in chunk.columns]

Pulizia dei nomi colonna: rimuove eventuali spazi precedenti o successivi alle intestazioni.


    rows = len(chunk)

Calcola il numero di righe effettive nel chunk (l’ultimo chunk potrebbe essere più piccolo).


    chunk.insert(0, "id", range(next_id, next_id + rows))
    next_id += rows

Creazione della chiave univoca:

  • chunk.insert(0, "id", range(...)) aggiunge una colonna id in prima posizione con valori next_id, next_id+1, ..., next_id+rows-1.

  • next_id viene 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.

Forse potrebbe interessarti anche:  Cos'è l'Uplift Modeling, come si calcola il CATE e perché è diverso dal Response Modeling

    chunk.to_csv(OUTPUT_CSV, mode="a", header=False, index=False, encoding="utf-8")

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,Equipment,Division,BodyweightKg,WeightClassKg,...,TotalKg,Place,Dots
0,Raw,Open,83.4,83, ... ,547.5,1,312.4
1,Single-ply,Masters 1,74.8,75, ... ,482.5,2,280.3
...
  • id: chiave numerica univoca per ogni riga nel file generato.

  • Le colonne numeriche restano in formato testuale nel CSV ma rappresentano float64 quando 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 id basati su hash stabile di colonne chiave;

  • mantenere una mappa persistente name->id salvata 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_id dall’ultimo id presente nel file di output all’avvio;

  • salvare next_id periodicamente in un file di checkpoint (es. next_id.txt o checkpoint.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={...} a read_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’id così 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, MeetID o 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.

Forse potrebbe interessarti anche:  Colloquio SQL per Data Analyst e Engineer: GROUP BY e Window Functions – Domande, Esercizi e Spiegazioni Pratiche (Parte 2)

Perché questo esercizio è interessante?

Questo snippet non è un semplice “leggi e scrivi”, ma un eccellente caso di studio per diverse ragioni:

  1. Trasforma un’operazione “Batch” in “Streaming”: Il punto focale è il parametro chunksize. Insegna al lettore che pd.read_csv non è 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”.
  2. Gestione dello Stato (Stateful Streaming): L’esercizio non è stateless (come un semplice .apply su ogni riga). L’introduzione di next_id lo 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.
  3. 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 come mode, header e index.
  4. 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 Pandas come un database: guida pratica alla gestione di chiavi primarie (PK) e integrità referenziale (FK)

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

👉 Padroneggia l’importazione dati in Python: 11 esercizi pratici con Pandas, CSV, Excel, JSON, SQL, HTML

👉 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.

Pubblicità