15  Flusso di lavoro per la pulizia dei dati

Dal caos al dataset analizzabile

Perché la pulizia dei dati è cruciale

“Data scientists spend 80% of their time cleaning data and 20% complaining about cleaning data.” — Anonimo (ma tristemente accurato)

Sebbene la fase più gratificante di un’analisi sia quella di trarre conclusioni e rispondere alle domande di ricerca, è innegabile che la parte più sostanziosa del lavoro risieda nella preparazione e nella pulizia dei dati. Questo passaggio, talvolta considerato “di routine”, è in realtà il fondamento di qualsiasi studio valido, poiché ne determina in modo decisivo la qualità, l’affidabilità e la chiarezza interpretativa.

Trascurare o svolgere frettolosamente la pulizia dei dati può compromettere l’intero processo analitico. Un dataset non accuratamente curato può generare risultati distorti, introdurre errori difficili da individuare a posteriori e, soprattutto, portare a conclusioni fuorvianti o errate. Investire tempo in questa fase non è quindi una mera formalità, ma una scelta metodologica essenziale per garantire rigore e credibilità ai propri risultati.

ImportantePrincipio fondamentale

Non modificare mai i dati originali. Lavora esclusivamente su copie e conserva sempre i dati grezzi intatti nella cartella data/raw/.

Prerequisiti

here::here("code", "_common.R") |> 
  source()

# Load packages
if (!requireNamespace("pacman")) install.packages("pacman")
pacman::p_load(janitor, skimr, pointblank, mice, labelled, stringr)

15.1 Un framework operativo in quattro fasi

Per affrontare in modo sistematico la fase di pulizia dei dati, non ci limiteremo a un elenco frammentario di operazioni, ma adotteremo un quadro strutturato organizzato in quattro macro-fasi:

  1. Ispeziona: comprendere la struttura, il contenuto e le caratteristiche iniziali del dataset.
  2. Pulisci: correggere errori strutturali, formati errati e incongruenze semantiche.
  3. Valida: verificare che i dati rispettino vincoli, regole di dominio e coerenza interna.
  4. Documenta: garantire trasparenza, riproducibilità e facilità di manutenzione futura.
Fase Obiettivo principale Strumenti e approcci tipici
Ispeziona Acquisire una comprensione iniziale della struttura e dei contenuti dei dati. glimpse(), skim(), summary(), esplorazione visiva.
Pulisci Correggere errori formali, standardizzare valori e risolvere problemi di qualità. janitor, dplyr/tidyr, tecniche di imputazione (es. mice).
Valida Assicurarsi che i dati puliti rispettino regole di business e vincoli attesi. Pacchetto pointblank, assertazioni logiche, controlli incrociati.
Documenta Rendere esplicite tutte le trasformazioni e le scelte effettuate per garantire riproducibilità. README del progetto, dizionario dati, commenti nel codice, script annotati.

15.2 Checklist operativa

Prima di procedere con l’analisi dei dati, è fondamentale condurre una verifica sistematica della loro qualità. Il controllo può essere organizzato attorno ai seguenti ambiti chiave:

NotaChecklist di data cleaning

Struttura Verificare che il dataset abbia la forma attesa: numero di righe e colonne conforme alle previsioni, nomi delle variabili chiari, consistenti e ben formattati, e file salvato in un formato corretto e leggibile.

Completezza Identificare e gestire dati duplicati, valori mancanti (sia espliciti che impliciti) e record incompleti o parziali che potrebbero inficiare l’analisi.

Validità Accertarsi che ogni variabile sia del tipo corretto (numerico, testuale, data, ecc.), che i valori rientrino in intervalli o range plausibili, e che le categorie utilizzate siano tutte valide e coerenti.

Coerenza Controllare l’uniformità nelle unità di misura, la consistenza nei formati di data e ora, e l’effettiva univocità degli identificatori.

Privacy e etica Applicare i necessari trattamenti per la protezione dei dati personali, rimuovendo o anonimizzando le informazioni identificative, e garantire la conformità con il consenso informato ottenuto e con la normativa vigente, come il GDPR.

15.3 Caso di studio: questionario di Math Self-Efficacy

