9 Lottare con i dati.

v1.1.1 5/11/2023

9.1 Cosa c’è da imparare in questo capitolo.

Partiamo da un fatto, del quale ti renderai conto appena inizierai ad usare R per analisi non interattive: le attività di preparazione e manipolazione (data wrangling in inglese: filtrare, ordinare, aggiungere e rimuovere colonne, unire file, cambiare la struttura di tabelle di dati, etc…) possono arrivare a “consumare” il 60% del tempo di un progetto di analisi (statistica) dei dati. È ovvio che sarebbe bene farlo in modo efficiente, trasparente e riproducibile. Eppure, questa è sicuramente l’attività più “respingente” per chi si accosta all’analisi statistica senza nessuna esperienza di programmazione. D’altra parte, proprio per l’importanza del data wrangling, e in generale, della programmazione in R, il materiale (ovviamente in inglese) disponibile, gratuitamente, in rete, sotto forma di filmati, libri, pagine web etc. è praticamente sterminato e, detto francamente, non credo proprio di poter fare di meglio.
Quindi, come al solito, proverò ad indicare un percorso di apprendimento minimo, costruito attorno al tidyverse. La ragione è sempre la stessa: mentre tutte le operazioni di data wrangling possono essere realizzate con funzioni di R base (o con funzioni scritte dall’utente) la “logica” del tidyverse e della grammatica dei dati è sicuramente più vicina al linguaggio naturale.
Se pensi di usare R prevalentemente in modo interattivo, e non hai una particolare necessità di automatizzare e rendere ripetibili i tuoi flussi di analisi, puoi tranquillamente saltare questo capitolo, senza rimorsi (ammesso che tu ne abbia mai avuti168). Altrimenti, ti tocca leggerlo più o meno tutto, magari scegliendo cosa approfondire in seguito, se e quando ti servirà169.
Il capitolo è organizzato intorno alle principali operazioni di “manipolazione” dei dati, organizzati in data frame o tibble (vedi Paragrafo 4.6.1), che, in genere devono essere combinate in un flusso non sempre lineare:

  • rendere “tidy” i dati (con tidyr in combinazione con dplyr)

    • passare dai formati long a wide, e viceversa con pivot_wider e pivot_longer

    • unire diverse colonne con unite o separare una colonna in più colonne con separate

  • filtrare i dati (estraendo gruppi di osservazioni da un data frame o una tibble) con dplyr::filter

  • selezionare o rinominare un sottoinsieme di colonne con dplyr::select o dplyr::rename

  • riordinare i dati con dplyr::arrange

  • trasformare i dati usando una varietà di funzioni native di R, o disponibili in pacchetti o create dall’utente con dplyr::mutate o dplyr::transmute

  • riassumere più osservazioni in un’unica osservazione usando funzioni che prendono vettori come input (come per esempio mean(), sum(), etc.)

  • unire diversi data frame usando variabili chiave con il gruppo di comandi join di dplyr

E molto altro ancora…

Questo approccio ha alcune cose in comune:

  • si parte da un data frame o una tibble, in generale all’inizio di un percorso di comandi separati da pipe: è l’oggetto dell’analisi

  • si applicano una serie di funzioni (i verbi), eventualmente operando su gruppi di dati (individuati da dplyr::group_by o dall’argomento .by) sottoinsiemi di colonne con una modalità chiamata tidy evaluation, che non richiede l’uso delle modalità di selezione di R base (vedi paragrafo 4.8); avverbi (come across e where) possono essere usati per individuare in maniera più generale le variabili su cui operare; incidentalmente, nei comandi dei pacchetti del tidyverse non è necessario usare le notazioni con il $ nè le virgolette per indicare le variabili, come del resto hai già visto nel Capitolo 6, il che è una gran bella comodità.

  • il risultato di queste operazioni è, come sempre, un oggetto (spesso una tibble), che può essere assegnato ad un nome o inviato alla console o far parte di un effetto collaterale (come il salvataggio di un file o la produzione di un grafico)

In definitiva, si usa una vera e propria grammatica dei dati che rende le serie di comandi (i periodi) molto più comprensibili. Ne vedrai diversi esempi nei paragrafi successivi.

