Guardrail per il ML Tabulare: la prospettiva di un Data Engineer su Data Leakage, Poisoning e Pipeline Fragili
Stavo sperimentando con TabICL, un large tabular model open-source, e allo stesso tempo mi è stato chiesto di “dare una rapida occhiata” a pipeline ML in produzione. Non sono un data scientist, ma passo gran parte del mio tempo tra sistemi dati, Arrow e motori di query. Quello che ho trovato — sia nei miei esperimenti sia in quelle pipeline — non erano failure esotiche del modello, ma qualcosa di molto più ordinario: pipeline tabulari fragili, che potevano rompersi in silenzio, o addirittura essere “avvelenate”, da problemi di dati molto elementari mai codificati come controlli. Il modello era la parte facile; assicurarsi che i dati in ingresso fossero davvero puliti, no.
Questo articolo è il mio tentativo di guardare il problema dal punto di vista di un data engineer: partire da un semplice script pandas, poi passare ad Apache DataFusion e Arrow per costruire guardrail che prevengano data leakage, scenari di poisoning e rotture della pipeline prima ancora che il modello veda i dati.
Il tipo di fragilità che continuavo a vedere
I problemi concreti variavano, ma avevano una forma comune:
- Set di training e valutazione che condividevano accidentalmente righe o entità identiche.
- Split temporali che in realtà non erano davvero temporali: dati “futuri” finivano nella finestra di training.
- Distribuzioni di classe che driftavano pesantemente tra training e serving senza che nessuno se ne accorgesse.
- Pipeline che addestravano serenamente su dati evidentemente corrotti, perché a monte nessuno imponeva contratti.
Per i data scientist non c’è nulla di nuovo: guide cloud sul ML e articoli pratici indicano data leakage e split sbagliati tra le cause più comuni di fallimento dei modelli in produzione. Quello che mi ha sorpreso è quanto spesso questi temi venissero ancora gestiti in modo informale: un notebook qui, una query SQL manuale là, e molta fiducia.
Ancora più evidente: quando i controlli esistevano, erano quasi sempre post-mortem. Qualcuno notava un degrado delle metriche in produzione, risaliva a un problema dati, poi scriveva un notebook una tantum per confermare l’ipotesi. Il controllo rimaneva in quel notebook, magari veniva copiato nel progetto successivo, ma non diventava mai una parte di prima classe della pipeline. Era forensica reattiva, non guardrail proattivi.
Quindi sono partito dagli strumenti che conosco meglio (Arrow, motori SQL, backend Rust) e mi sono chiesto: cosa succederebbe se trattassimo questi controlli come cittadini di prima classe della pipeline?
Step 1 — Un controllo leakage diretto con pandas
Partiamo da qualcosa di molto familiare: una utility pandas che controlla il leakage tra training set e test set.
| |
L’esempio poi genera un dataset sintetico, introduce leakage deliberato ed esegue la funzione:
| |
Cosa ottieni:
- Leakage a livello riga: righe di feature duplicate esatte tra train e test, ignorando il target.
- Entity leakage: ID condivisi (es. utenti, sessioni) tra split.
- Un segnale semplice di target drift: differenza della media del target tra train e test.
Una nota: il rilevamento dei duplicati basato su merge confronta valori di feature esatti. Per feature continue ad alta dimensionalità funziona bene (match esatti rari, a meno che i dati non siano stati copiati letteralmente), ma su dataset ricchi di variabili categoriche può produrre falsi positivi. Tienilo presente nell’interpretazione.
È già molto meglio di niente e riflette i consigli base di molti documenti di best practice ML: assicurati che gli split siano disgiunti, che le entità siano assegnate correttamente e che le distribuzioni non siano troppo diverse.
Il problema? Appena i dati superano qualche milione di righe, fare inner join e scan completi in pandas inizia a pesare. E quando i dati vivono in un data lake, portarli in memoria Python a ogni controllo diventa esso stesso il collo di bottiglia.