Per illustrare in pratica il processo di pulizia dei dati, utilizzeremo un dataset realistico proveniente da uno studio longitudinale randomizzato e controllato (RCT). La ricerca ha coinvolto diverse scuole, assegnate casualmente a un gruppo di trattamento o di controllo, ed è stato somministrato un questionario per valutare l’autoefficacia in matematica, composto da quattro item con risposte su scala Likert da 1 a 4.

I dati, raccolti inizialmente su carta e successivamente digitalizzati, presentano le tipiche imperfezioni di questo tipo di acquisizione. Il nostro obiettivo è applicare il framework presentato per pulire e preparare i dati della prima rilevazione dello studio.

15.3.1 Struttura del progetto

Il lavoro è organizzato per garantire trasparenza e riproducibilità:

project/
├── data/
│   ├── raw/        # Dati originali e immutabili
│   └── processed/  # Dataset puliti e pronti per l'analisi
├── scripts/
│   └── 01_data_cleaning.R  # Codice di pulizia
└── README.md                # Documentazione del progetto

15.4 FASE 1 · Ispeziona

15.4.1 Importazione e primo controllo

Il primo passo consiste nell’importare i dati grezzi e osservarne la struttura di base.

svy_raw <- rio::import(
  here("data", "w1_mathproj_stu_svy_raw.csv")
)

glimpse(svy_raw)
#> Rows: 6
#> Columns: 7
#> $ stu_id      <int> 1347, 1368, 1377, 1387, 1347, 1399
#> $ svy_date    <IDate> 2023-02-13, 2023-02-13, 2023-02-13, 2023-02-13, 2023-02-14…
#> $ grade_level <int> 9, 10, 9, 11, 9, 12
#> $ math1       <int> 2, 3, 4, 3, 2, 4
#> $ math2       <chr> "1", "2", "\n4", "3", "2", "1"
#> $ math3       <int> 3, 2, 4, NA, 4, 3
#> $ math4       <int> 3, 2, 4, NA, 2, 1

In questa fase è utile porsi alcune domande guida: il numero di osservazioni è plausibile rispetto al disegno di studio? Le variabili presenti corrispondono a quelle previste dal questionario? I tipi di dato (numerico, testuale, data) sono quelli attesi?

15.4.2 Esplorazione sintetica

Per avere una panoramica più dettagliata si utilizza una funzione di riepilogo esplorativo.

skim(svy_raw)
Data summary
Name svy_raw
Number of rows 6
Number of columns 7
_______________________
Column type frequency:
character 1
Date 1
numeric 5
________________________
Group variables None

Variable type: character

skim_variable n_missing complete_rate min max empty n_unique whitespace
math2 0 1 1 2 0 4 0

Variable type: Date

skim_variable n_missing complete_rate min max median n_unique
svy_date 0 1 2023-02-13 2023-02-14 2023-02-13 2

Variable type: numeric

skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
stu_id 0 1.00 1370.8 21.15 1347 1352.25 1372.5 1384.50 1399 ▇▁▇▃▃
grade_level 0 1.00 10.0 1.26 9 9.00 9.5 10.75 12 ▇▂▁▂▂
math1 0 1.00 3.0 0.89 2 2.25 3.0 3.75 4 ▇▁▇▁▇
math3 1 0.83 3.2 0.84 2 3.00 3.0 4.00 4 ▃▁▇▁▇
math4 1 0.83 2.4 1.14 1 2.00 2.0 3.00 4 ▃▇▁▃▃

È importante prestare attenzione a: quantità e distribuzione dei valori mancanti; valori minimi e massimi di ciascuna variabile; eventuali pattern insoliti o sospetti.

AvvisoSegnali di allarme comuni

Durante l’ispezione, alcuni elementi richiedono particolare cautela:

  • Nomi di colonne generici (es. X1, V2);
  • Presenza di righe duplicate identiche;
  • Uso di codici speciali per indicare valori mancanti (es. 999, -99, "NULL");
  • Date importate come testo anziché come oggetti data.

15.5 FASE 2 · Pulisci

