Tabyl e dintorni {janitor}

Author

Domenico Vistocco

Una premessa: l’operatore pipe (|> e/o %>%)

Nelle precedenti lezioni abbiamo visto la sintassi standard per la chiamata di una funzione in R:

# chiamata di funzione con passaggio degli argomenti per nome
log(10, 2)
[1] 3.321928
# chiamata di funzione con passaggio degli argomenti per posizione
log(x = 10, base = 2)
[1] 3.321928

A partire dalle recenti versioni di R è disponibile un operatore nativo (il pipe |>), che implementa funzionalità analoghe all’operatore pipe introdotto dal package magrittr ed ampiamente sfruttato nella creazione di catene di codice (pipeline) che rovesciano il punto di vista classico della chiamata di una funzione. In particolare, il primo operando del pipe diventa il primo argomento della funzione che è al secondo operando, consentendo così di creare una catena di comandi, o funzioni innestate, che permettono di scomporre un comando in piccoli pezzi senza dover creare variabili sul workspace.

L’equivalente (inutile) della chiamata della precedente funzione sfruttando il pipe è cioè:

# il primo operando del pipe (10) diventa il primo argomento della
# funzione al secondo operando (log)
10 |> log(2)
[1] 3.321928
# per sfruttare il pipe del package magrittr è necessario caricarlo
# in memoria (nota: il package è automaticamente disponibile quando
# si carica tidyverse)
library(magrittr)
10 %>% log(2)
[1] 3.321928

E’ possibile spostare la posizione di utilizzo del primo operando nella funzione al secondo operando usando un placeholder (segnaposto), che è differente nei due operatori pipe:

# usando _ indico al pipe nativo che il 2 deve essere usato come
# secondo argomento della funzione
2 |> log(10, base = _)
[1] 3.321928
# comando analogo per il pipe di magrittr: in questo caso il
# segnaposto è il .
2 %>% log(10, base = .)
[1] 3.321928

Oltre al carattere utilizzato come segnaposto ci sono ulteriori differenze tra il pipe nativo e il pipe di magrittr, che però sono apprezzabili sono in utilizzi avanzati di questo tipo di sintassi. Vale la pena solo evidenziare la seguente:

# calcolo la media (funzione al secondo operando) di una sequenza di 
# numeri (ottenuta al primo operando) con il pipe nativo
seq(1, 10, by = 2) |> mean()
[1] 5
# comando analogo sfruttando il pipe di magrittr
seq(1, 10, by = 2) %>% mean()
[1] 5
# quando si utilizza il pipe di magrittr è possibile omettere le parentesi
# nella funzione al secondo membro nel caso in cui non siano presenti
# ulteriori argomenti oltre a quello passato con il %>%
# non è possibile lo stesso usando il pipe nativo
seq(1, 10, by = 2) %>% mean
[1] 5

Nel caso degli esempi illustrativi sopra riportati il ricorso all’operatore pipe non è molto utile, ed anzi spesso può essere anche fuorviante. Questo tipo di operatore è invece molto comodo in diversi contesti di analisi di dati (in particolare per la data manipulation) che richiedono l’utilizzo di funzioni innestate che possono essere scomposte in una catena di chiamate di funzioni successive, come si vedrà anche nel seguito di questo tutorial.

La logica del pipe è cioè rovesciata. Consideriamo a titolo illustrativo il seguente esempio di una chiamata innestata di cinque funzioni:

f5(f4(f3(f2(f1(input)))))

Questa chiamata viene risolta dall’interno verso l’esterno, e diventa articolata da leggere e da comporre soprattutto quando ciascuna delle funzioni utilizzate ha diversi argomenti in input. La stessa chiamata utilizzando il pipe può essere invece scritta direttamente seguendo il flusso di esecuzione del codice, ovvero:

input |> f1() |> f2() |> f3() |> f4() |> f5()

