In altre parti del percorso abbiamo visto vari tipi di dati con caratteristiche diverse.
In questo concetto vedremo come memorizzare dati multidimensionali ed eterogenei. In pratica, la maggior parte dei dati del mondo reale è così: adesso arriviamo al cuore di come R viene usato (per lo più) nella pratica.
Nel corso dei decenni, R ha aggiunto diversi tipi di dati per gestire dati tabellari.
Questo percorso si concentrerà principalmente sui tibble, ma è utile conoscere alcune alternative.
data.table
I tibble (descritti più avanti) e i data.table sono entrambi tentativi apprezzati di migliorare il tradizionale data.frame del Base R.
C'è inevitabilmente molta discussione su quale sia «il migliore».
Forse c'è un certo grado di consenso sui punti seguenti (anche se saranno criticati come semplicistici).
tibble è ottimizzato soprattutto per la facilità d'uso e per l'integrazione con l'ecosistema Tidyverse.data.table è ottimizzato soprattutto per la potenza grezza e la scalabilità, specialmente quando si lavora con dataset molto grandi.In ogni caso, data.table non è disponibile su Exercism, quindi lo citiamo qui solo per completezza.
data.frame
Nel Base R, un data.frame è una list di vectors di uguale lunghezza.
Lo si può immaginare come una tabella rettangolare di dati, in cui ogni colonna è omogenea, ma ogni riga può contenere (e di solito contiene) tipi di dati diversi.
Un esempio per illustrarlo:
# create the column vectors
languages <- c("Fortran", "R", "Python", "Julia")
created <- c(1957, 1993, 1991, 2012)
has.syllabus <- c(FALSE, TRUE, TRUE, TRUE)
# join columns to create the dataframe
df <- data.frame(languages, created, has.syllabus)
df
#> languages created has.syllabus
#> 1 Fortran 1957 FALSE
#> 2 R 1993 TRUE
#> 3 Python 1991 TRUE
#> 4 Julia 2012 TRUE
# look at the structure
str(df)
#> 'data.frame': 4 obs. of 3 variables:
#> $ languages : chr "Fortran" "R" "Python" "Julia"
#> $ created : num 1957 1993 1991 2012
#> $ has.syllabus: logi FALSE TRUE TRUE TRUE
Abbiamo una colonna di stringhe, una colonna di numeri e una colonna di valori booleani. Su scala più grande, questo è un modo intuitivo per rappresentare molte raccolte di dati del mondo reale.
tibble
Il design del data.frame è vecchio.
Decenni di esperienza, più il cambiamento delle modalità d'uso di R, hanno portato a una riprogettazione, creando un'alternativa modernizzata nel Tidyverse: i tibble.
Rispetto al Base R, i tibble hanno:
In breve, un tibble punta a «fare di meno e lamentarsi di più», descritto anche come «pigro e scontroso».
I tipi sono di solito intercambiabili: qualsiasi funzione che accetta un data.frame accetterà anche un tibble, e vice versa.
Le conversioni tra i tipi sono facili, con as_tibble(df) e as.data.frame(tbl).
Per il lavoro nuovo, usare i tibble probabilmente ti aiuterà a scrivere codice più robusto.
Tuttavia, nel mondo di R il codice e i dati di vecchia data sono molto abbondanti, quindi è probabile che il data.frame resti comune a lungo.
# column vectors are same as for data.frame
library(tibble)
tbl <- tibble(languages, created, has.syllabus)
tbl
# A tibble: 4 × 3
#> languages created has.syllabus
#> <chr> <dbl> <lgl>
#> 1 Fortran 1957 FALSE
#> 2 R 1993 TRUE
#> 3 Python 1991 TRUE
#> 4 Julia 2012 TRUE
str(tbl)
#> tibble [4 × 3] (S3: tbl_df/tbl/data.frame)
#> $ languages : chr [1:4] "Fortran" "R" "Python" "Julia"
#> $ created : num [1:4] 1957 1993 1991 2012
#> $ has.syllabus: logi [1:4] FALSE TRUE TRUE TRUE
Nota il formato di stampa predefinito: la riga di commento con le dimensioni viene stampata automaticamente e vengono mostrati anche i tipi delle colonne.
I tibble sono una parte fondamentale del Tidyverse, quindi aggiungili con library(tibble) oppure library(tidyverse).
La documentazione è piuttosto ampia, nello stile del Tidyverse:
Nel modo più semplice, possiamo usare la funzione tibble() per unire vettori colonna, come per data.frame().
Un esempio è stato mostrato in una sezione precedente.
Se è più comodo inserire i valori per riga, la funzione corrispondente è tribble().
tbl_r <- tribble(
# column names marked with tilde prefix
~languages, ~created, ~has.syllabus,
"Fortran", 1957, FALSE,
"R", 1993, TRUE,
"Python", 1991, TRUE,
"Julia", 2012, TRUE
)
tbl_r
# A tibble: 4 × 3
#> languages created has.syllabus
#> <chr> <dbl> <lgl>
#> 1 Fortran 1957 FALSE
#> 2 R 1993 TRUE
#> 3 Python 1991 TRUE
#> 4 Julia 2012 TRUE
In pratica, ci sono decine di modi per creare tibble, perché sono il formato di output predefinito di un'ampia gamma di funzioni del Tidyverse. Ci torneremo in un concetto futuro.
Il concetto di Programmazione funzionale ha presentato la libreria purrr per manipolare vettori e liste (strutture di dati monodimensionali).
Per i dataframe (tradizionali o tibble), la libreria corrispondente da usare è dplyr.
Abbiamo già introdotto dplyr, nel concetto Switch.
Lì abbiamo usato solo alcune funzioni di utilità, ma ora possiamo iniziare a esplorare il resto di questa grande libreria.
I dataframe, tibble inclusi, possono essere trattati come liste di vettori colonna, quindi l'indicizzazione di una lista estrae una colonna specifica.
tbl
# A tibble: 4 × 3
#> languages created has.syllabus
#> <chr> <dbl> <lgl>
#> 1 Fortran 1957 FALSE
#> 2 R 1993 TRUE
#> 3 Python 1991 TRUE
#> 4 Julia 2012 TRUE
tbl$created
#> [1] 1957 1993 1991 2012
Un dataframe può anche essere indicizzato con un'indicizzazione in stile matrice.
tbl[c(2, 4), 1:2]
# A tibble: 2 × 2
#> languages created
#> <chr> <dbl>
#> 1 R 1993
#> 2 Julia 2012
Nel R moderno con l'ecosistema Tidyverse, le funzioni di dplyr sono in genere più flessibili e comode, e saranno al centro del resto di questo concetto.
Per ottenere una singola colonna, usa pull() con il nome o il numero sequenziale (i numeri negativi contano da destra verso sinistra).
tbl |> pull(created)
#> [1] 1957 1993 1991 2012
È lo stesso risultato di tbl$created, ma ottenuto con una funzione che si presta all'uso in una pipeline.
Per ottenere più colonne, la funzione appropriata è select(), che è estremamente versatile.
Ottieni (o escludi) colonne in base alle proprietà del loro nome o del loro tipo.
# Range with position and/or name
tbl |> select(1:created)
# A tibble: 4 × 2
#> languages created
#> <chr> <dbl>
#> 1 Fortran 1957
#> 2 R 1993
#> 3 Python 1991
#> 4 Julia 2012
# Exclude a column
tbl |> select(!created)
# A tibble: 4 × 2
#> languages has.syllabus
#> <chr> <lgl>
#> 1 Fortran FALSE
#> 2 R TRUE
#> 3 Python TRUE
#> 4 Julia TRUE
# Use type of column
tbl |> select(where(is.numeric))
# A tibble: 4 × 1
#> created
#> <dbl>
#> 1 1957
#> 2 1993
#> 3 1991
#> 4 2012
Sono ammessi più criteri, usando gli operatori booleani &, | e ! (e, o, non).
I nomi di colonna che sono identificatori R validi non hanno bisogno di virgolette dentro una select().
I nomi non validi (ad esempio quelli che contengono spazi) possono essere racchiusi tra backtick, anche se rinominarli potrebbe essere meglio.
La funzione select() può lavorare con una serie di funzioni ausiliarie per scegliere i nomi delle colonne: starts_with, contains, num_range e varie altre.
matches consente la corrispondenza completa con RegEx.
Consulta la documentazione per i dettagli.
Tutta questa potenza sembra piuttosto eccessiva con il nostro dataframe giocattolo di linguaggi.
Per fortuna, il tibble starwars è incluso in dplyr e ci dà qualcosa di più grande con cui esercitarci.
# limit display to top 3 rows of non-list columns
starwars |>
select(!where(is.list)) |>
head(3)
# A tibble: 3 × 11
#> name height mass hair_color skin_color eye_color birth_year sex gender homeworld species
#> <chr> <int> <dbl> <chr> <chr> <chr> <dbl> <chr> <chr> <chr> <chr>
#> 1 Luke Skywalker 172 77 blond fair blue 19 male masculine Tatooine Human
#> 2 C-3PO 167 75 NA gold yellow 112 none masculine Tatooine Droid
#> 3 R2-D2 96 32 NA white, blue red 33 none masculine Naboo Droid
# pick a subset of columns
starwars |>
select(name | ends_with("color")) |>
head(5)
# A tibble: 5 × 4
#> name hair_color skin_color eye_color
#> <chr> <chr> <chr> <chr>
#> 1 Luke Skywalker blond fair blue
#> 2 C-3PO NA gold yellow
#> 3 R2-D2 NA white, blue red
#> 4 Darth Vader none white yellow
#> 5 Leia Organa brown light brown
Chiaramente, dplyr offre modi potenti per selezionare le colonne per nome.
Possiamo fare qualcosa di simile con i nomi delle righe?
No!
I dataframe tradizionali di R possono avere nomi di riga, ma (dopo una storia di bug e problemi di prestazioni) i nomi di riga non sono ammessi nei tibbles.
Se vuoi dei nomi, mettili in una colonna <chr> (di solito la colonna 1), usata come qualsiasi altra colonna.
Le funzioni di importazione come as_tibble() la creeranno automaticamente quando importi dati con righe con nome.
Se questa limitazione sui nomi di riga sembra stranamente restrittiva, ricorda che la maggior parte dei grandi sistemi di database gestisce le tabelle allo stesso modo: Oracle, SQL Server, PostgreSQL, MySQL...
Ottieni le righe che soddisfano certi criteri con filter(), oppure escludile con filter_out().
starwars |>
select(name:mass) |>
filter(between(height, 150, 165) & !is.na(mass))
# A tibble: 4 × 3
#> name height mass
#> <chr> <int> <dbl>
#> 1 Leia Organa 150 49
#> 2 Beru Whitesun Lars 165 75
#> 3 Nien Nunb 160 68
#> 4 Ben Quadinaros 163 65
I criteri di filtro possono essere arbitrariamente complessi, ma sono sempre basati sul contenuto delle righe.
Se si conoscono i numeri delle righe, possiamo usare una varietà di funzioni slice().
starwars |>
select(name | homeworld) |>
slice(20:25)
# A tibble: 6 × 2
#> name homeworld
#> <chr> <chr>
#> 1 Palpatine Naboo
#> 2 Boba Fett Kamino
#> 3 IG-88 NA
#> 4 Bossk Trandosha
#> 5 Lando Calrissian Socorro
#> 6 Lobot Bespin
# random sample of rows
starwars |>
select(name | homeworld) |>
slice_sample(n = 4)
# A tibble: 4 × 2
#> name homeworld
#> <chr> <chr>
#> 1 Shaak Ti Shili
#> 2 Luminara Unduli Mirial
#> 3 Grievous Kalee
#> 4 Palpatine Naboo
Per rimuovere le righe duplicate, usa distinct().
Prima avvertenza: il comportamento predefinito copy-on-modify fa sì che il tibble originale di solito rimanga invariato.
La maggior parte delle modifiche viene applicata per colonna.
I nomi delle colonne possono essere cambiati con rename(newname = oldname), oppure con rename_with() per applicare una funzione.
Un uso tipico è ripulire i nomi importati per renderli più facili da usare in R, rimuovendo gli spazi e imponendo un formato coerente per i nomi correlati.
Nota la sintassi dentro rename().
Il contenuto della colonna oldname viene associato al nome newname, da cui l'ordine.
L'ordine delle colonne può essere cambiato con relocate().
Le colonne specificate vengono spostate di default nelle posizioni più a sinistra, ma si può usare un argomento .before o .after per un posizionamento più preciso.
sw <- starwars |> select(name:species) |> slice(1:4)
sw
# A tibble: 4 × 11
#> name height mass hair_color skin_color eye_color birth_year sex gender homeworld species
#> <chr> <int> <dbl> <chr> <chr> <chr> <dbl> <chr> <chr> <chr> <chr>
#> 1 Luke Skywalker 172 77 blond fair blue 19 male masculine Tatooine Human
#> 2 C-3PO 167 75 NA gold yellow 112 none masculine Tatooine Droid
#> 3 R2-D2 96 32 NA white, blue red 33 none masculine Naboo Droid
#> 4 Darth Vader 202 136 none white yellow 41.9 male masculine Tatooine Human
sw |> relocate(c(species, homeworld), .after = name)
# A tibble: 4 × 11
#> name species homeworld height mass hair_color skin_color eye_color birth_year sex gender
#> <chr> <chr> <chr> <int> <dbl> <chr> <chr> <chr> <dbl> <chr> <chr>
#> 1 Luke Skywalker Human Tatooine 172 77 blond fair blue 19 male masculine
#> 2 C-3PO Droid Tatooine 167 75 NA gold yellow 112 none masculine
#> 3 R2-D2 Droid Naboo 96 32 NA white, blue red 33 none masculine
#> 4 Darth Vader Human Tatooine 202 136 none white yellow 41.9 male masculine
Per modifiche più grandi, mutate() ti permette di:
NULL
Chiaramente, mutate() è potente, potenzialmente confondente, e un motivo per essere molto grati al copy-on-modify.
Non c'è un motivo evidente per interessarsi all'indice di massa corporea dei personaggi di Star Wars, ma non si sa mai:
starwars |>
select(c(name, species, height, mass)) |>
mutate(BMI = mass / (height / 100)^2) |>
head(4)
# A tibble: 4 × 5
#> name species height mass BMI
#> <chr> <chr> <int> <dbl> <dbl>
#> 1 Luke Skywalker Human 172 77 26.0
#> 2 C-3PO Droid 167 75 26.9
#> 3 R2-D2 Droid 96 32 34.7
#> 4 Darth Vader Human 202 136 33.3
Quando vuoi operare su un sottoinsieme delle colonne con funzioni come mutate(), la sequenza select() |> mutate() dell'esempio precedente è una possibilità.
Nel risultato ci saranno solo le colonne selezionate.
In alternativa, può essere comodo usare pick() dentro la chiamata a mutate():
starwars |> mutate(pick(c(name, species, height, mass)), BMI = mass / (height / 100)^2) |> head(4)
When you want to operate on a subset of the columns with functions such as `mutate()`, the `select() |> mutate()` sequence in the above example is one option.
Only the selected columns will be in the result.
Alternatively, it can be convenient to use `pick()` _within_ the `mutate()` call:
```R
starwars |> mutate(pick(c(name, species, height, mass)), BMI = mass / (height / 100)^2) |> head(4)
# A tibble: 4 × 15
#> name height mass hair_color skin_color eye_color birth_year sex gender homeworld species films vehicles
#> <chr> <int> <dbl> <chr> <chr> <chr> <dbl> <chr> <chr> <chr> <chr> <lis> <list>
#> 1 Luke Skywal… 172 77 blond fair blue 19 male mascu… Tatooine Human <chr> <chr>
#> 2 C-3PO 167 75 NA gold yellow 112 none mascu… Tatooine Droid <chr> <chr>
#> 3 R2-D2 96 32 NA white, bl… red 33 none mascu… Naboo Droid <chr> <chr>
#> 4 Darth Vader 202 136 none white yellow 41.9 male mascu… Tatooine Human <chr> <chr>
# ℹ 2 more variables: starships <list>, BMI <dbl>
Nella mutazione vengono usate solo le colonne passate a pick, ma vengono restituite tutte le colonne.
Le operazioni per riga sono meno comuni quando si modifica un singolo tibble (l'unione di più tibble sarà discussa in un concetto successivo).
Un'eccezione: arrange() ordina le righe in base ai valori di una o più colonne.
tbl
# A tibble: 4 × 3
#> languages created has.syllabus
#> <chr> <dbl> <lgl>
#> 1 Fortran 1957 FALSE
#> 2 R 1993 TRUE
#> 3 Python 1991 TRUE
#> 4 Julia 2012 TRUE
tbl |> arrange(languages)
# A tibble: 4 × 3
#> languages created has.syllabus
#> <chr> <dbl> <lgl>
#> 1 Fortran 1957 FALSE
#> 2 Julia 2012 TRUE
#> 3 Python 1991 TRUE
#> 4 R 1993 TRUE
I dataframe, tradizionali o tibble, sono centrali nel modo in cui il R moderno viene tipicamente usato.
La maggior parte delle funzioni del Tidyverse (non solo dplyr) accetta tibble in input e/o li crea come output.
Questo concetto ha fornito solo una breve introduzione, scalfendo appena la superficie di ciò che è possibile.
Concetti successivi tratteranno diversi altri aspetti dei dataframe (entro i vincoli tecnici di Exercism).