La pulizia deve essere eseguita seguendo un ordine logico, per evitare di introdurre nuovi errori durante la correzione di quelli esistenti.

15.5.1 Gestione dei duplicati

Se presenti record duplicati, è necessario definirne una gestione esplicita.

svy <- svy_raw |>
  arrange(svy_date) |>           # Ordina per data
  distinct(stu_id, .keep_all = TRUE) # Mantiene l'ultima osservazione per ID

Decisione documentata: in questo caso si è scelto di conservare il record più recente per ogni studente, ordinando per data di compilazione.

15.5.2 Standardizzazione dei nomi delle variabili

Utilizzare nomi chiari e coerenti migliora la leggibilità e previene errori.

svy <- svy |> janitor::clean_names()

Il risultato sarà un set di nomi in formato snake_case, tutti in minuscolo e privi di caratteri speciali.

15.5.3 De-identificazione

La protezione della privacy è un passaggio imprescindibile. Vanno rimosse tutte le informazioni potenzialmente identificative prima di procedere con l’analisi.

svy <- svy |> select(-svy_date)

15.5.4 Correzione dei tipi di dato e dei valori

A volte i valori necessitano di pulizia, ad esempio rimuovendo caratteri di formattazione indesiderati.

svy <- svy |>
  mutate(
    math2 = str_remove_all(math2, "\\n"), # Rimuove interruzioni di riga
    math2 = as.numeric(math2)             # Converte in numero
  )

15.5.5 Gestione dei valori mancanti

Nel nostro caso i valori mancanti sono pochi e il pattern è plausibilmente Missing at Random (MAR).1 Si procede con un’imputazione singola tramite Predictive Mean Matching (PMM).2

imputed <- mice(
  svy |> select(starts_with("math")),
  m = 1, method = "pmm", seed = 123
)
#> 
#>  iter imp variable
#>   1   1  math3  math4
#>   2   1  math3  math4
#>   3   1  math3  math4
#>   4   1  math3  math4
#>   5   1  math3  math4

math_cols <- svy |> select(starts_with("math")) |> names()
svy[ , math_cols] <- complete(imputed) |> round()
ConsiglioQuando è appropriata l’imputazione multipla?

✔ Per quantità moderate di dati mancanti
✔ Quando esistono altre variabili correlate per la stima
✘ Non è adatta se i dati mancanti sono eccessivi
✘ Va evitata con campioni di dimensioni molto ridotte

15.5.6 Aggiunta di metadati ed etichette

Arricchire il dataset con metadati lo rende autoesplicativo e più facile da utilizzare nel tempo.

svy <- svy |>
  set_variable_labels(
    math1 = "Math self-efficacy: item 1",
    math2 = "Math self-efficacy: item 2",
    math3 = "Math self-efficacy: item 3",
    math4 = "Math self-efficacy: item 4"
  )

15.6 FASE 3 · Valida

La validazione consente di verificare formalmente che i dati puliti rispettino i vincoli attesi, prevenendo errori che potrebbero compromettere le analisi successive.

agent <- create_agent(svy) |>
  rows_distinct(vars(stu_id)) |>          # Gli ID devono essere univoci
  col_vals_between(vars(math1:math4), 1, 4) |>  # I punteggi devono essere tra 1 e 4
  interrogate()

Interpretazione dei risultati:

  • PASS → Il dato soddisfa il vincolo e può essere utilizzato.
  • FAIL → È necessario interrompere il processo, individuare la causa del fallimento e correggere il problema.

15.7 FASE 4 · Documenta

La documentazione non è un’aggiunta opzionale, ma una parte integrante e cruciale del processo di pulizia. Comprende la creazione di dataset puliti in formati appropriati (es. .csv, .rds), un dizionario dati dettagliato e un file README che registri tutte le decisioni e le trasformazioni effettuate.

Un dataset senza una documentazione adeguata diventa rapidamente inutilizzabile e incomprensibile, anche per il suo stesso creatore.

15.8 Criteri di qualità finale

