Utilizzo di nomi di colonne come argomenti di una funzione
Per utilizzare i nomi di colonne di una tabella come argomenti di una funzione è necessario utilizzare una strategia diversa. Per mostrare l’approccio da seguire sfruttiamo la tabella diamonds``{ggplot2}, di cui stampiamo le prime righe a video:
# A tibble: 6 × 10
carat cut color clarity depth table price x y z
<dbl> <ord> <ord> <ord> <dbl> <dbl> <int> <dbl> <dbl> <dbl>
1 0.23 Ideal E SI2 61.5 55 326 3.95 3.98 2.43
2 0.21 Premium E SI1 59.8 61 326 3.89 3.84 2.31
3 0.23 Good E VS1 56.9 65 327 4.05 4.07 2.31
4 0.29 Premium I VS2 62.4 58 334 4.2 4.23 2.63
5 0.31 Good J SI2 63.3 58 335 4.34 4.35 2.75
6 0.24 Very Good J VVS2 62.8 57 336 3.94 3.96 2.48
Siamo ora interessati a definire una funzione che calcoli una statistica di sintesi, la media ad esempio, su una variabili numerica per i vari livelli di una variabile di stratificazione, ed una seconda funzione che permetta di ottenere un grafico utilizzando gli argomenti per specificare le variabili da rappresentare.
Costruiamo a titolo di esempio una tabella di sintesi che contenga i prezzi medi dei diamanti (colonna price) per i vari livelli della variabile clarity:
Costruiamo una rappresentazione grafica organizzata in pannelli dove i livelli della variabile clarity sono associati ai pannelli, in ciascuno dei quali rappresentiamo un diagramma di dispersione delle variabili carat e price:
ggplot(diamonds) +aes(x = carat, y = price) +geom_point() +facet_wrap(facets =vars(clarity))
Per utilizzare i nomi di colonna come argomenti di una funzione è necessario un doppio passaggio: il nome della colonna passato in input deve prima essere trasformato usando la funzione ensym, che indica al sistema che l’argomento contiene un “simbolo” (il nome di una colonna in questo caso), e l’oggetto risultante deve essere utilizzato all’interno di una doppia parentesi graffa per ottenere il risultato desiderato. La seguente funzione prende tre argomenti di input: la tabella dati, la variabile di stratificazione e la variabile da sintetizzare:
I seguenti esempi di chiamata mostra che i nomi delle colonne possono essere passate in input usando la stessa sintassi delle funzioni del mondo tidyverse (senza racchiuderli tra virgolette) ma anche come stringhe di testo che contengono i nomi delle colonne da utilizzare per il calcolo:
E’ possibile anche definire un nome in modo dinamico componendo una stringa che contiene il nome della colonna passato in input. Ecco un piccolo miglioramento della funzione precedente che usa nella tabella di output il nome della colonna in input con un suffisso che descrive la funzione di sintesi calcolata (da notare che in questo caso è necessario l’utilizzo dell’operatore := invece del classico =:
La funzione così definita può essere utilizzata su tabelle diverse specificando in maniera opportune la colonna di stratificazione e quella di calcolo. Ecco un esempio che calcola la lunghezza media della variabile Sepal.Length per i vari livelli della variabile Species sulla tabella iris disponibile direttamente in R:
calcola_tabella(iris, Species, Sepal.Length)
# A tibble: 3 × 2
Species Sepal.Length_avg
<fct> <dbl>
1 setosa 5.01
2 versicolor 5.94
3 virginica 6.59
Ed ecco il calcolo dell’aspettativa di vita media (lifeExp come variabile di sintesi) per i vari continenti (continent come variabile di stratificazione) sui dati della tabella gapminder``{gapminder}:
# A tibble: 5 × 2
continent lifeExp_avg
<fct> <dbl>
1 Africa 48.9
2 Americas 64.7
3 Asia 60.1
4 Europe 71.9
5 Oceania 74.3
Un’ulteriore generalizzazione può essere ottenuta inserendo un ulteriore argomento che permetta di personalizzare anche la funzione di sintesi utilizzata, personalizzando di conseguenza anche il nome della colonna di sintesi:
Se non si utilizza l’argomento fun, la funzione crea_tabella calcola per default la media come funzione di sintesi:
calcola_tabella_fun(diamanti, cut, price)
# A tibble: 5 × 2
cut price_mean
<ord> <dbl>
1 Fair 4359.
2 Good 3929.
3 Very Good 3982.
4 Premium 4584.
5 Ideal 3458.
Ma è possibile calcolare qualunque funzione di sintesi; ecco un esempio che calcola il minimo della variabile price per i differenti livelli della variabile cut sulla tabella diamonds:
calcola_tabella_fun(diamonds, cut, price, fun = min)
# A tibble: 5 × 2
cut price_min
<ord> <int>
1 Fair 337
2 Good 327
3 Very Good 336
4 Premium 326
5 Ideal 326
La funzione crea_tabella può essere utilizzata su altre tabelle e all’interno di catene di comandi ottenute usando l’operatore pipe. Ecco il calcolo della tabella delle aspettative di vita media per i vari continenti a partire dai dati contenuti nella tabella gapminder``{gapminder} con riferimento al solo anno 2007:
# A tibble: 5 × 2
continent lifeExp_mean
<fct> <dbl>
1 Africa 54.8
2 Americas 73.6
3 Asia 70.7
4 Europe 77.6
5 Oceania 80.7
ed ecco la stessa tabella che invece delle medie calcola gli scarti quadratici medi:
gapminder::gapminder |>filter(year ==2007) |>calcola_tabella_fun(continent, pop, fun = sd)
# A tibble: 5 × 2
continent pop_sd
<fct> <dbl>
1 Africa 24917726.
2 Americas 68833781.
3 Asia 289673399.
4 Europe 23624744.
5 Oceania 11538855.
Estensioni al caso di funzioni “grafiche”
Lo stesso approccio può essere utilizzato per definire delle funzioni utili a definire una sorta di modello di rappresentazione grafica da utilizzare su differenti tabelle e differenti variabili. A titolo esemplificato, la seguente funzione prende in input tre argomenti, una tabella dati e due variabili in essa contenute e traccia uno scatterplot sfruttando il package ggplot2. Le variabili, come visto nel caso sopra, possono essere passate come simbolo o come stringa:
Il ciclo for permette di effettuare un’iterazione su più elementi di una struttura dati.
Consideriamo un semplice esempio in cui sfruttando un semplice esempio una variabile detta contatore (i nell’esempio) viene utilizzata per muoversi lungo un vettore, stampando a video il valore del contatore e l’elemento nella corrispondente posizione:
x <-c(10, 20, 30, 40, 50)for(i in1:length(x)){print(paste("Posizione", i, "del vettore:"))print(paste("Valore alla posizione", i, "del vettore", x[i]))}
[1] "Posizione 1 del vettore:"
[1] "Valore alla posizione 1 del vettore 10"
[1] "Posizione 2 del vettore:"
[1] "Valore alla posizione 2 del vettore 20"
[1] "Posizione 3 del vettore:"
[1] "Valore alla posizione 3 del vettore 30"
[1] "Posizione 4 del vettore:"
[1] "Valore alla posizione 4 del vettore 40"
[1] "Posizione 5 del vettore:"
[1] "Valore alla posizione 5 del vettore 50"
for(i inseq_along(x)){print(paste("Posizione", i, "del vettore:"))print(paste("Valore alla posizione", i, "del vettore", x[i]))}
[1] "Posizione 1 del vettore:"
[1] "Valore alla posizione 1 del vettore 10"
[1] "Posizione 2 del vettore:"
[1] "Valore alla posizione 2 del vettore 20"
[1] "Posizione 3 del vettore:"
[1] "Valore alla posizione 3 del vettore 30"
[1] "Posizione 4 del vettore:"
[1] "Valore alla posizione 4 del vettore 40"
[1] "Posizione 5 del vettore:"
[1] "Valore alla posizione 5 del vettore 50"
for(el in x){print(paste("Valore del vettore:", el))}
[1] "Valore del vettore: 10"
[1] "Valore del vettore: 20"
[1] "Valore del vettore: 30"
[1] "Valore del vettore: 40"
[1] "Valore del vettore: 50"
Proviamo ad usare il ciclo for per definire una funzione che calcoli la sommatoria di un vettore in input:
sommatoria <-function(x){for(i inseq_along(x)){if(i ==1) somma = x[i]else somma = somma + x[i] }return(somma)}
Seppure la funzione calcola correttamente la sommatoria:
sommatoria(c(10, 20, 30))
[1] 60
sfrutta un if per calcolare il primo valore da attribuire alla somma quando inizia lo scorrimento del vettore. Si può ottenere una struttura più snella senza ricorrere all’if e sfruttando una variabile di accumulo inizializzata a 0 prima dell’iterazione:
sommatoria <-function(x){ somma <-0for(i inseq_along(x)){ somma = somma + x[i] }return(somma)}sommatoria(c(10, 20, 30))
[1] 60
Usando lo stesso approccio è possibile definire una funzione che calcoli la produttoria, inizializzando questa volta la variabile di accumulo ad 1:
Iterazione sfruttando la funzione map (e varianti)
Il package purrr, disponibile direttamente quando si carica tidyverse introduce la funzione map e le sue varianti che possono essere utilizzate come alternative al ciclo for. Sebbene non ci siano differenze in termini di velocità di esecuzione se il ciclo for è ben strutturato, la sintassi di map spesso permette di ottenere una sintassi più snella.
Consideriamo ad esempio il seguente ciclo for che stampa a video gli elementi di un vettore:
x <-c(10, 11, 12, 13, 14, 15)for(i inseq_along(x))print(x[i])
[1] 10
[1] 11
[1] 12
[1] 13
[1] 14
[1] 15
L’equivalente si può ottenere usando la funzione map:
La funzione map restituisce sempre una lista della stessa lunghezza del primo argomento. Mostriamo un esempio “inutile” che calcola il logaritmo in base 2 degli elementi di un vettore:
Una sintassi più compatta si può ottenere sfruttando la ~ per utilizzare direttamente la sintassi tipica funzionale, usando .x come segnaposto per gli elementi su cui la funzione map effettua l’iterazione:
La funzione map_vec permette di semplificare la struttura di output in un vettore laddove questo sia possibile:
map_vec(c(10, 20, 30), log, base =2)
[1] 3.321928 4.321928 4.906891
L’esempio sopra è meramente didascalico ed “inutile” perchè la maggior parte delle funzioni di R, tra cui anche la funzione log, lavorano direttamente elemento ad elemento quando utilizzate su un vettore:
log(c(10, 20, 30), base =2)
[1] 3.321928 4.321928 4.906891
Esistono diverse varianti della funzione map, map_*, che possono essere utilizzate per cercare di ottenere una struttura vettoriale in output laddove possibile forzando la tipologica di dati ottenuta. Il seguente esempio sfrutta la funzione map_lgl per ottenere un vettore di valori booleani che controllano se un numero è pari (TRUE) o dispari (FALSE):
Si ottiene lo stesso risultato sfruttando la più generale funzione map_vec che determina automaticamente la modalità del vettore in base al tipo di dati:
Proviamo ora ad applicare i cicli di iterazionefor e la variante funzionale map a qualcosa di più interessante, ovvero al calcolo delle medie di un gruppo di variabili numeriche i cui nomi sono memorizzati in un vettore di stringhe. Vale la pena evidenziare che a differenza di quanto avviene richiamando una funzione sul prompt senza assegnazione ad un oggetto, se si utilizza una funzione all’interno di un ciclo for l’espressione non produce alcun output a video, come è possibile verificare usando il seguente chunk di codice:
E’ naturalmente possibile usare una delle sintassi alternativa già viste in precedenza per far muovere il contatore del ciclo for, ovvero v_num in vars_numeriche:
vars_numeriche <-c("price", "x", "y", "z")for(v_num in vars_numeriche)print(mean(diamonds[[v_num]]))
[1] 3932.8
[1] 5.731157
[1] 5.734526
[1] 3.538734
oppure sfruttare la funzione seq_along come di seguito:
Per creare un vettore con le medie delle variabili numeriche di interesse possiamo inizializzare prima del ciclo for un vettore di lunghezza pari al numero di variabili:
Possiamo sfruttare il vettore con i nomi delle variabili per assegnare un nome agli elementi del vettore:
names(vet_medie) <- vars_numerichevet_medie
price x y z
3932.799722 5.731157 5.734526 3.538734
La funzione map ci permette di ottenere lo stesso risultato usando un’espressione più concisa. Calcoliamo ad esempio la media di tutte le variabili numeriche presenti nella tabella diamanti:
per calcolare le medie di ciascuna di queste variabili in corrispondenza dei vari livelli della variabile cut, sfruttando la funzione calcola_tabella creata in precedenza: