here::here("code", "_common.R") |>
source()
# Load packages
if (!requireNamespace("pacman")) install.packages("pacman")
pacman::p_load(janitor, skimr, pointblank, mice, labelled, stringr)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.
Non modificare mai i dati originali. Lavora esclusivamente su copie e conserva sempre i dati grezzi intatti nella cartella data/raw/.
Prerequisiti
- Lewis, C. — Cleaning sample data in standardized way.
- Buchanan et al. (2021) — Getting Started Creating Data Dictionaries: How to Create a Shareable Data Set.
- Data Management in Large-Scale Education Research — capitolo “Documentation”.
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:
- Ispeziona: comprendere la struttura, il contenuto e le caratteristiche iniziali del dataset.
- Pulisci: correggere errori strutturali, formati errati e incongruenze semantiche.
- Valida: verificare che i dati rispettino vincoli, regole di dominio e coerenza interna.
- 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:
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, 1In 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)| 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.
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 IDDecisione 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()✔ 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.
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-29Bibliografia
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.↩︎
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.↩︎