Se ve lo state chiedendo la pipe è un operatore che passa i dati dal comando a sinistra a quello a destra. Le pipe sono state introdotte originariamente con il pacchetto magrittr(https://magrittr.tidyverse.org) che usa come pipe principale %>% (che hai visto già usare molto in questo libro). R 4.1 ha introdotto una pipe nativa, |>, che si comporta in modo abbastanza simile alla pipe di magrittr.

Ovviamente le strutture di dati di R non sono solo data frame, ed esistono strumenti molto più flessibili, come quelli di reshape2 (l’equivalente di tidyr) e plyr (l’equivalente di dplyr), che operano anche su matrici, array e liste, oppure i comandi di data.table, ottimizzati per oggetti di granid dimensioni.

Infine, un flusso di lavoro può richiedere strutture di controllo che permettono di ripetere operazioni (i loop vedi Paragrafo 9.4.1) o svolgerle in modo condizionale (le strutture tipo if...else, vedi Paragrafo 9.4.3). I loop, che sono tipicamente lenti e verbosi intermini di codice, possono essere spesso sostituiti da “functionals” (vedi Paragrafo 9.4.2), funzioni che prendono come argomento altre funzioni e le applicano a vettori o strutture di altro tipo, spesso in maniera parallela. Cose utili, ma noiose: se vuoi puoi saltarle.

9.2 Rendere tidy i dati.

Del concetto dei dati tidy ho già parlato altrove (vedi paragrafo 4.1.5). Mentre i dati tidy si riconoscono subito, quelli untidy possono avere moltissimi priblemi diversi (inclusa dati mancanti in maniera implicita vedi paragrafo 4.1.6).
Prova a usare questo codice in uno script nel source pane:

require(tidyverse)
# tidyr fa fa parte del tidyverse
data(cms_patient_care)
?cms_patient_care
View(cms_patient_care)
data(billboard)
?billboard
View(billboard)
data(who)
?who
View(who)
data(relig_income)
?relig_income
View(relig_income)
  • cms_patinet_care contiene più tipi di misure (measure_abbr) in una sola variabile (score) e dovrebbe essere trasformato da long a wide;

  • billboard ha il problema inverso: i dati della posizione in classifica sono nelle variabili wk1-wk76: dovremmo creare una variabile wk e trasformare da wide a long; potremmo poi rimuovere “wk” dalle entries della variabile wk e trasformarla in numerica;

  • who è un data set molto grande con una parte del report sulla tubercolosi della World Health Organization ed è letterlamente un incubo fatto di dati in formato sbagliato (wide, mentre dovrebbe essere long) e di nomi di variabili che contengono informazioni che dovrebbero formare altre variabili (metodi di diagnosi, genere, gruppo di età)

  • relig_income è un incubo di variabili con nomi non sintattici (vedi paragrafo 4.3 con dati di una sola variabile, la conta delle persone che ricadono in una determinata combinazione di religione e classe di reddito, sparsi in più variabili

La vignetta “pivot” di tidyr illustra come rendere ordinati molti di questi data frame. Prova ora a capire che operazioni dovresti compiere per rendere tidy i seguenti data set:

  • tidyr::construction

  • tidyr::us_rent_income

  • tidyr::world_bank_pop (qui potresti anche pensare a come puoi ricavare dati da who)

9.2.1 Riordinare con tidyr e dplyr: esempio minimo.

Ora creiamo due piccolissimi data set di esempio per illustrare alcune delle funzioni di tidyr e dplyr.

mlab <- tribble(
  ~Specie,              ~Specie_b, ~Ceppo_specie, ~Repl, ~niente, ~H,   ~HM,  ~M,
  "Lb_brevis",          "L_br",    "L_br_E06",    "a",   0.40,    0.40, 0.36, 0.41,
  "Lb_brevis",          "L_br",    "L_br_E06",    "b",   0.30,    0.35, 0.35, 0.36,
  "Leuc_mesenteroides", "Le_me",   "Le_me_E08",   "a",   0.73,    0.82, 0.74, 0.76,
  "Leuc_mesenteroides", "Le_me",   "Le_me_E08",   "b",   0.63,    0.71, 0.61, 0.68,
  "Leuc_mesenteroides", "Le_me",   "Le_me_E09",   "a",   0.65,    0.78, 0.89, 0.63,
  "Leuc_mesenteroides", "Le_me",   "Le_me_E09",   "b",   0.62,    0.81, 0.82, 0.61,
  "Leuc_citreum",       "Le_ci",   "Le_ci_E05",   "a",   0.65,    0.86, 0.86, 0.70,
  "Leuc_citreum",       "Le_ci",   "Le_ci_E05",   "b",   0.79,    0.83, 0.86, 0.66
)
chiave_lab <- tribble(
  ~abbr_genere, ~abbr_specie, ~genere,            ~specie,
  "L",         "br",         "Levilactobacillus", "brevis",
  "Le",         "me",         "Leuconostoc",       "mesenteroides",
  "Le",         "ci",         "Leuconostoc",       "citreum"
)

E visualizziamo la prima tabella:

require(kableExtra)
knitr::kable(mlab, digits =2, 
             caption = "Crescita di batteri lattici (assorbanza a 650 nm) in presenza di diversi supplementi.", "html") %>% kable_styling("striped") %>% scroll_box(width = "100%") 
Tabella 9.1: Crescita di batteri lattici (assorbanza a 650 nm) in presenza di diversi supplementi.
Specie Specie_b Ceppo_specie Repl niente H HM M
Lb_brevis L_br L_br_E06 a 0.40 0.40 0.36 0.41
Lb_brevis L_br L_br_E06 b 0.30 0.35 0.35 0.36
Leuc_mesenteroides Le_me Le_me_E08 a 0.73 0.82 0.74 0.76
Leuc_mesenteroides Le_me Le_me_E08 b 0.63 0.71 0.61 0.68
Leuc_mesenteroides Le_me Le_me_E09 a 0.65 0.78 0.89 0.63
Leuc_mesenteroides Le_me Le_me_E09 b 0.62 0.81 0.82 0.61
Leuc_citreum Le_ci Le_ci_E05 a 0.65 0.86 0.86 0.70
Leuc_citreum Le_ci Le_ci_E05 b 0.79 0.83 0.86 0.66

La tabella 9.1 riporta alcuni dati di un esperimento in cui 2 supplementi (H emina, M menaquinone) sono stati aggiunti da soli o in combinazione, ad un substrato per la crescita di batteri lattici, appartenenti a diverse specie. Nei dati originali erano presenti più ceppi (entità al di sotto della specie) per ciascuna specie. Le prime tre colonne contengono informazioni in parte incomplete (manca un’indicazione estesa del nome di genere presente nella seconda tabella), in parte ridondanti (la combinazione dell’abbreviazione specie_genere compare due volte), in parte contenute in altre colonne (la sigla del ceppo, E06 etc. è presente in una colonna che include anche l’abbreviazione di genere e specie). Inoltre la variabile misurata è in realtà una sola (la crescita, misurata come assorbanza - una misura indiretta della torbidità di un substrato in seguito alla crescita di microrganismi - a 650 nm).
Inoltre, i dati delle specie sono contenuti in una seconda tabella:

knitr::kable(chiave_lab, 
             caption = "Abbreviazioni di genere e specie per alcuni batteri lattici.",
             "html") %>% kable_styling("striped") %>% scroll_box(width = "100%")
Tabella 9.2: Abbreviazioni di genere e specie per alcuni batteri lattici.
abbr_genere abbr_specie genere specie
L br Levilactobacillus brevis
Le me Leuconostoc mesenteroides
Le ci Leuconostoc citreum

Nota come nella tabella 9.2 manchi una colonna che unisca le abbreviazioni di genere e specie, in modo da fornire una chiave univoca che metta in corrispondenza le osservazioni di questa tabella con quelle della tabella 9.1.

Qui potremmo essere interessati a compiere le seguenti operazioni:

  1. trasformare il file da formato “largo” (wide), a “lungo”, creando una variabile che contenga i nomi delle prime 4 colonne (i diversi tipi di substrato) e un’altra che contenga i valori;

  2. estrarre la sigla del ceppo

  3. creare nella tabella 9.2 una nuova colonna unendo le colonne con l’abbreviazione di genere e specie

  4. aggiungere (merge) le informazioni della tabella 9.2 alla 9.1 (lo faremo più avanti, vedi paragrafo 9.3.5)

Questo formato ci permetterebbe, fra le altre cose di eseguire una serie di analisi statistiche (per esempio il calcolo della media, per gruppi, delle due repliche, o un’analisi della varianza).
Ora dimostrerò le prime 2 operazioni, seguite dal calcolo della media, usando comandi di tidyre dplyr, due pacchetti del tidyverse.

# da largo a lungo
mlab_lungo <- mlab %>% 
  pivot_longer(cols = niente:M, names_to = "trattamenti", values_to = "A650")
# le prime 8 righe
head(mlab_lungo, 8)
## # A tibble: 8 × 6
##   Specie    Specie_b Ceppo_specie Repl  trattamenti  A650
##   <chr>     <chr>    <chr>        <chr> <chr>       <dbl>
## 1 Lb_brevis L_br     L_br_E06     a     niente       0.4 
## 2 Lb_brevis L_br     L_br_E06     a     H            0.4 
## 3 Lb_brevis L_br     L_br_E06     a     HM           0.36
## 4 Lb_brevis L_br     L_br_E06     a     M            0.41
## 5 Lb_brevis L_br     L_br_E06     b     niente       0.3 
## 6 Lb_brevis L_br     L_br_E06     b     H            0.35
## 7 Lb_brevis L_br     L_br_E06     b     HM           0.35
## 8 Lb_brevis L_br     L_br_E06     b     M            0.36
# e ora le medie, creando, in più una variabile che contenga solo 
# la sigla del ceppo
mlab_medie <- mlab_lungo %>%
  separate(Ceppo_specie, into = c("gen","sp","ceppo"), remove = F) %>%
  dplyr::summarise(mediaA650 = mean (A650), 
                   .by = c("Ceppo_specie", "trattamenti"))

# le prime 8 righe
head(mlab_medie, 8)
## # A tibble: 8 × 3
##   Ceppo_specie trattamenti mediaA650
##   <chr>        <chr>           <dbl>
## 1 L_br_E06     niente          0.35 
## 2 L_br_E06     H               0.375
## 3 L_br_E06     HM              0.355
## 4 L_br_E06     M               0.385
## 5 Le_me_E08    niente          0.68 
## 6 Le_me_E08    H               0.765
## 7 Le_me_E08    HM              0.675
## 8 Le_me_E08    M               0.72

Nota che qui

  • l’argomento cols di pivot_longer individua le colonne da utilizzare nell’operazione di “fusione” del data frame mentre names_to e values_to indicano i nomi delle variabili che conterranno rispettivamente i nomi delle colonne che vengono “fuse”

  • ho usato l’argomento .by per ottenere una media per gruppi con summarise

  • ho separato la colonna Ceppo_specie in tre elementi usando separate, eliminando poi quelli che non servivano con select; consulta l’aiuto di separate per capire come è possibile omettere o indicare il carattere che separa gli elementi che vogliono disporre in colonne diverse

Incidentalmente, quello che abbiamo creato è un minimal reproducible example che è molto utile per fornire un minimo di elementi riproducibili quando si chiede aiuto in un forum (vedi sezione 3.3).

Abbastanza prevedibilmente, il comando che in tidyr fa la cosa opposta a separate è unite:

chiave_lab <- chiave_lab %>% 
  unite(col = "gen_specie_abbr", abbr_genere:abbr_specie, remove = F, sep = "_")
chiave_lab
## # A tibble: 3 × 5
##   gen_specie_abbr abbr_genere abbr_specie genere            specie       
##   <chr>           <chr>       <chr>       <chr>             <chr>        
## 1 L_br            L           br          Levilactobacillus brevis       
## 2 Le_me           Le          me          Leuconostoc       mesenteroides
## 3 Le_ci           Le          ci          Leuconostoc       citreum

Bene, ora proviamo a fare il contrario, passando dal formato “long” a quello “wide”.
Prova prima di tutto questo codice, che restituirà un errore:

mlab_lungo %>% 
  dplyr::select(!Repl) %>% 
  pivot_wider(names_from = trattamenti, values_from = A650)

il problema è causato dalla rimozione della variabile Repl, che identificava in maniera unica i valori che devono essere inseriti nelle celle delle nuove variabili (i trattamenti). Infatti con valori unici il problema non si pone:

mlab_medie %>% 
  pivot_wider(names_from = trattamenti, values_from = mediaA650)

Fortunatamente, le ultime versioni di tidyr permettono di usare una funzione per aggregare dati multipli170. Nell’esempio che segue usiamo la media:

mlab_lungo %>% 
  dplyr::select(!Repl) %>% 
  pivot_wider(names_from = trattamenti, values_from = A650, values_fn = mean)
## # A tibble: 4 × 7
##   Specie             Specie_b Ceppo_specie niente     H    HM     M
##   <chr>              <chr>    <chr>         <dbl> <dbl> <dbl> <dbl>
## 1 Lb_brevis          L_br     L_br_E06      0.35  0.375 0.355 0.385
## 2 Leuc_mesenteroides Le_me    Le_me_E08     0.68  0.765 0.675 0.72 
## 3 Leuc_mesenteroides Le_me    Le_me_E09     0.635 0.795 0.855 0.62 
## 4 Leuc_citreum       Le_ci    Le_ci_E05     0.72  0.845 0.86  0.68

Naturalmente, ci sono molti più casi che tidyr può risolvere brillantemente, come la gestione dei dati mancanti oppure la gestione di colonne multiple durante il passaggio da “long” a “wide”. Se ne avessi bisogno, puoi consultare la vignetta “pivot” di tidyr scrivendo nella console:

vignette("pivot", package = "tidyr")

9.2.2 Funzioni avanzate con tidyr.

tidyr, usato in combinazione con broom e dplyr può fare cose veramente pazzesche. Le operazioni per creare nested data frames (dataframe annidati, in cui una delle colonne è una colonna di data frames171) e trasformarli di nuovo in data frame convenzionali permettono di automatizzare molte analisi statistiche. I due comandi rilevanti sono nest() e unnest() Prova a vedere cosa succede con questo codice per capire cos’è un nested data frame:

data(mpg)
glimpse(mpg)
mpg %>% group_by(class) %>% nest()
# o anche
mpg %>% nest(.by = class)

L’argomento va un po’ oltre quello che posso trattare qui, quindi, se sei interessat* ti consiglio di leggere le vignette di tidyr e broom, o, meglio, il capitolo sui tidy data della versione italaiana di “R for data science”. Un argomento correlato è il rectangling, cioè la trasformazione di liste annidate (che spesso derivano dall’importanzione di dati in formati JSON o XML): l’uso di comandi come unnest_wider() e hoist() può facilitare questi compiti, come dimostrato nella vignetta relativa:

vignette("rectangle", package = "tidyr")

9.3 il cavallo di battaglia del data wrangling: dplyr.

dplyr, insieme a ggplot2 è decisamente il pacchetto del tidyverse che ti troverai ad usare più spesso. Le funzioni sono tantissime e, abbastanza ovviamente, hanno equivalenti nelle funzioni di R base e di alcuni pacchetti precedenti, come plyr. Per confrontare le funzioni prova a scrivere nella console:

`library(dplyr) vignette("base", package="dplyr")

Qui mi limiterò a darti qualche esempio delle funzioni principali, raggruppandole in maniera, spero, logica. Ti accorgerai come quasi tutte le funzioni che ti mostrerò qui sono comparse nei capitoli precedenti e, tanto per cambiare, userò il data set mpg.

9.3.1 Estrarre gruppi di righe o colonne.

Abbastanza prevedibilmente il comando che si usa per filtrare le osservazioni in base ad uno o più criteri è dplyr::filter()172. Filter prende come argomento un data frame o una tibble, applica una condizione logica che individua una combinazione di osservazioni e restituisce (come tibble) il risultato.
Guarda che succede:

data(mpg, verbose = F)
glimpse(mpg)
## Rows: 234
## Columns: 11
## $ manufacturer <chr> "audi", "audi", "audi", "audi", "audi", "audi", "audi", "…
## $ model        <chr> "a4", "a4", "a4", "a4", "a4", "a4", "a4", "a4 quattro", "…
## $ displ        <dbl> 1.8, 1.8, 2.0, 2.0, 2.8, 2.8, 3.1, 1.8, 1.8, 2.0, 2.0, 2.…
## $ year         <int> 1999, 1999, 2008, 2008, 1999, 1999, 2008, 1999, 1999, 200…
## $ cyl          <int> 4, 4, 4, 4, 6, 6, 6, 4, 4, 4, 4, 6, 6, 6, 6, 6, 6, 8, 8, …
## $ trans        <chr> "auto(l5)", "manual(m5)", "manual(m6)", "auto(av)", "auto…
## $ drv          <chr> "f", "f", "f", "f", "f", "f", "f", "4", "4", "4", "4", "4…
## $ cty          <int> 18, 21, 20, 21, 16, 18, 18, 18, 16, 20, 19, 15, 17, 17, 1…
## $ hwy          <int> 29, 29, 31, 30, 26, 26, 27, 26, 25, 28, 27, 25, 25, 25, 2…
## $ fl           <chr> "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p…
## $ class        <chr> "compact", "compact", "compact", "compact", "compact", "c…

Selezioniamo le auto con 4 cilindri prodotte da audi

audi4 <- mpg %>% dplyr::filter(cyl == 4 & manufacturer == "audi")
glimpse(audi4)
## Rows: 8
## Columns: 11
## $ manufacturer <chr> "audi", "audi", "audi", "audi", "audi", "audi", "audi", "…
## $ model        <chr> "a4", "a4", "a4", "a4", "a4 quattro", "a4 quattro", "a4 q…
## $ displ        <dbl> 1.8, 1.8, 2.0, 2.0, 1.8, 1.8, 2.0, 2.0
## $ year         <int> 1999, 1999, 2008, 2008, 1999, 1999, 2008, 2008
## $ cyl          <int> 4, 4, 4, 4, 4, 4, 4, 4
## $ trans        <chr> "auto(l5)", "manual(m5)", "manual(m6)", "auto(av)", "manu…
## $ drv          <chr> "f", "f", "f", "f", "4", "4", "4", "4"
## $ cty          <int> 18, 21, 20, 21, 18, 16, 20, 19
## $ hwy          <int> 29, 29, 31, 30, 26, 25, 28, 27
## $ fl           <chr> "p", "p", "p", "p", "p", "p", "p", "p"
## $ class        <chr> "compact", "compact", "compact", "compact", "compact", "c…

Selezioniamo le auto con che percorrono almeno 25 km con un gallone in autostrada

hwy25 <- mpg %>% dplyr::filter(hwy >= 25 )
glimpse(hwy25)
## Rows: 116
## Columns: 11
## $ manufacturer <chr> "audi", "audi", "audi", "audi", "audi", "audi", "audi", "…
## $ model        <chr> "a4", "a4", "a4", "a4", "a4", "a4", "a4", "a4 quattro", "…
## $ displ        <dbl> 1.8, 1.8, 2.0, 2.0, 2.8, 2.8, 3.1, 1.8, 1.8, 2.0, 2.0, 2.…
## $ year         <int> 1999, 1999, 2008, 2008, 1999, 1999, 2008, 1999, 1999, 200…
## $ cyl          <int> 4, 4, 4, 4, 6, 6, 6, 4, 4, 4, 4, 6, 6, 6, 6, 6, 8, 8, 8, …
## $ trans        <chr> "auto(l5)", "manual(m5)", "manual(m6)", "auto(av)", "auto…
## $ drv          <chr> "f", "f", "f", "f", "f", "f", "f", "4", "4", "4", "4", "4…
## $ cty          <int> 18, 21, 20, 21, 16, 18, 18, 18, 16, 20, 19, 15, 17, 17, 1…
## $ hwy          <int> 29, 29, 31, 30, 26, 26, 27, 26, 25, 28, 27, 25, 25, 25, 2…
## $ fl           <chr> "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p…
## $ class        <chr> "compact", "compact", "compact", "compact", "compact", "c…

dplyr::filter() quindi usa un’espressione che restituisce un vettore logico (contenente vero per le osservazioni che soddisfano l’espressione, e che verranno mantenute nel data frame filtrato, e falso per quelle che non la soddisfano, che verranno espluse).
Gli operatori logici, che possono essere usati in combinazioni abbastanza complesse sono stati trattati nel paragrafo 4.10.1.

Analoga alla necessità di estrarre alcune righe è quella di estrarre alcune colonne: il comando corrispondente è dplyr::select(). La sintassi, basata sulla non-standard evaluation, è molto più semplice di quella di R base (vedi sezione 4.8). Prova a usare i seguenti comandi in uno script:

# selezionare le colonne per nome
mpg %>% dplyr::select(model, year, cyl)
# selezionare per posizione (e riordinare le colonne) usando l'operatore OR (|)
mpg %>% dplyr::select(11 | 2 | 9:8)
# escludere colonne con l'operatore NOT (!)
mpg %>% dplyr::select(!year & !trans)
# ma anche
mpg %>% dplyr::select(!c(year,trans))
# selezionare e rinominare
mpg %>% dplyr::select(modello = model, anno = year, cilindri = cyl)

Incidentalmente, è possibile usare la funzione rename(nuovo_nome = vecchio_nome) per rinominare colonne, con lo stesso criterio che hai appena visto.
Come altre funzioni, dplyr::select() può usare l’avverbio where() per selezionare colonne in modo programmatico. where() e alcune funzioni helper (contains, starts_with, etc.173) individuano delle condizioni logiche per la selezione di colonne in base a condizioni più specifiche. Ecco alcuni esempi, che puoi provare nella console o in uno script:

# la colonna model e tutte le colonne numeriche
mpg %>% dplyr::select(model | where(is.numeric))
# le colonne che contengono la lettera y 
mpg %>% dplyr::select(contains("y"))

9.3.2 Riordinare i dati.

Ordinare le righe sulla base di uno o più criteri, in ordine discendente o ascendente, è facilissimo con dplyr::arrange(). L’ordinamento di default è ascendente; per ottenere un ordinamento in ordine decrescente si usa desc(). Si tratta di comandi molto intuitivi174

# non ordinato
head(mpg, 10)
## # A tibble: 10 × 11
##    manufacturer model      displ  year   cyl trans drv     cty   hwy fl    class
##    <chr>        <chr>      <dbl> <int> <int> <chr> <chr> <int> <int> <chr> <chr>
##  1 audi         a4           1.8  1999     4 auto… f        18    29 p     comp…
##  2 audi         a4           1.8  1999     4 manu… f        21    29 p     comp…
##  3 audi         a4           2    2008     4 manu… f        20    31 p     comp…
##  4 audi         a4           2    2008     4 auto… f        21    30 p     comp…
##  5 audi         a4           2.8  1999     6 auto… f        16    26 p     comp…
##  6 audi         a4           2.8  1999     6 manu… f        18    26 p     comp…
##  7 audi         a4           3.1  2008     6 auto… f        18    27 p     comp…
##  8 audi         a4 quattro   1.8  1999     4 manu… 4        18    26 p     comp…
##  9 audi         a4 quattro   1.8  1999     4 auto… 4        16    25 p     comp…
## 10 audi         a4 quattro   2    2008     4 manu… 4        20    28 p     comp…
# ordinato in ordine crescente di numero di cilindri e decrescente per 
# miglia per gallone in autostrada
mpg %>% arrange(cyl, desc(hwy)) %>% head()
## # A tibble: 6 × 11
##   manufacturer model      displ  year   cyl trans  drv     cty   hwy fl    class
##   <chr>        <chr>      <dbl> <int> <int> <chr>  <chr> <int> <int> <chr> <chr>
## 1 volkswagen   jetta        1.9  1999     4 manua… f        33    44 d     comp…
## 2 volkswagen   new beetle   1.9  1999     4 manua… f        35    44 d     subc…
## 3 volkswagen   new beetle   1.9  1999     4 auto(… f        29    41 d     subc…
## 4 toyota       corolla      1.8  2008     4 manua… f        28    37 r     comp…
## 5 honda        civic        1.8  2008     4 auto(… f        25    36 r     subc…
## 6 honda        civic        1.8  2008     4 auto(… f        24    36 c     subc…

9.3.3 Trasformare con mutate().

Hai già visto parecchi esempi di come sia possibile trasformare una colonna con un’operazione più o meno complessa con dplyr::mutate(). L’approccio è molto più semplice di quello utilizzato in R base, anche perché è possibile trasformare diverse colonne creandone di nuove in un unico comando in maniera simile a base::transform(), ma con parecchie più opzioni (che puoi scoprire leggendo l’aiuto).
Confronta questi tre esempi (che puoi eseguire in uno script): tutti e tre fanno la stessa cosa (trasformano i consumi in mpg da miglia per gallone a litri per 100 km).

library(tidyverse)
library(magrittr) # per la assignment pipe
data(mpg, verbose = F)
# usando $
mpg$hwyl100km <- mpg$hwy*235.21
mpg$ctyl100km <- mpg$cty*235.21
# usando transform
mpg <- base::transform(mpg, 
                       hwyl100km = 235.21/hwy, 
                       ctyl100km = 235.21/cty)
# usando mutate
mpg <- mpg %>%
  mutate(
    hwyl100km = 235.21/hwy, 
    ctyl100km = 235.21/cty 
  )
# usando la assignment pipe, anch'essa disponibile nel pacchetto magrittr 
mpg %<>% mutate(
  hwyl100km = 235.21/hwy, 
  ctyl100km = 235.21/cty
)
# creando una nuova tibble in cui le colonne usate 
# per il calcolo sono rimosse (si potrebbe fare con select)
mpg2 <- mpg %>% mutate(
  hwyl100km = 235.21/hwy, 
  ctyl100km = 235.21/cty,
  .keep = "unused"
)

L’opzione .keep è piuttosto flessibile e permette, utilizzando l’opzione “none” di sostituire il vecchio comando transmute(), che produceva una nuova tibble contenente solo le colonne risultanti dal calcolo.
mutate() è un comando molto flessibile e trova applicazioni interessanti insieme ai comandi di broom, un pacchetto che, come ti ho già mostrato (paragrafo 7.5.3), piuttosto che rendere tidy i dati rende tidy i risultati delle analisi statistiche.
Quando si desidera eseguire la stessa trasformazione su variabili multiple con mutate() è possibile utilizzare la combinazione di “avverbi” across() e where(). Ecco tre piccolissimi esempi (da eseguire alla console) Il primo esempio moltiplica per 1000 le prime due variabili numeriche e il secondo ne calcola il logaritmo decimale

>mpg3 <- mpg %>% mutate(across(displ:year, ~.x*1000))
>mpg4 <- mpg %>% mutate(across(displ:year, log10))

~.x*1000 definisce quella che si chiama lambda o funzione anonima. Qui ho usato la notazione del pacchetto purrr. Due alternative:

>mpg3 <- mpg %>% mutate(across(displ:year, function(x) x*1000))
>mpg3 <- mpg %>% mutate(across(displ:year, \(x) x*1000))

L’ultima opzione è stata introdotta con R 4.1 (ed è difficile dire se sia un miglioramento…). Infine, con where(), che hai già incontrato nel paragrafo 9.3.1 e che con mutate() si usa all’interno di across() possiamo, per esempio, trasformare in fattori le colonne che sono di tipo carattere:

mpg5 <- mpg %>% mutate(across(where(is.character), as.factor))

L’argomento è un pochino complesso per questo libro, quindi, se sei interessat* ti consiglio di leggere l’aiuto di across().

9.3.4 Riassumere i dati con summarise().

mutate() trasforma i dati creando una nuova osservazione per ciascuna osservazione presente nel data frame originale. summarise() invece, riassume gruppi di osservazioni in un unico valore, ed è particolarmente interessante per calcolare statistiche riassuntive come hai visto nel paragrafo 7.4.3. Qui ti faccio solo alcuni esempi, utilizzando fra le altre cose, l’opzione group_by():

mpg_riassunto <- mpg %>%
  group_by(manufacturer, class) %>%
  dplyr::summarise(
    media_hwy = mean(hwy, na.rm = T),
    devst_why = sd(hwy, na.rm = T),
    media_cty = mean(hwy, na.rm = T),
    devst_cty_cty = sd(hwy, na.rm = T)
  ) %>%
  ungroup()
## `summarise()` has grouped output by 'manufacturer'. You can override using the
## `.groups` argument.
mpg_riassunto
## # A tibble: 32 × 6
##    manufacturer class      media_hwy devst_why media_cty devst_cty_cty
##    <chr>        <chr>          <dbl>     <dbl>     <dbl>         <dbl>
##  1 audi         compact         26.9     2.02       26.9         2.02 
##  2 audi         midsize         24       1          24           1    
##  3 chevrolet    2seater         24.8     1.30       24.8         1.30 
##  4 chevrolet    midsize         27.6     1.82       27.6         1.82 
##  5 chevrolet    suv             17.1     2.20       17.1         2.20 
##  6 dodge        minivan         22.4     2.06       22.4         2.06 
##  7 dodge        pickup          16.1     2.21       16.1         2.21 
##  8 dodge        suv             16       2          16           2    
##  9 ford         pickup          16.4     0.787      16.4         0.787
## 10 ford         subcompact      23.2     2.17       23.2         2.17 
## # ℹ 22 more rows

Il comando ungroup() serve per rimuovere il raggruppamento. Un modo più semplice nelle versioni recenti di dplyr è usare l’opzione .by.

mpg_riassunto <- mpg %>%
  dplyr::summarise(
    media_hwy = mean(hwy, na.rm = T),
    devst_why = sd(hwy, na.rm = T),
    media_cty = mean(hwy, na.rm = T),
    devst_cty_cty = sd(hwy, na.rm = T),
    .by = c(manufacturer, class)
  ) 
mpg_riassunto

Incidentalmente, con across() e where() si possono fare cose interessanti. Prova questo:

>mpg %>%
   dplyr::summarise(across(where(is.numeric),
   list(media = mean, devst = sd)),
   .by = class)

Carino, no? Come esercizio, partendo da mpg, riusciresti a creare una tabella in cui, dopo aver calcolato (raggruppate per class) media e deviazione standard di hwy e cyl, arrotondare a due cifre decimali il risultato e poi unire media e dev. st. in un’unica colonna separandole con il carattere “±”? Come suggerimento: sevi usare una combinazione di summarise con across e .by, poi usare le funzioni round e unite... Se proprio non ce la fai, vai al paragrafo 9.5.

9.3.5 Database e tabelle relazionali: unire tabelle con _join.

In molte situazioni i dati di cui abbiamo bisogno sono dispersi in più tabelle, collegate da una qualche chiave (per esempio un valore unico per una o più osservazioni presente in una colonna in due tabelle diverse). Unire tabelle per righe o colonne è possibile (con alcune limitazioni) usando comandi come bind_cols() e bind_rows() (due comandi di dplyr che corrispondono a cbind() e rbind()). Guarda questi due esempi, basati come al solito su Arthritis:

data(Arthritis)
# il comando slice() consente di selezionare righe
A1 <- Arthritis %>% slice(1:52)
A2 <- Arthritis %>% slice(53:84)
A3 <- bind_rows(A1,A2)
head(A3)
##   ID Treatment  Sex Age Improved
## 1 57   Treated Male  27     Some
## 2 46   Treated Male  29     None
## 3 77   Treated Male  30     None
## 4 17   Treated Male  32   Marked
## 5 36   Treated Male  46   Marked
## 6 23   Treated Male  58   Marked
identical(A3, Arthritis)
## [1] TRUE
# è possibile perché sono presenti le stesse colonne
A4 <- Arthritis %>% dplyr::select(ID:Sex) 
A5 <- Arthritis %>% dplyr::select(Age:Improved) 
A6 <- bind_cols(A4,A5)
head(A6)
##   ID Treatment  Sex Age Improved
## 1 57   Treated Male  27     Some
## 2 46   Treated Male  29     None
## 3 77   Treated Male  30     None
## 4 17   Treated Male  32   Marked
## 5 36   Treated Male  46   Marked
## 6 23   Treated Male  58   Marked
identical(A6,Arthritis)
## [1] TRUE

La situazione diventa un po’ più complessa se volessimo unire le informazioni presenti nei due file creati nel paragrafo 9.2.1. Per farlo, potremmo per esempio creare in chiave_lab una colonna contenente l’abbreviazione di genere e specie unite da “_” e poi usare questa colonna come chiave per aggiungere i nomi estesi di genere e specie. Per farlo, possiamo usare i verbi a due tabelle _join di dplyr. Guarda cosa succede:

chiave_lab_2 <- chiave_lab %>%
  unite("Specie_b", abbr_genere, abbr_specie, sep = "_") %>%
  dplyr::select(Specie_b, genere, specie)
mlab_2 <- left_join(mlab,chiave_lab_2)
## Joining with `by = join_by(Specie_b)`
head(mlab_2)
## # A tibble: 6 × 10
##   Specie      Specie_b Ceppo_specie Repl  niente     H    HM     M genere specie
##   <chr>       <chr>    <chr>        <chr>  <dbl> <dbl> <dbl> <dbl> <chr>  <chr> 
## 1 Lb_brevis   L_br     L_br_E06     a       0.4   0.4   0.36  0.41 Levil… brevis
## 2 Lb_brevis   L_br     L_br_E06     b       0.3   0.35  0.35  0.36 Levil… brevis
## 3 Leuc_mesen… Le_me    Le_me_E08    a       0.73  0.82  0.74  0.76 Leuco… mesen…
## 4 Leuc_mesen… Le_me    Le_me_E08    b       0.63  0.71  0.61  0.68 Leuco… mesen…
## 5 Leuc_mesen… Le_me    Le_me_E09    a       0.65  0.78  0.89  0.63 Leuco… mesen…
## 6 Leuc_mesen… Le_me    Le_me_E09    b       0.62  0.81  0.82  0.61 Leuco… mesen…

Specie_b è la chiave: è la chiave primaria in mlab e la chiave esterna in chiave_lab_2. La chiave primaria è, appunto, la colonna che si intende utilizzare nella tabella principlae per ottenere la corrispondenza dei casi, e ad essa devono corrispondere valori della chiave esterna nella seconda tabella. Non è necessario che le due chiavi abbiano lo stesso nome (ma almeno alcuni valori devono corrispondere), perché l’argomento by consente di usare colonne con nomi diversi; è anche possibile usare più di una colonna come chiave (in questo caso valgono le combinazioni delle colonne che si usano)

Esistono due gruppi principali di _join:

  • mutating joins: la tabella che ne risulta contiene una combinazione di tutte colonne delle due tabelle originali (la o le colonne chiave non sono duplicate)

    • ìnner_join() conserva solo le osservazioni della tabella a sinistra che hanno una corrispondenza in quella a destra

    • left_join() usa tutti i casi della tabella a sinistra (la prima tabella che si usa nell’espressione join) e i casi corrispondenti della tabella a destra

    • right_join()` usa tutti i casi della tabella a destra (la seconda tabella che si usa nell’espressione join) e i casi corrispondenti della tabella a sinistra

    • full_join()` usa tutti i casi di entrambe le tabelle

  • filtering joins: contengono solo una parte dei casi delle due tabelle utilizzate:

    semi_join() conserva solo le osservazioni di x che hanno una corrispondenza nella tabella a destra

    anti_join() conserva solo le osservazioni della tabella a sinistra che non hanno una corrispondenza in quella a destra.

Anche se l’uso dei _join è piuttosto intuitivo ci sono diverse sottigliezze:

  • che succede se un’istanza della chiave non ha una corrispondenza in entrambele tabelle?

  • che succede se ci sono valori multipli della chiave in una delle due tabelle?

L’argomento è piuttosto complesso ed è trattato in maniera molto esaustiva in R4DS che ti consiglio di leggere se e quando ne avrai bisogno. Intanto, per farti un’idea prova a cercare di capire come sono collegati i diversi data frame del pacchetto nycflights13. Prova a dare uno sguardo facendo girare questo script:

data(airlines)
head(airlines)
data(airports)
head(airports)
data(flights)
head(flights)
data(planes)
head(planes)
data(weather)
head(weather)

La tabella principale è chiaramente flights, che contiene i dati dei voli. Usando le altre è possibile ottenere dati ulteriori che, se fossero stati presenti in flights, avrebbero reso la tabella pesantissima, duplicando inutilmente molte informazioni.

9.4 Strutture di controllo.

Come hai visto nel paragrafo 3.1.2 e in diversi altri punti di questo libro eseguire un gruppo di istruzioni può richiedere delle strutture di controllo che indirizzino il flusso o eseguano in maniera iterativa gruppi più o meno complessi di funzioni. R è un linguaggio interpretato e, durante l’esecuzione dello script, le istruzioni vengono eseguite una alla volta: le strutture di controllo permettono di:

  • ripetere un gruppo di istruzioni in base ad una condizione (loop for, while, repeat, etc.)

  • eseguire in maniera alternativa dei gruppi di istruzioni (if … else)

9.4.1 Loop (o cicli) e cose simili.

loop for

I loop più comuni sono sicuramente i loop for, presenti in praticamente tutti i linguaggi di programmazione.
La loro struttura è semplice:

  • l’istruzione for() crea una sequenza, utilizzando un indice: for(i in 1:10){corpo} genera una sequenza di 10 iterazioni con l’indice i che va da 1 a 10

  • all’interno della sequenza, generalmente fra parentesi graffe (che non sono necessarie se il corpo è composto da una sola espressione) c’è il corpo del loop, le operazioni che verranno eseguite una volta per ogni iterazione della sequenza

Ecco un piccolo esempio, con un loop relativamente inefficiente che serve per calcolare l’età media di 50 campioni casuali di 10 osservazioni estratti dal data set Arthritis:

# uso set.seed per impostare il seme del generatore di numeri casuali, 
# in modo che l'operazione sia riproducibile (soltanto per scopi 
# didattici)
set.seed(1234) 

# creo manualmente una prima iterazione, 10 righe, tutte con l'indice 1

subsample10 <- as.data.frame(
  cbind(
    indice = rep(1,10),
    eta = sample(Arthritis$Age, size=10, replace = T)
    )
)

for (i in 2:50){ # la sequenza
  subsample <- cbind(indice = rep(i,10), 
                     eta = sample(Arthritis$Age, size=10, replace = T))
  subsample10 <- rbind(subsample10, subsample)
  # questo è il corpo del loop
}
subsample10$indice <- as.factor(subsample10$indice)
glimpse(subsample10)
## Rows: 500
## Columns: 2
## $ indice <fct> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, …
## $ eta    <dbl> 58, 66, 55, 63, 46, 68, 32, 32, 55, 65, 64, 70, 31, 46, 32, 32,…

nota come l’indice ì è stato utilizzato all’interno del loop per tenere traccia dell’iterazione e aggiungerla al data frame.
Questo codice è veramente brutto e verboso e, in teoria, lento (l’operazione di cbind richiede che per ogni ciclo tutti i dati vengano copiati in memoria). In generale è più efficiente creare un contenitore vuoto della lunghezza desiderata (una lista, che può essere poi trasformata in data frame) e poi riempirlo.
Anche questo codice funziona:

iterazioni <- 50
subcampioni <- 10
contenitore <- vector("list", iterazioni)
for(i in 1:iterazioni){
  contenitore[[i]] <- sample(Arthritis$Age, size=10, replace = T)
}
subsamples_2 <- tibble(
  indice = rep(1:iterazioni, each = subcampioni),
  eta = unlist(contenitore)
  )

Un modo semplice per generare una sequenza senza conoscerne in anticipo la lunghezza è usare seq_along()

seq_along(Arthritis$ID)
##  [1]  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
## [26] 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
## [51] 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
## [76] 76 77 78 79 80 81 82 83 84
seq_along(Arthritis$Sex)
##  [1]  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
## [26] 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
## [51] 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
## [76] 76 77 78 79 80 81 82 83 84
# ma che succede se genero una sequenza sui livelli di un fattore?
seq_along(levels(Arthritis$Sex))
## [1] 1 2

E’ possibile annidare loop uno dentro l’altro. Per esempio prova ad eseguire questo codice in uno script:

for(i in 1:10){
  for(j in 1:5){
    for(k in 1:2) print(i*j*k)
  }
}

E’ possibile interrompere un loop utilizzando i comandi:

  • next: passa immediatamente all’iterazione successiva

  • break: esce dal loop

while, repeat.

Nei loop for la sequenza viene valutata alla fine del loop: se la sequenza ha lunghezza 1 il loop viene eseguito almeno una volta, se ha lunghezza 0 non viene eseguita; se anche si modifica l’indice della sequenza all’interno del loop il progresso del loop non cambia.

Nei loop while il corpo della sequenza viene eseguito fin quando la condizione while(condition) è vera e questo può causare delle ripetizioni infinite. La condizione viene valutata all’inizio del loop. Prova il sequenge codice in uno script. E’ un loop testardo, che richiede interazione da parte dell’utente, e va eseguito un’istruzione per volta…

Readanswer <- function(){
  ans <- readline(prompt="Per favore, immetti la tua RISPOSTA: ")
}
answer <- as.character(Readanswer())
while(answer != 42){
  cat("Scusa, ho bisogno della risposta alla domanda fondamentale sulla vita, l'universo e tutto quanto",
      "\n","Riprova...", sep = "")
  answer <- as.character(Readanswer())
}
?cat

Finché non darai la risposta giusta (notoriamente, 42) il loop andrà avanti all’infinito… repeat è n altro modo (stupido) per creare loop e richiede l’uso specifico di break (in genere in una condizione if()) per uscire dal loop. Ecco un loop piuttosto stupido che stampa buongiorno almeno 5 volte oppure si lamenta che sia già pomeriggio.

n <- 0
hour <- as.numeric(format(Sys.time(),"%H"))
repeat {
  if (hour<=12 & n<5){
    print("Buongiorno!")
    hour <- as.numeric(format(Sys.time(),"%H"))
    n<-n+1
  } else {
    print("Accidenti, è già pomeriggio oppure ho eseguito il loop 5 volte!")
    break
  }
}

9.4.2 Un’alternativa (veloce e ordinata) ai loop: i “functional”.

Un’alternativa, veloce e ordinata, alla maggior parte dei loop sono i così detti funzionali (functionals in inglese): si tratta di funzioni che hanno come argomento delle funzioni. Ne hai già visto un esempio nel paragrafo 3.1.2. Ci sono molti tipi di funzionali in R:

  • le funzioni di base del gruppo apply: apply(), lapply(), sapply(), tapply(), mapply()

  • le funzioni del pacchetto plyr, estremamente flessibili negli input e output

  • le funzioni del pacchetto purrr

Per brevità, ti riporto una versione ridotta del codice presentato nel paragrafo 3.1.2:

# creo un data frame (in realtà una tibble) con numeri casuali 
n_casi <- 10000
df <- tibble( a = rnorm(n_casi), b = rnorm(n_casi),
              c = rnorm(n_casi), d = rnorm(n_casi),
              e = rnorm(n_casi), f = rnorm(n_casi),
              g = rnorm(n_casi), h =rnorm(n_casi))
# calcolo le mediane con un loop
output <- vector("double", length = ncol(df))
for (i in seq_along(df)) { # la sequenza 
  output[[i]] <- median(df[[i]]) # il corpo del loop 
} 
output
## [1]  0.006402963 -0.007366067 -0.005574402  0.007971636 -0.017403041
## [6] -0.015778585 -0.001770304  0.002987635
# uso apply()
(mediane_apply <- apply(df, 2, median))
##            a            b            c            d            e            f 
##  0.006402963 -0.007366067 -0.005574402  0.007971636 -0.017403041 -0.015778585 
##            g            h 
## -0.001770304  0.002987635
# uso la funzione map_dbl() del pacchetto purrr
# map_dbl "applica" la funzione median alle colonne di df
(le_mie_mediane <- map_dbl(df, median))
##            a            b            c            d            e            f 
##  0.006402963 -0.007366067 -0.005574402  0.007971636 -0.017403041 -0.015778585 
##            g            h 
## -0.001770304  0.002987635

Le mediane, ovviamente, sono identiche, e il codice è più o meno elegante e comprensibile. Questo argomento è decisamente troppo complesso per gli scopi di questo testo. Per saperne di più consulta, fra le altre cose, R4DS, la vignetta che compara le funzioni di purrr con quelle di R base, o questo vecchio articolo su plyr.

9.4.3 Esecuzione condizionale.

In molti casi ci si può traovare nella situazione di voler far eseguire pezzetti diversi di codice a seconda di una condizione. In molte occasioni abbiamo visto l’uso della struttura più semplice:

if(condizione){
  istruzioni se condizione è vera
} else {
  istruzioni se condizione è falsa
}

Un esempio è la funzione Readanswer() proposta nel paragrafo precedente. La struttura è molto semplice:

  • dopo if, fra le parentesi tonde c’è una condizione che deve restituire un vettore logico di lunghezza 1, con valore TRUE o FALSE

  • se la condizione è TRUE viene eseguito il comando o il gruppo di comandi (in questo caso devono essere fra parentesi graffe) immediatamente successivo

  • se la condizione è FALSE viene eseguito il comando o gruppo di comandi dopo else (se esiste) o l’esecuzione salta al comando dopo

Naturalmente, bisogna stare molto attenti a non definire oggetti in condizioni che potrebbero non essere eseguite e richiamarli più tardi.

Prova a seguire questo esempio, abbastanza stupido, che dimostra anche l’uso degli if annidati (un if all’interno di un altro if, può servire a gestire situazioni in cui ci sono più di due condizioni):

if(exists("cinque")) rm(cinque) # per eliminare l'oggetto col nome cinque, se esiste
cinquenumeri <- c(1,2,3,4,5)
for(i in seq_along(cinquenumeri)){
  if(cinquenumeri[i]%%2==0){
    cat("la posizione", i, "vale", cinquenumeri[i], " ed è pari\n", sep = " ")
  } else {
    cat("la posizione", i, "vale", cinquenumeri[i], "ed è dispari\n", sep = " ")
    if(cinquenumeri[i] == 5) {
      cinque<-"cinque è il numero perfetto"
      print(cinque)
      }
  }
}
## la posizione 1 vale 1 ed è dispari
## la posizione 2 vale 2  ed è pari
## la posizione 3 vale 3 ed è dispari
## la posizione 4 vale 4  ed è pari
## la posizione 5 vale 5 ed è dispari
## [1] "cinque è il numero perfetto"

Prova, in uno script, ad usare per cinquenumeri un vettore di cinque numeri che non contenga il 5: che pensi che succederà?

Gli if() sono molto usati nell’error trapping, una tecnica usata per “intrappolare” eventuali errori causati dall’esecuzione di una funzione o uno script, ridirigendo il flusso di esecuzione o generando un messaggio di errore o un warning. Se vuoi saperne di più prova a leggere [questa sezione] (https://adv-r.hadley.nz/conditions.html) di Advanced R.

In realtà, ci sono modi più semplici di gestire condizioni multiple, come il comando switch() di R base o il comando dplyr::case_when():

# cerca switch nell'aiuto, se vuoi
# ?switch
xx <- 1:15
# un altro esempio stupido con la funzione modulo
case_when(
  xx %% 15 == 0 ~ "divisibile per 15, 5 e 3",
  xx %% 5 == 0 ~ "divisibile per 5",
  xx %% 3 == 0 ~ "divisibile per 3",
  TRUE ~ as.character(xx)
)
##  [1] "1"                        "2"                       
##  [3] "divisibile per 3"         "4"                       
##  [5] "divisibile per 5"         "divisibile per 3"        
##  [7] "7"                        "8"                       
##  [9] "divisibile per 3"         "divisibile per 5"        
## [11] "11"                       "divisibile per 3"        
## [13] "13"                       "14"                      
## [15] "divisibile per 15, 5 e 3"

L’ultima istruzione serve, genericamente, per restituire il valore di xx come carattere.

Infine, se si desidera operare con una condizione su un vettore di condizioni logiche di lunghezza maggiore di uno, è possibile usare il comando ifelse() o, meglio ancora, dplyr::if_else(): in entrambi i casi la struttura è: if_else(vettore_logico, esegui se vero, esegui se falso)

x <- c(-1,1,-1,1,0)
y <- ifelse(x>0,1,0)
z <- if_else(x>=0,"positivo o zero","negativo")
# nota che i due comandi trattano lo 0 in modi diversi
y
## [1] 0 1 0 1 0
z
## [1] "negativo"        "positivo o zero" "negativo"        "positivo o zero"
## [5] "positivo o zero"
# guarda come ifelse() o if_else() possono essere usati per gestire 
# condizioni che causano dei warning
sqrt(x) # genera dei warning
## Warning in sqrt(x): NaNs produced
## [1] NaN   1 NaN   1   0
sqrt(ifelse(x>=0,x,NA)) # niente warning, ma restituisce, NA per i valori negativi
## [1] NA  1 NA  1  0

9.5 La soluzione dell’esercizio.

Prova questo codice in uno script:

mpg %>%
  summarise(across(c(hwy,cty),
                   list(media = mean, devst = sd)),
            .by = class) %>%
  mutate(across(where(is.numeric), ~round(.x, digits = 2))) %>%
  unite("hwy",starts_with("hwy"), sep="±") %>%
  unite("cty",starts_with("cty"), sep="±")

Incidentalmente, questo è proprio i modo in cui mostreresti una tabella con media e deviazione standard in un lavoro scientifico: pensa che noia rifare la tabella a mano…

9.6 Altre risorse.

9.6.1 Risorse in italiano.

Documenti e pagine web.

9.6.2 Risorse in inglese.

Come sempre, c’è a scegliere


  1. rispetto all’analisi dei dati, è ovvio↩︎

  2. e, indovina? Ti servirà sicuramente…↩︎

  3. un po’ come accade nel vecchio pacchetto reshape2↩︎

  4. ricorda, un data frame è una lista, quindi nulla vieta che una delle colonne sia una colonna di liste↩︎

  5. ho usato questa sintassi perché come vedrai, in funzione dell’ordine di caricamento dei pacchetti il comando filter potrebbe essere “mascherato” da altri comandi.↩︎

  6. scrivi nella console >help(language, package = "tidyselect")↩︎

  7. ma con qualche differenza dalle funzioni base: leggi l’aiuto, senti a me…↩︎