consentendo così anche la scrittura del codice seguendo il flusso dell’algoritmo risolutivo del particolare problema che si sta affrontando, che parte in questo esempio dall’utilizzo della funzione f1() fino ad arrivare alla funzione f5().

janitor: un package per la fase di pulizia ed esplorazione dei dati

Il package janitor è un package di servizio che contiene alcune funzioni che si rivelano molto utili nella fase di pulizia ed esplorazione dei dati. Il package contiene diverse funzioni:

library(janitor)
ls("package:janitor")
 [1] "%>%"                   "add_totals_col"        "add_totals_row"       
 [4] "adorn_crosstab"        "adorn_ns"              "adorn_pct_formatting" 
 [7] "adorn_percentages"     "adorn_rounding"        "adorn_title"          
[10] "adorn_totals"          "as_tabyl"              "chisq.test"           
[13] "clean_names"           "compare_df_cols"       "compare_df_cols_same" 
[16] "convert_to_date"       "convert_to_datetime"   "convert_to_NA"        
[19] "crosstab"              "describe_class"        "excel_numeric_to_date"
[22] "find_header"           "fisher.test"           "get_dupes"            
[25] "get_one_to_one"        "make_clean_names"      "remove_constant"      
[28] "remove_empty"          "remove_empty_cols"     "remove_empty_rows"    
[31] "round_half_up"         "round_to_fraction"     "row_to_names"         
[34] "sas_numeric_to_date"   "signif_half_up"        "single_value"         
[37] "tabyl"                 "top_levels"            "untabyl"              
[40] "use_first_valid_of"   

anche se in questa pagina facciamo riferimento solo alle funzioni clean_names, per uniformare i nomi delle colonne di una tabella, e tabyl, per tabulare i dati.

tabyl: una funzione avanzata per il conteggio

L’operazione di conteggio è la più semplice e comune operazione in qualunque analisi di dati. La funzione di base per il conteggio, table(), lascia però molto a desiderare per i seguenti aspetti:

  • non accetta in input oggetti di tipo data.frame o tibble, per cui diventa complesso sfruttare la sintesi basata sugli operatori pipe (|> oppure %>%)
  • non restituisce in output oggetti di tipo data.frame ma una classe particolare di oggetti table
  • i risultati in output sono difficili da formattare, questo ancor di più se si comparano con le opzioni di formattazione di una tabella pivot di Microsoft Excel.

tabyl() è un approccio per la tabulazione di variabili che cerca di rispondere a questi limiti. E’ contenuto nel package janitor: l’operazione di conteggio è una parte fondamentale della fase di pulizia ed esplorazione dei dati.

tabyl() è una funzione che risponde ai principi di tidyverse (prende una tabella in input e restituisce una tabella in output) ed è basata sui package dplyr e tidyr.

Dettagli tecnici

tabyl() produce apparentemente tabelle di frequenza ad 1, 2 o 3 variabili. In realtà dietro le quinte tabyl() inserisce una copia di questi conteggi come attributo al data.frame risultante. In altre parole, il risultato di tabyl sembra un data.frame di conteggi, ma è un oggetto di tipo tabyl che contiene queste informazioni come metadati, e questo consente di sfruttare le funzioni di tipo adorn_ (ornamento) per inserire informazioni aggiuntive ed ottenere una migliore formattazione della tabella di output.

Sebbene le funzioni adorn_ siano state progettate per lavorare su oggetti di tipo tabyl, sono disponibili anche versioni che lavorano allo stesso modo su oggetti di tipo data.frame.

Alcuni esempi

Per mostrare tabyl (e clean_names) in azione sfruttiamo la tabella titanic disponibile in formato .xlsx su questo link

# carico i package utilizzati ---------------------------------------------
library(readxl)      # per importare il file MS-Excel
library(tidyverse)   # per i verbi di pulizia e manipolazione dei dati {dplyr}
library(janitor)     # per sfruttare clean_names e tabyl