Prima di considerare conclusa la fase di pulizia e di passare all’analisi, assicurati che il tuo dataset soddisfi i seguenti criteri:

  • Completo: gestione appropriata di missing e duplicati.
  • Valido: i valori rientrano nei range e nelle categorie attese.
  • Coerente: uniformità di formati, unità di misura e identificatori.
  • De-identificato: rimozione di informazioni personali sensibili.
  • Documentato: tutte le scelte sono registrate e riproducibili.
  • Analizzabile: il dataset è pronto per essere utilizzato nei modelli statistici.
ConsiglioMessaggio chiave
  • La pulizia dei dati non è una perdita di tempo; è il fondamento di un’analisi robusta e credibile.
  • Un errore non documentato è molto più pericoloso di un errore esplicito, perché rischia di passare inosservato.
  • Un dataset di qualità è, per definizione, pulito, validato, documentato e riproducibile.

Riflessioni conclusive

Nel processo di analisi dei dati, la fase di pulizia e pre-elaborazione è cruciale per garantire la qualità e l’integrità dei risultati finali. Sebbene questa fase possa sembrare meno interessante rispetto all’analisi vera e propria, essa costituisce la base su cui si costruiscono tutte le successive elaborazioni e interpretazioni. Attraverso una serie di passaggi strutturati, come quelli illustrati in questo capitolo, è possibile trasformare dati grezzi e disordinati in un dataset pulito, coerente e pronto per l’analisi. La cura nella gestione dei dati, dalla rimozione di duplicati alla creazione di un dizionario dei dati, è fondamentale per ottenere risultati affidabili e riproducibili.

Prossimo capitolo: utilizzeremo questi dati puliti per l’analisi esplorativa vera e propria, alla ricerca di pattern, anomalie e ipotesi.

sessionInfo()
#> R version 4.5.2 (2025-10-31)
#> Platform: aarch64-apple-darwin20
#> Running under: macOS Tahoe 26.2
#> 
#> Matrix products: default
#> BLAS:   /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBLAS.dylib 
#> LAPACK: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1
#> 
#> locale:
#> [1] C.UTF-8/UTF-8/C.UTF-8/C/C.UTF-8/C.UTF-8
#> 
#> time zone: Europe/Rome
#> tzcode source: internal
#> 
#> attached base packages:
#> [1] stats     graphics  grDevices utils     datasets  methods   base     
#> 
#> other attached packages:
#>  [1] stringr_1.6.0         labelled_2.16.0       mice_3.19.0          
#>  [4] pointblank_0.12.3     skimr_2.2.1           ragg_1.5.0           
#>  [7] tinytable_0.15.2      withr_3.0.2           systemfonts_1.3.1    
#> [10] patchwork_1.3.2       ggdist_3.3.3          tidybayes_3.0.7      
#> [13] bayesplot_1.15.0      ggplot2_4.0.1         reliabilitydiag_0.2.1
#> [16] priorsense_1.2.0      posterior_1.6.1       loo_2.9.0            
#> [19] rstan_2.32.7          StanHeaders_2.32.10   brms_2.23.0          
#> [22] Rcpp_1.1.0            sessioninfo_1.2.3     conflicted_1.2.0     
#> [25] janitor_2.2.1         matrixStats_1.5.0     modelr_0.1.11        
#> [28] tibble_3.3.0          dplyr_1.1.4           tidyr_1.3.2          
#> [31] rio_1.2.4             here_1.0.2           
#> 
#> loaded via a namespace (and not attached):
#>   [1] RColorBrewer_1.1-3    tensorA_0.36.2.1      jsonlite_2.0.0       
#>   [4] shape_1.4.6.1         magrittr_2.0.4        TH.data_1.1-5        
#>   [7] estimability_1.5.1    jomo_2.7-6            farver_2.1.2         
#>  [10] nloptr_2.2.1          rmarkdown_2.30        vctrs_0.6.5          
#>  [13] memoise_2.0.1         minqa_1.2.8           base64enc_0.1-3      
#>  [16] forcats_1.0.1         htmltools_0.5.9       haven_2.5.5          
#>  [19] distributional_0.5.0  curl_7.0.0            broom_1.0.11         
#>  [22] mitml_0.4-5           htmlwidgets_1.6.4     sandwich_3.1-1       
#>  [25] emmeans_2.0.1         zoo_1.8-15            lubridate_1.9.4      
#>  [28] cachem_1.1.0          lifecycle_1.0.5       iterators_1.0.14     
#>  [31] pkgconfig_2.0.3       Matrix_1.7-4          R6_2.6.1             
#>  [34] fastmap_1.2.0         rbibutils_2.4         snakecase_0.11.1     
#>  [37] digest_0.6.39         colorspace_2.1-2      rprojroot_2.1.1      
#>  [40] textshaping_1.0.4     timechange_0.3.0      abind_1.4-8          
#>  [43] compiler_4.5.2        S7_0.2.1              backports_1.5.0      
#>  [46] inline_0.3.21         QuickJSR_1.8.1        pkgbuild_1.4.8       
#>  [49] R.utils_2.13.0        pan_1.9               MASS_7.3-65          
#>  [52] tools_4.5.2           otel_0.2.0            nnet_7.3-20          
#>  [55] R.oo_1.27.1           glue_1.8.0            nlme_3.1-168         
#>  [58] grid_4.5.2            checkmate_2.3.3       generics_0.1.4       
#>  [61] gtable_0.3.6          R.methodsS3_1.8.2     data.table_1.18.0    
#>  [64] hms_1.1.4             foreach_1.5.2         pillar_1.11.1        
#>  [67] splines_4.5.2         lattice_0.22-7        survival_3.8-3       
#>  [70] tidyselect_1.2.1      knitr_1.51            reformulas_0.4.3.1   
#>  [73] arrayhelpers_1.1-0    gridExtra_2.3         V8_8.0.1             
#>  [76] stats4_4.5.2          xfun_0.55             bridgesampling_1.2-1 
#>  [79] stringi_1.8.7         yaml_2.3.12           pacman_0.5.1         
#>  [82] boot_1.3-32           evaluate_1.0.5        codetools_0.2-20     
#>  [85] cli_3.6.5             blastula_0.3.6        rpart_4.1.24         
#>  [88] RcppParallel_5.1.11-1 xtable_1.8-4          Rdpack_2.6.4         
#>  [91] repr_1.1.7            coda_0.19-4.1         svUnit_1.0.8         
#>  [94] parallel_4.5.2        rstantools_2.5.0      Brobdingnag_1.2-9    
#>  [97] lme4_1.1-38           glmnet_4.1-10         mvtnorm_1.3-3        
#> [100] scales_1.4.0          purrr_1.2.1           rlang_1.1.7          
#> [103] multcomp_1.4-29

