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 condplyr
)passare dai formati
long
awide
, e viceversa conpivot_wider
epivot_longer
unire diverse colonne con
unite
o separare una colonna in più colonne conseparate
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
odplyr::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
odplyr::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
didplyr
E molto altro ancora…
Questo approccio ha alcune cose in comune:
si parte da un
data frame
o unatibble
, in generale all’inizio di un percorso di comandi separati dapipe
: è l’oggetto dell’analisisi 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à chiamatatidy evaluation
, che non richiede l’uso delle modalità di selezione di R base (vedi paragrafo 4.8); avverbi (comeacross
ewhere
) possono essere usati per individuare in maniera più generale le variabili su cui operare; incidentalmente, nei comandi dei pacchetti deltidyverse
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 dalong
awide
;billboard
ha il problema inverso: i dati della posizione in classifica sono nelle variabili wk1-wk76: dovremmo creare una variabile wk e trasformare dawide
along
; 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 dawho
)
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%")
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%")
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:
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;
estrarre la sigla del ceppo
creare nella tabella 9.2 una nuova colonna unendo le colonne con l’abbreviazione di genere e specie
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 tidyr
e 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
dipivot_longer
individua le colonne da utilizzare nell’operazione di “fusione” del data frame mentrenames_to
evalues_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 consummarise
ho separato la colonna
Ceppo_specie
in tre elementi usandoseparate
, eliminando poi quelli che non servivano conselect
; consulta l’aiuto diseparate
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:
## 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
## 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
## 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
## # 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.
## # 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
## [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
## [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)`
## # 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 destraleft_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 destraright_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 destraanti_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 10all’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()
## [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
## [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
## [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 successivabreak
: 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 outputle 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
## 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
oFALSE
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
## [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
## [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.2 Risorse in inglese.
Come sempre, c’è a scegliere
il sito del tidyverse dà una visione organica dei pacchetti per la manipolazione “tidy” dei dati
il concetto di “tidy data” è descritto in maniera eccellente da Hadley Wickham
le vignette dei pacchetti del tidyverse sono un’ottima guida per approfondire gli aspetti dei singoli pacchetti
un’ottima guida allo stile di programmazione è quella del tidyverse
si sono diversi eccellenti libri che includono ampi capitoli sul data wrangling o sono specializzati sulla programmazione con R:
il solito R for data science ormai disponibile anche in lingua italiana
Hands on programming with R fornisce un’ottima introduzione per chi non è neanche abituato all’idea di ragionare per algoritmi
R programming for data science sicuramente più articolato e completo e parecchio più noioso
Advanced R è, ovviamente, la guida definitiva alla programmazione avanzata con R.
rispetto all’analisi dei dati, è ovvio↩︎
e, indovina? Ti servirà sicuramente…↩︎
un po’ come accade nel vecchio pacchetto
reshape2
↩︎ricorda, un data frame è una lista, quindi nulla vieta che una delle colonne sia una colonna di liste↩︎
ho usato questa sintassi perché come vedrai, in funzione dell’ordine di caricamento dei pacchetti il comando filter potrebbe essere “mascherato” da altri comandi.↩︎
scrivi nella console
>help(language, package = "tidyselect")
↩︎ma con qualche differenza dalle funzioni base: leggi l’aiuto, senti a me…↩︎