# carico e pulisco i dati -------------------------------------------------
# importo il foglio titanic dalla cartella MS-Excel
titanic <- read_excel(paste0(here::here(), "/data/", "titanic-excel.xlsx"), sheet = "titanic")
# nomi delle colonne (variabili) della tabella dati
names(titanic)
 [1] "PassengerId" "Survived"    "Pclass"      "Name"        "Sex"        
 [6] "Age"         "SibSp"       "Parch"       "Ticket"      "Fare"       
[11] "Cabin"       "Embarked"   
# pulisco i nomi delle colonne
titanic <- titanic |> clean_names()
# nomi delle colonne dopo averli uniformati
names(titanic)
 [1] "passenger_id" "survived"     "pclass"       "name"         "sex"         
 [6] "age"          "sib_sp"       "parch"        "ticket"       "fare"        
[11] "cabin"        "embarked"    
# ricodifico i dati usando descrittori testuali e rinomino la colonna pclass
titanic <- titanic |> 
  mutate(survived = recode(survived, "1" = "Sì", "0 " = "No"),
         pclass = recode(pclass, 
                         "1" = "prima", "2" = "seconda", "3" = "terza"),
         embarked = recode(embarked, 
                           "S" = "Southampthon", "Q" = "Queentown", 
                           "C" = "Cherbourg")) |> 
  rename(class = pclass)

Tabelle di frequenza univariata (one-way tabyl)

La tabulazione di una singola variabile è l’operazione più semplice che si può effettuare con la funzione tabyl:

# tabella di frequenza della variabile class
tabyl(titanic, class)
   class   n   percent
   prima 216 0.2424242
 seconda 184 0.2065095
   terza 491 0.5510662
# assegno la tabella di frequenza ad un oggetto sul workspace
t1 <- titanic |> tabyl(class)
t1
   class   n   percent
   prima 216 0.2424242
 seconda 184 0.2065095
   terza 491 0.5510662
# si tratta di un particolare data.frame
class(t1)
[1] "tabyl"      "data.frame"
# su cui sono definiti alcuni attributi utili alla successiva manipolazione
attributes(t1)
$names
[1] "class"   "n"       "percent"

$class
[1] "tabyl"      "data.frame"

$row.names
[1] 1 2 3

$core
    class   n   percent
1   prima 216 0.2424242
2 seconda 184 0.2065095
3   terza 491 0.5510662

$tabyl_type
[1] "one_way"

In caso di valori NA, tabyl() visualizza anche le percentuali “valide”, vale a dire le percentuali calcolate sui solo valori validi. Come accennato in precedenza, anche se tabyl() è progettato in maniera da considerare in input una tabella data (data.frame o tibble) e i corrispondenti nomi delle colonne da tabulare, è possibile anche passare come input un vettore per ottenere la corrispondenza tabella di frequenza univariata:

# creo un semplice vettore con un valore mancante
x <- c("basso", "basso", "alto", "alto", "alto", NA)
# e ne calcolo la tabella di frequenza
tabyl(x)
     x n   percent valid_percent
  alto 3 0.5000000           0.6
 basso 2 0.3333333           0.4
  <NA> 1 0.1666667            NA
# tabella di frequenza scegliendo di non visualizzare i valori mancanti
# che comunque non sono considerati nel calcolo delle percentuali
tabyl(x, show_na = FALSE)
     x n percent
  alto 3     0.6
 basso 2     0.4

Le funzioni ausiliare adorn_ sono progettate per tabyl a due vie, ma alcune possono essere utilizzate anche su tabelle univariate, ed in particolare adorn_totals() e adorn_pct_formatting():

# aggiungo una riga con i totali
t1 |> adorn_totals()
   class   n   percent
   prima 216 0.2424242
 seconda 184 0.2065095
   terza 491 0.5510662
   Total 891 1.0000000
# l'argomento per gestire come calcolare i totali è where
# per default calcola i totali di colonna (aggiunge cioè una riga)
t1 |> adorn_totals(where = "row")
   class   n   percent
   prima 216 0.2424242
 seconda 184 0.2065095
   terza 491 0.5510662
   Total 891 1.0000000