Bibliografia

Buchanan, E. M., Crain, S. E., Cunningham, A. L., Johnson, H. R., Stash, H., Papadatou-Pastou, M., Isager, P. M., Carlsson, R., & Aczel, B. (2021). Getting started creating data dictionaries: How to create a shareable data set. Advances in Methods and Practices in Psychological Science, 4(1), 2515245920928007.

  1. MAR (Missing at Random): i dati mancano secondo un pattern spiegabile con le informazioni che abbiamo. Esempio concreto: se gli studenti più giovani tendono a saltare le domande più difficili, i valori mancanti dipendono dall’età (che è osservata nel dataset), non dal contenuto della risposta mancante stessa. Questo è importante perché permette di usare le variabili osservate (età, risposte ad altri item) per stimare ragionevolmente i valori mancanti. Al contrario, se i dati mancassero in modo completamente casuale (MCAR) o dipendessero dal valore non osservato stesso (MNAR), servirebbero strategie diverse.↩︎

  2. PMM (Predictive Mean Matching): tecnica che imputa ogni valore mancante cercando nel dataset osservazioni “vicine” con caratteristiche simili. Funziona così: (1) si costruisce un modello predittivo usando le risposte complete, (2) per ogni caso con dato mancante, si identificano i 3-5 casi osservati con predizioni più simili, (3) si estrae casualmente uno di questi valori reali e si usa come imputazione. Questo approccio ha due vantaggi: garantisce che i valori imputati siano plausibili (perché provengono da risposte reali) e preserva meglio la variabilità naturale dei dati rispetto a metodi più semplici come la sostituzione con la media.↩︎