var_pop <- function(x){
n <- length(x)
scarti_quad <- (x - mean(x))^2
ssq <- sum(scarti_quad)
var_distorta <- ssq / n
return(var_distorta)
}Creazione di funzioni in R (approfondimenti)
La funzione varianza campionaria non corretta
Le funzioni definite dall’utente possono essere utilizzate allo stesso modo delle funzioni predefinite in R anche nel mondo tidyverse. Supponiamo ad esempio di essere interessati a definire una funzione che calcoli la varianza campionaria non corretta, ovvero dividendo per n e non per (n - 1) la somma dei quadrati degli scarti dalla media.
Possiamo definire la funzione effettuando tutti i calcoli, ovvero calcolando il vettore degli scarti al quadrato dalla media, la devianza e la varianza campionaria dividendo per n:
Oppure possiamo sfruttare la funzione var predefinita in R effettuando una semplice trasformazione:
var_pop_smart <- function(x){
n <- length(x)
return(var(x) * (n- 1) / n)
}Utilizziamo la funzione var e le due funzioni personalizzate su una colonna della tabella gapminder{gapminder} passata in input come un vettore sfruttando l’operatore $ per estrarre la colonna dalla tabella:
library(tidyverse)
gap_data <- gapminder::gapminder
var(gap_data$lifeExp)[1] 166.8517
var_pop(gap_data$lifeExp)[1] 166.7537
var_pop_smart(gap_data$lifeExp)[1] 166.7537
Utilizzo di una funzione personalizzata con l’approccio tidyverse
Le funzioni definite dall’utente possono essere utilizzate allo stesso modo delle funzioni predefinite di R all’interno dei verbi del mondo tidyverse:
gap_data |>
summarise(var(lifeExp), var(gdpPercap),
var_pop(lifeExp), var_pop(gdpPercap),
var_pop_smart(lifeExp), var_pop_smart(gdpPercap)) |>
knitr::kable()| var(lifeExp) | var(gdpPercap) | var_pop(lifeExp) | var_pop(gdpPercap) | var_pop_smart(lifeExp) | var_pop_smart(gdpPercap) |
|---|---|---|---|---|---|
| 166.8517 | 97169410 | 166.7537 | 97112386 | 166.7537 | 97112386 |
Ecco un esempio che effettua gli stessi calcoli sfruttando l’avverbio across:
gap_data |>
summarise(across(.cols = c(lifeExp, gdpPercap),
.fns = c(var, var_pop, var_pop_smart))) |>
knitr::kable()| lifeExp_1 | lifeExp_2 | lifeExp_3 | gdpPercap_1 | gdpPercap_2 | gdpPercap_3 |
|---|---|---|---|---|---|
| 166.8517 | 166.7537 | 166.7537 | 97169410 | 97112386 | 97112386 |
E’ possibile assegnare un nome alle colonne per favorire la leggibilità delle colonne della tabella di input:
gap_data |>
summarise(across(.cols = c(lifeExp, gdpPercap),
.fns = c(var = var,
var_pop = var_pop,
var_pop_smart = var_pop_smart))) |>
knitr::kable()| lifeExp_var | lifeExp_var_pop | lifeExp_var_pop_smart | gdpPercap_var | gdpPercap_var_pop | gdpPercap_var_pop_smart |
|---|---|---|---|---|---|
| 166.8517 | 166.7537 | 166.7537 | 97169410 | 97112386 | 97112386 |
In molti casi è possibile ottenere lo stesso risultato senza necessità di definire una nuova funzione ma sfruttando opportune operazioni di manipolazione. Ecco un esempio che sfrutta la funzione mutate per trasformare il risultato della funzione predefinita var in modo da ottenere la varianza campionaria non corretta:
n <- nrow(gap_data)
gap_data |>
summarise(across(.cols = c(lifeExp, gdpPercap),
.fns = var)) |>
mutate(lifeExp = lifeExp * (n - 1) / n,
gdpPercap = gdpPercap * (n - 1) / n) |>
knitr::kable()| lifeExp | gdpPercap |
|---|---|
| 166.7537 | 97112386 |
Definizione di funzioni anonime (on-the-fly)
Nel caso non si è interessati a riutilizzare più volte una funzione, è possibile utilizzare funzioni anonime senza creare un corrispondente oggetto sul workspace:
gap_data |>
summarise(across(.cols = c(lifeExp, gdpPercap),
.fns = function(x) sum((x - mean(x))^2) / length(x))) |>
knitr::kable()| lifeExp | gdpPercap |
|---|---|
| 166.7537 | 97112386 |
Nell’esempio precedente la funzione consisteva di un’unica espressione per cui non è stato necessario delimitare il corpo della funzione dalle parentesi graffe. Anche se solitamente si ricorre a funzioni anonime per calcoli veloci, è possibile definire una funzione anonima su più righe, come nella seguente variante:
gap_data |>
summarise(across(.cols = c(lifeExp, gdpPercap),
.fns = function(x) {
n <- length(x)
ssq_quad <- sum(x - mean(x))^2
var_pop <- ssq_quad / n
return(var_pop)
})) |>
knitr::kable()| lifeExp | gdpPercap |
|---|---|
| 0 | 0 |
Le funzioni anonime possono essere definite in due modi, ovvero usando la parola chiave function, che è stata utilizzata anche per definire oggetti di tipo function sul workspace, oppure usando una sintassi \( ). La seguente funzione che effettua una trasformazione dell’output della funzione predefinita var:
gap_data |>
summarise(across(.cols = c(lifeExp, gdpPercap),
.fns = function(x) var(x) * length(x - 1) / length(x) )) |>
knitr::kable()| lifeExp | gdpPercap |
|---|---|
| 166.8517 | 97169410 |
è perciò equivalente alla seguente funzione anonima definita usando la sintassi \(x):
gap_data |>
summarise(across(.cols = c(lifeExp, gdpPercap),
.fns = \(x) var(x) * length(x - 1) / length(x) )) |>
knitr::kable()| lifeExp | gdpPercap |
|---|---|
| 166.8517 | 97169410 |
Se la funzione richiede più argomenti in input si utilizza la stessa logica: una funzione che prende due argomenti in input definita come function(x, y) è equivalente alla definizione breve \(x, y), ed analogamente nel caso di più di due argomenti.
Un ulteriore esempio con la funzione quantile
Le funzioni anonime sono utili anche nel caso in cui si è interessati a passare argomenti aggiuntivi ad una funzione all’interno dei verbi tidyverse. Consideriamo ad esempio la funzione quantile che calcola per default la sintesi a cinque di un vettore:
quantile(gap_data$lifeExp) 0% 25% 50% 75% 100%
23.5990 48.1980 60.7125 70.8455 82.6030
Possiamo calcolare un quantile specifico sfruttando l’argomento probs, passandogli un valore con la sintassi di chiamata per nome:
quantile(gap_data$lifeExp, probs = 0.75) 75%
70.8455
o per posizione:
quantile(gap_data$lifeExp, 0.75) 75%
70.8455
Supponiamo ora di essere interessati a calcolare il terzo quartile di due variabili della nostra tabella. Possiamo utilizzare la sintassi estesa esplicitando la chiamata per ciascuna colonna:
gap_data |> summarise(quantile(lifeExp, 0.75),
quantile(gdpPercap, 0.75))# A tibble: 1 × 2
`quantile(lifeExp, 0.75)` `quantile(gdpPercap, 0.75)`
<dbl> <dbl>
1 70.8 9325.
Oppure provare a passare il quantile di interesse come argomento aggiuntivo alla funzione quantile sfruttando l’argomento ...:
gap_data |>
summarise(across(.cols = c(lifeExp, gdpPercap),
.fns = quantile,
probs = 0.75))Warning: There was 1 warning in `summarise()`.
ℹ In argument: `across(.cols = c(lifeExp, gdpPercap), .fns = quantile, probs =
0.75)`.
Caused by warning:
! The `...` argument of `across()` is deprecated as of dplyr 1.1.0.
Supply arguments directly to `.fns` through an anonymous function instead.
# Previously
across(a:b, mean, na.rm = TRUE)
# Now
across(a:b, \(x) mean(x, na.rm = TRUE))
# A tibble: 1 × 2
lifeExp gdpPercap
<dbl> <dbl>
1 70.8 9325.
Il messaggio di warning che questo tipo di chiamata è deprecated (ovvero non sarà più supportato nelle future versioni) e ci invita ad utilizzare una funzione anonima per il passaggio di ulteriori argomenti alla funzione di calcolo fornendoci anche un esempio per il calcolo della media di un vettore che contiene missing value. Nel nostro caso la sintassi consigliata è pertanto:
gap_data |>
summarise(across(.cols = c(lifeExp, gdpPercap),
.fns = \(x) quantile(x, 0.75)))# A tibble: 1 × 2
lifeExp gdpPercap
<dbl> <dbl>
1 70.8 9325.
Una nota sul reframe
Se utilizziamo all’interno del verbo summarise una funzione che restituisce più di un valore di output il sistema ci segnala che questo tipo di funzionalità è deprecated e ci invita ad utilizzare la funzione reframe. Supponiamo ad esempio di essere interessati a calcolare i tre quartili di una (o più) colonna(e) di una tabella:
gap_data |> summarise(quantile(lifeExp, probs = c(0.25, 0.5, 0.75)))Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
dplyr 1.1.0.
ℹ Please use `reframe()` instead.
ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
always returns an ungrouped data frame and adjust accordingly.
# A tibble: 3 × 1
`quantile(lifeExp, probs = c(0.25, 0.5, 0.75))`
<dbl>
1 48.2
2 60.7
3 70.8
Possiamo definire una funzione di servizio quartili che calcola per default i tre quartili di un vettore in input:
quartili <- function(x, probs = c(0.25, 0.5, 0.75)) {
tibble(
value = quantile(x, probs, na.rm = TRUE),
prob = probs
)
}e sfruttare il verbo reframe invece di summarise:
gap_data |>
reframe(across(.cols = c(lifeExp, gdpPercap),
.fns = quartili))# A tibble: 3 × 2
lifeExp$value $prob gdpPercap$value $prob
<dbl> <dbl> <dbl> <dbl>
1 48.2 0.25 1202. 0.25
2 60.7 0.5 3532. 0.5
3 70.8 0.75 9325. 0.75
La funzione reframe restituisce un packed data frame, ovvero una tabella in cui alcune colonne sono innestate all’interno di altre. Questo si può notare già se assegniamo l’outout restituito da reframe ad un oggetto sul workspace:
df_packed <- gap_data |>
reframe(across(.cols = c(lifeExp, gdpPercap),
.fns = quartili))e confrontiamo il contenuto dell’oggetto:
df_packed# A tibble: 3 × 2
lifeExp$value $prob gdpPercap$value $prob
<dbl> <dbl> <dbl> <dbl>
1 48.2 0.25 1202. 0.25
2 60.7 0.5 3532. 0.5
3 70.8 0.75 9325. 0.75
con i nomi delle colonne ottenuti sfruttando la funzione colnames:
colnames(df_packed)[1] "lifeExp" "gdpPercap"
In un packed data frame è possibile accedere alla struttura innestata usando l’operatore $ Qui ad esempio ispeziono il risultato della funzione quartile per la variabile lifeExp:
df_packed$lifeExp# A tibble: 3 × 2
value prob
<dbl> <dbl>
1 48.2 0.25
2 60.7 0.5
3 70.8 0.75
e posso poi estrarre i soli quantili di interesse usando di nuovo l’operatore $ per accedere alla colonna di interesse:
df_packed$lifeExp$value 25% 50% 75%
48.1980 60.7125 70.8455
Se non sono interessato a conservare la struttura innestata posso impostare a TRUE l’argomento .unpack della funzione reframe:
df_unpack <- gap_data |>
reframe(across(.cols = c(lifeExp, gdpPercap),
.fns = quartili,
.unpack = TRUE))Questa volta il contenuto dell’oggetto:
df_unpack# A tibble: 3 × 4
lifeExp_value lifeExp_prob gdpPercap_value gdpPercap_prob
<dbl> <dbl> <dbl> <dbl>
1 48.2 0.25 1202. 0.25
2 60.7 0.5 3532. 0.5
3 70.8 0.75 9325. 0.75
è coerente con i nomi delle colonne:
colnames(df_unpack)[1] "lifeExp_value" "lifeExp_prob" "gdpPercap_value" "gdpPercap_prob"
Da notare che per la funzione reframe è disponibile anche un ulteriore argomento .by (analogo a quello di summarise) che permette di calcolare la funzione per gruppi. Ecco un esempio di calcolo dei quartili della colonna lifeExp per i vari continenti:
gap_data |>
reframe(across(.cols = c(lifeExp, gdpPercap),
.fns = quartili),
.by = continent)# A tibble: 15 × 3
continent lifeExp$value $prob gdpPercap$value $prob
<fct> <dbl> <dbl> <dbl> <dbl>
1 Asia 51.4 0.25 1057. 0.25
2 Asia 61.8 0.5 2647. 0.5
3 Asia 69.5 0.75 8549. 0.75
4 Europe 69.6 0.25 7213. 0.25
5 Europe 72.2 0.5 12082. 0.5
6 Europe 75.5 0.75 20461. 0.75
7 Africa 42.4 0.25 761. 0.25
8 Africa 47.8 0.5 1192. 0.5
9 Africa 54.4 0.75 2377. 0.75
10 Americas 58.4 0.25 3428. 0.25
11 Americas 67.0 0.5 5466. 0.5
12 Americas 71.7 0.75 7830. 0.75
13 Oceania 71.2 0.25 14142. 0.25
14 Oceania 73.7 0.5 17983. 0.5
15 Oceania 77.6 0.75 22214. 0.75
Impostando a TRUE l’argomento .unpack posso “liberarmi” della struttura innestata della tabella:
gap_data |>
reframe(across(.cols = c(lifeExp, gdpPercap),
.fns = quartili,
.unpack = TRUE),
.by = continent)# A tibble: 15 × 5
continent lifeExp_value lifeExp_prob gdpPercap_value gdpPercap_prob
<fct> <dbl> <dbl> <dbl> <dbl>
1 Asia 51.4 0.25 1057. 0.25
2 Asia 61.8 0.5 2647. 0.5
3 Asia 69.5 0.75 8549. 0.75
4 Europe 69.6 0.25 7213. 0.25
5 Europe 72.2 0.5 12082. 0.5
6 Europe 75.5 0.75 20461. 0.75
7 Africa 42.4 0.25 761. 0.25
8 Africa 47.8 0.5 1192. 0.5
9 Africa 54.4 0.75 2377. 0.75
10 Americas 58.4 0.25 3428. 0.25
11 Americas 67.0 0.5 5466. 0.5
12 Americas 71.7 0.75 7830. 0.75
13 Oceania 71.2 0.25 14142. 0.25
14 Oceania 73.7 0.5 17983. 0.5
15 Oceania 77.6 0.75 22214. 0.75
Utilizzo dei ... come argomento di una funzione
Vediamo ora un semplice esempio che sfrutta l’argomento ... che è predefinito in molte funzioni di R: si tratta di un argomento che permette di passare in input uno o più argomenti che possono essere a loro volta “veicolati” in input ad una seconda funzione. Un semplice (quanto inutile) esempio consiste nel definire una funzione che traccia un istogramma usando un colore blu per le barre:
istogramma_blue <- function(x, ...){
hist(x, col = "darkblue", ...)
}Tracciamo l’istogramma della variabile lifeExp:
istogramma_blue(gap_data$lifeExp)
Se passiamo ulteriori argomenti non definiti esplicitamente nella testata della funzione istogramma_blue questi vengono veicolati alla funzione hist tramite i .... Ecco un esempio in cui fisso il numero di classi desiderate usando l’argomento breaks:
istogramma_blue(gap_data$lifeExp, breaks = 7)
oppure gli argomenti che permettono di definire titolo del grafico e titolo degli assi:
istogramma_blue(gap_data$lifeExp,
main = "Titolo",
xlab = "etichetta X",
ylab = "etichetta Y")