# oltre ai totali formatto adeguatamente la colonna delle percentuali
t1 |> 
  adorn_totals(where = "row") |> 
  adorn_pct_formatting()
   class   n percent
   prima 216   24.2%
 seconda 184   20.7%
   terza 491   55.1%
   Total 891  100.0%

Tabelle di frequenza bivariate (two-way tabyl)

Le tabelle a doppia entrata, dette anche crosstab o tabelle di contingenza, si ottengono passando a tabyl un data.frame e le due colonne su cui effettuare la tabulazione:

# calcolo una tabella a doppia entrata
t2 <- titanic %>%
  tabyl(class, survived)
# e la stampo a video
t2
   class  No  Sì
   prima  80 136
 seconda  97  87
   terza 372 119
Dettaglio avanzato

Si ottiene lo stesso risultato combinando le funzioni dplyr::count() e tidyr::pivot_wider().

Per dettagli vedi le lezioni successive.

Poichè si tratta di un oggetto di tipo tabyl è possibile sfruttare le funzioni ausiliarie per arricchirlo in termini informativi:

# se utilizzo il valore di default della funzione adorn_totals 
# viene aggiunta una nuova riga con i totali di colonna
t2 |> adorn_totals()
   class  No  Sì
   prima  80 136
 seconda  97  87
   terza 372 119
   Total 549 342
# posso aggiungere una nuova colonna con i totali di riga
t2 |> adorn_totals(where = "col")
   class  No  Sì Total
   prima  80 136   216
 seconda  97  87   184
   terza 372 119   491
# così come posso aggiungere i totali su entrambe le dimensioni
t2 |> adorn_totals(where = c("row", "col"))
   class  No  Sì Total
   prima  80 136   216
 seconda  97  87   184
   terza 372 119   491
   Total 549 342   891
# posso calcolare le frequenze percentuali in base al gran totale
t2 |> 
  adorn_totals(where = c("row", "col")) |> 
  adorn_percentages(denominator = "all")
   class         No        Sì     Total
   prima 0.08978676 0.1526375 0.2424242
 seconda 0.10886644 0.0976431 0.2065095
   terza 0.41750842 0.1335578 0.5510662
   Total 0.61616162 0.3838384 1.0000000
# posso calcolare le percentuali utilizzando come denominatori i totali di 
# riga: distribuzioni percentuali della variabile in colonna condizionate
# sui vari livelli della variabile in riga (profili riga)
t2 |> 
  adorn_totals(where = c("row", "col")) |> 
  adorn_percentages(denominator = "row")
   class        No        Sì Total
   prima 0.3703704 0.6296296     1
 seconda 0.5271739 0.4728261     1
   terza 0.7576375 0.2423625     1
   Total 0.6161616 0.3838384     1
# oppure le percentuali utilizzando come denominatori i totali di colonna:
# distribuzioni percentuali della variabile in riga condizionate
# sui vari livelli della variabile in colonna (profili colonna)
t2 |> 
  adorn_totals(where = c("row", "col")) |> 
  adorn_percentages(denominator = "col")
   class        No        Sì     Total
   prima 0.1457195 0.3976608 0.2424242
 seconda 0.1766849 0.2543860 0.2065095
   terza 0.6775956 0.3479532 0.5510662
   Total 1.0000000 1.0000000 1.0000000
# una volta colcolate le frequenze percentuali di interesse posso procedere
# alla formattazione
t2 |> 
  adorn_totals(where = c("row", "col")) |> 
  adorn_percentages(denominator = "row") |> 
  adorn_pct_formatting(digits = 2)
   class     No     Sì   Total
   prima 37.04% 62.96% 100.00%
 seconda 52.72% 47.28% 100.00%
   terza 75.76% 24.24% 100.00%
   Total 61.62% 38.38% 100.00%