Step 2 — Spostare il lavoro pesante in Apache DataFusion
Osservando pipeline fragili, uno schema ricorrente era: dati in Parquet/Iceberg, ma controlli qualità in notebook pandas. Questo significa:
- Estrarre continuamente tabelle grandi da storage economico verso memoria Python costosa.
- Avere controlli ad hoc invece di integrarli nel motore che già scansiona i dati.
Apache DataFusion offre un’opzione diversa: registrare tabelle Parquet o Iceberg come relazioni logiche ed eseguire i controlli leakage come SQL direttamente sopra.
Un esempio minimale è questo:
| |
Per una tabella Iceberg, puoi fare qualcosa del genere:
| |
Qui il lavoro pesante è svolto dal motore:
- I dati restano in formati colonnari basati su Arrow.
- Join, filtri e aggregazioni vengono compilati ed eseguiti in modo efficiente.
- Python orchestra soltanto: registra, esegue, recupera un risultato piccolo.
Dal mio punto di vista orientato ai sistemi, questo è già un passo verso la robustezza: sposti i controlli allo stesso layer che esegue analytics ed ETL, invece di incapsularli in notebook sparsi.
DataFusion ha risolto il problema di performance. Ma i controlli erano ancora sparsi negli script: non c’era una singola fonte di verità su cosa significasse davvero “dato valido”.
Step 3 — Da controlli ad hoc a un ML data contract (DCE)
Una volta che hai un motore SQL in gioco, il passo successivo è naturale: descrivere le aspettative come contratto e lasciare che sia il motore a farle rispettare.
Ecco un esempio interno semplificato di “Data Contract for Evaluation” (DCE) scritto in YAML:
| |
Ognuno di questi controlli si mappa bene su SQL sopra DataFusion:
Target leakage —
SELECT corr(feature, target) FROM data. DataFusion ha l’aggregazionecorr(x, y)integrata, quindi puoi verificare correlazioni sospettosamente alte tra feature e target senza codice custom.Feature drift (es. PSI) — istogrammi in SQL. Usa
approx_percentile_contoNTILE()per definire confini per quantili, poi unaCASE WHENdi binning per contare quanti valori cadono in ogni bin.Unicità —
COUNT(*) - COUNT(DISTINCT key). Nessuna logica HashSet in Rust: il motore sa già calcolarlo su grandi volumi.Completezza —
COUNT(field) / COUNT(*). Una singola aggregazione che produce il rapporto di valori non null.Bilanciamento classi — group-by sulla label.
SELECT label, COUNT(*) * 1.0 / SUM(COUNT(*)) OVER () AS proportion FROM data GROUP BY labelrestituisce le proporzioni per classe in una sola query.
In pratica stai trasformando controlli scritti a mano in contratti dichiarativi:
- Vivono vicino alla definizione del dataset.
- Possono essere versionati e revisionati come codice.
- Sono imposti da un motore scalabile, non da un notebook messo insieme alla buona.
Nota sugli strumenti esistenti
Esistono framework maturi in questo spazio, come Great Expectations, Pandera, Deepchecks, Evidently, e risolvono bene problemi reali. Il motivo per cui ho scelto direttamente DataFusion e Arrow è che le pipeline che osservavo giravano già su questi motori per analytics ed ETL. Aggiungere un altro framework significava un altro runtime, un altro confine di serializzazione e un altro set di concetti da imparare per il team. Esprimere gli stessi controlli come contratti SQL nel motore che già toccava i dati mi è sembrata la strada a minor attrito. Naturalmente dipende dal contesto: se il tuo stack è già costruito attorno a uno di questi strumenti, usa quello.
Dal punto di vista di fragilità pipeline e poisoning
Il poisoning è molto più difficile se hai regole forti di no-intersection e temporalità su ciò che può entrare in training e valutazione. Drift e sbilanciamento diventano violazioni di contratto, non “cose che forse un umano noterà in dashboard un giorno”.
I contratti coprono bene il layer SQL. Ma alcune pipeline hanno componenti C++ legacy che devono usare gli stessi dati senza reinventare I/O. Qui entra in gioco il pezzo successivo.
Step 4 — Attraversare il confine con Arrow C Data Interface
Fin qui tutto resta al livello SQL/motore. Ma in alcune pipeline che ho visto c’erano componenti C++ legacy che facevano cose molto sofisticate sui dati: controlli di integrità custom, punteggi business-specific, ecc. Il problema era sempre lo stesso: come passargli i dati senza copiare e senza reinventare un formato binario?
L’Arrow C Data Interface è pensata esattamente per questo: un ABI stabile per scambiare array e schemi Arrow tra runtime diversi (Python, Rust, C++, R, ecc.) senza copiare buffer.
Lo snippet seguente mostra un esempio semplificato con Polars + PyArrow. Nota che l’API _export_to_c mostrata qui è un’interfaccia legacy semi-privata: l’approccio moderno è la Arrow PyCapsule Interface (__arrow_c_array__, __arrow_c_schema__), che evita la dipendenza da PyArrow e riduce i rischi di gestione della memoria. Uso l’API più vecchia qui perché rende più esplicita la meccanica di passaggio dei puntatori:
| |
Alcuni dettagli importanti:
scan_parquetcostruisce un piano lazy; i dati vengono letti solo quando chiamicollect().- Materializzi soltanto le colonne necessarie al controllo (
timestamp,user_id). _export_to_cpopola le structArrowArrayeArrowSchemacon puntatori ai buffer esistenti, senza serializzare in un formato ad hoc.
Da quel punto, una funzione C++ come validate_temporal_integrity può:
- Leggere i buffer con Arrow-C++.
- Eseguire controlli domain-specific difficili o lenti da esprimere in Python.
- Restituire un semplice status code/string tramite ctypes.
È qui che emerge il mio bias “systems”: invece di agganciare controlli qualità al modello, ottieni:
- Un data contract al layer SQL (DCE).
- Un data plane Arrow ovunque (motore, Python, C++).
- Validator specializzati che si collegano a quel data plane senza reinventare I/O.
Uno scenario concreto di poisoning

Quando si parla di “poisoning” nei sistemi ML, spesso si pensa ad esempi avversariali e attacchi a livello di gradiente. A livello pipeline, però, la maggior parte dei problemi è più noiosa e molto più comune.
Considera questo scenario: un job ETL a monte rotto duplica silenziosamente un batch di righe dove target = 1 (per esempio prestiti approvati). La duplicazione non è avversariale: è un bug di retry, una chiave di idempotenza mal configurata, una partizione rielaborata. Ma l’effetto è reale: il training set ha ora una distribuzione di classe distorta e il modello impara che le approvazioni sono più frequenti di quanto siano davvero. In produzione, inizia ad approvare prestiti che non dovrebbe.
Questo è “poisoning” in senso pratico: non un attacco sofisticato, ma dati corrotti che degradano il comportamento del modello. Ed è prevenibile con i guardrail descritti sopra:
- Il contratto
no_intersectionintercetta righe duplicate se sono finite anche nell’eval set. - Il controllo
categorical_distributionintercetta lo shift di sbilanciamento tra classi: setarget = 1passa improvvisamente dal 50% al 70% del training, scattano le sogliemin_frequency/ max-frequency. - Il controllo
temporal_splitintercetta partizioni rielaborate se le righe duplicate hanno timestamp che violano l’ordinamento temporale.
Nessuno di questi controlli richiede di sapere che è avvenuto un attacco. Sono invarianti strutturali: se i dati li violano, la pipeline si ferma e chiede a un umano di investigare.
Conclusione: dal post-mortem al proattivo
Questo non è un articolo su quale classificatore scegliere o su come fare tuning degli iperparametri. È il punto di vista di chi ragiona in termini di buffer Arrow, motori di query e confini ABI, ed è stato catapultato in pipeline ML vedendole fallire per ragioni evitabili.
Il pattern che ho visto più spesso è: i controlli esistono, ma sono post-mortem. Qualcuno nota una regressione in produzione, apre un notebook, conferma il problema dati, lo corregge e va avanti. Il controllo non diventa mai infrastruttura. Il trimestre dopo, un problema simile colpisce un’altra pipeline e il ciclo si ripete.
La ricetta proposta qui è volutamente semplice:
- Parti da qualcosa come
evaluate_data_leakagecome modello mentale e rete di sicurezza nei notebook. - Sposta i controlli pesanti in un motore SQL come DataFusion, operando direttamente su Parquet/Iceberg.
- Codifica le aspettative come contratti (DCE) che il motore può imporre in continuo, non solo in fase di training.
- Dove serve, collega componenti C++ legacy o ad alte prestazioni tramite Arrow C Data Interface invece di duplicare pipeline.
Cosa non copre questo approccio: validazione a livello modello (attacchi avversariali basati su gradiente, audit di fairness, sensibilità agli iperparametri). Sono temi reali che richiedono strumenti diversi. Qui il focus è specificamente sul data plane: fare in modo che il modello non veda mai dati che violano invarianti strutturali di base.
Per team già investiti in Arrow, Rust o architetture lakehouse, questa è una strada a basso attrito per aggiungere guardrail robusti alle pipeline ML senza dover diventare data scientist prima.