# e posso arricchire la tabella aggiungendo in ogni cella anche le
# le corrispondenti frequenze assolute
t2 |> 
  adorn_totals(where = c("row", "col")) |> 
  adorn_percentages("row") |> 
  adorn_pct_formatting(digits = 2) |> 
  adorn_ns()
   class           No           Sì         Total
   prima 37.04%  (80) 62.96% (136) 100.00% (216)
 seconda 52.72%  (97) 47.28%  (87) 100.00% (184)
   terza 75.76% (372) 24.24% (119) 100.00% (491)
   Total 61.62% (549) 38.38% (342) 100.00% (891)

Le funzioni di ornamento hanno una serie di opzioni che permettono di controllare gli assi, le funzioni di arrotondamento ed altre scelte relative alla formattazione (vedi sotto per dettagli in merito).

Tabelle di frequenza trivariate (lista di two-way tabyl)

Così come la funzione table() lavora su t La funzione tabyl(), allo stesso modo della funzione table(), può lavorare anche con una terza variabile in input. In questo caso tabyl() restituisce una lista di tabyl:

# un esempio di tabella a tre vie
t3 <- titanic |> tabyl(class, survived, sex)
# il risultato è una tabella doppia divisa in una lista sui 
# livelli della terza variabile
t3 
$female
   class No Sì
   prima  3 91
 seconda  6 70
   terza 72 72

$male
   class  No Sì
   prima  77 45
 seconda  91 17
   terza 300 47
# si tratta di un oggetto di tipo lista
class(t3)
[1] "list"
# i cui elementi sono due tabyl bivariati
class(t3$female)
[1] "tabyl"      "data.frame"
class(t3$male)
[1] "tabyl"      "data.frame"

Nel caso in cui le funzioni ausiliarie adorn_ sono chiamate su una lista di data.frame, come nel caso appunto di una tabyl a tre vie, sfruttando la funzione purrr::map() lavorano automaticamente come se fossero applicate (mapping) a ciacun data.frame che compone la lista:

t3 |>
  adorn_totals("row") |> 
  adorn_percentages("all") |> 
  adorn_pct_formatting(digits = 1) |> 
  adorn_ns() |> 
  adorn_title()
$female
           survived            
   class         No          Sì
   prima  1.0%  (3) 29.0%  (91)
 seconda  1.9%  (6) 22.3%  (70)
   terza 22.9% (72) 22.9%  (72)
   Total 25.8% (81) 74.2% (233)

$male
            survived            
   class          No          Sì
   prima 13.3%  (77)  7.8%  (45)
 seconda 15.8%  (91)  2.9%  (17)
   terza 52.0% (300)  8.1%  (47)
   Total 81.1% (468) 18.9% (109)

Questa “mappatura” (mapping) automatica permette all’utente di riutilizzare il codice relativo alle funzioni di ornamento anche se cambia le variabili di cui richiede la tabulazione. Se ad esempio effettua la tabulazione di tre variabili tab_dati |> tabyl(var1, var2, var3) e a questa tabella aggiunge le chiamate delle funzioni adorn_, se modifica le variabili in tabulazione cambiando la parte iniziale del comando, ad esempio tab_dati |> tabyl(var1, var2, var4), non deve riscrivere le successive chiamate delle funzioni adorn_, che sfruttando la funzione map() si adattano automaticamente alla nuova tabella.

Nota tecnica (avanzata)

E’ possibile sfruttare anche la sintassi esplicita richiamando map() sulla lista di data.frame, map(t3, adorn_percentages), oppure utilizzare la funzione di base lapply(), lapply(t3, adorn_percentages).

Per dettagli sulle funzioni lapply e map() e sul package purr() si rimanda ad una lezione successiva.

Ulteriori dettagli tecnici su tabyl

  • Se utilizzato su una variabile di tipo factor, tabyl mostra tra i risultati anche i “missing levels” (livelli non presenti nei dati). Questa opzione di default può però essere cambiata:
# creo una variabile factor con tre livelli ma con soli due valori presenti
var_factor <- factor(c("alto", "basso", "alto", "basso", "alto"), 
                     levels = c("basso", "medio", "alto"))
# per default nella tabella in output vengono mostrati tutti i livelli del factor
var_factor |> tabyl()
 var_factor n percent
      basso 2     0.4
      medio 0     0.0
       alto 3     0.6
# posso però nascondere i livelli non presenti nei dati
var_factor |> tabyl(show_missing_levels = FALSE)
 var_factor n percent
      basso 2     0.4
       alto 3     0.6
  • lo stesso discorso vale per gli NA, che possono essere gestisti sfruttando l’argomento show_na
  • la funzione di stampa per tabyls non visualizza i numeri di riga
  • è possibile sfruttare le funzioni chisq.test() e fisher.test() su un oggetto tabyl a due vie per eseguire i corrispondenti test statistici, così come per un tradizionale oggetto table()

Le funzioni adorn_*

These modular functions build on a tabyl to approximate the functionality of a PivotTable in Microsoft Excel. They print elegant results for interactive analysis or for sharing in a report, e.g., with knitr::kable(). For example:

Le funzioni di ornamento sono funzioni modulari che sono progettate per ottenere funzionalità simili a quelle di una tabella pivot in Microsoft Excel. E’ naturalmente possibile sfruttare le funzioni di formattazione di una tabella per stamparla in un report in formato conveniente. Ecco un esempio sfruttando la funzione knitr::kable():

t2 |> 
  adorn_totals(c("row", "col")) |> 
  adorn_percentages("row") |> 
  adorn_pct_formatting(rounding = "half up", digits = 0) |> 
  adorn_ns() |> 
  adorn_title("combined") |> 
  knitr::kable()
class/survived No Total
prima 37% (80) 63% (136) 100% (216)
seconda 53% (97) 47% (87) 100% (184)
terza 76% (372) 24% (119) 100% (491)
Total 62% (549) 38% (342) 100% (891)

Le funzioni adorn_ (ornamento) disponibili sono:

  • adorn_totals(): per aggiungere totali di riga, colonna o su entrambe le dimensioni
  • adorn_percentages(): per calcolare le percentuali lungo ciascuna dimensione o sull’intera tabella (gran totale)
  • adorn_pct_formatting(): per formattare le colonne contenenti le percentuali, controllando sia il numero di cifre decimali che la posizione del simbolo %
  • adorn_rounding(): per arrotondare un data.frame di numeri (di solito il risultato di adorn_percentages), utilizzando la funzione base R round() o utilizzando round_half_up() di janitor per arrotondare per eccesso tutti i valori con decimale 0.5, così come succede in MS-Excel
    • round(10.5) in R base restituisce 10, mentre round_half_up(10.5) restituisce 11.
    • adorn_rounding() restituisce colonne di classe numeric, che sono quindi utilizzabili ai fini di rappresentazioni grafiche e successive manipolazioni. Si tratta di un’alternativa meno aggressiva della funzione adorn_pct_formatting(). Queste due funzioni non dovrebbero pertanto essere utilizate insieme in una pipeline di codice.
  • adorn_ns(): per aggiungere i conteggi (frequenze assolute) ad una tabella. Questi possono essere ricavati dai conteggi sottostanti all’oggetto tabyl, che sono allegati al tabyl come metadati, oppure possono essere forniti in input dall’utente.
  • adorn_title(): per aggiungere un titolo ad un tabyl (o ad un altro tipo di data.frame). Le opzioni disponibili includono l’inserimento del nome di colonna in una nuova riga in testa al data.frame o combinando i nomi di riga e colonna nella prima cella del data.frame.

NOTA: queste funzioni di ornamento dovrebbero essere chiamate in ordine logico (ad esempio aggiungendo prima i totali e poi calcolando le percentuali) per ottenere risultati coerenti.