Noutras partes do programa, já vimos vários tipos de dados com características diferentes.
Neste conceito, vamos ver formas de armazenar dados multidimensionais e heterogéneos. Na prática, a maior parte dos dados do mundo real é assim, por isso estamos agora a chegar ao cerne de como o R é (sobretudo) usado na prática.
Ao longo das décadas, o R foi acrescentando vários tipos de dados para lidar com dados tabulares.
Este programa vai focar-se sobretudo nos tibbles, mas é útil conhecer algumas alternativas.
data.table
Os tibbles (descritos abaixo) e os data.tables são duas tentativas muito respeitadas de melhorar o tradicional data.frame do R base.
Há inevitavelmente muita discussão sobre qual deles é "melhor".
Talvez haja algum consenso em torno dos seguintes pontos (mesmo que sejam criticados por serem simplistas).
tibble está otimizado sobretudo para a facilidade de utilização e para a integração com o ecossistema Tidyverse.data.table está otimizado sobretudo para a potência bruta e a escalabilidade, especialmente ao trabalhar com conjuntos de dados muito grandes.De qualquer forma, o data.table não está disponível no Exercism, por isso é mencionado aqui apenas por uma questão de completude.
data.frame
No R base, um data.frame é uma list de vectors com o mesmo comprimento.
Podemos pensar nisto como uma tabela retangular de dados, em que cada coluna é homogénea, mas cada linha pode conter (e normalmente contém) tipos de dados diferentes.
Um exemplo para ilustrar isto:
# 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
Temos uma coluna de strings de carateres, uma coluna de números e uma coluna de valores Boolean. Quando aumentarmos a escala, esta é uma forma intuitiva de representar muitos conjuntos de dados do mundo real.
tibble
O modelo do data.frame é antigo.
Décadas de experiência, a par da mudança nos padrões de utilização do R, levaram a uma reformulação que criou uma alternativa modernizada no Tidyverse: os tibbles.
Comparados com o R base, os tibbles têm:
Em suma, um tibble procura "fazer menos e queixar-se mais", também descrito como "preguiçoso e mal-humorado".
Os tipos são geralmente intercambiáveis: qualquer função que aceite um data.frame também aceita um tibble, e vice-versa.
As conversões entre os tipos são fáceis, com as_tibble(df) e as.data.frame(tbl).
Para trabalho novo, usar tibbles provavelmente ajuda-te a escrever código mais robusto.
No entanto, há imenso código e dados legados no mundo do R, por isso é provável que o data.frame continue a ser comum durante muito tempo.
# 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
Repara no formato de impressão predefinido: a linha de comentário com as dimensões é impressa automaticamente e os tipos das colunas também são apresentados.
Os tibbles são uma parte central do Tidyverse, por isso carrega-os com library(tibble) ou library(tidyverse).
A documentação é bastante extensa, ao estilo do Tidyverse:
Da forma mais simples, podemos usar a função tibble() para juntar vetores coluna, tal como em data.frame().
Um exemplo disto foi mostrado numa secção anterior.
Se for mais conveniente introduzir os valores linha a linha, a função correspondente é 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
Na prática, há dezenas de formas de criar tibbles, uma vez que são o formato de saída predefinido de uma grande variedade de funções do Tidyverse. Voltaremos a isto num conceito futuro.
O conceito de Programação Funcional abordou a biblioteca purrr para manipular vetores e listas (estruturas de dados unidimensionais).
Para dataframes (tradicionais ou tibbles), a biblioteca a usar é o dplyr.
Já falámos do dplyr anteriormente, no conceito Switch.
Nesse conceito usámos apenas algumas funções utilitárias, mas agora podemos começar a explorar o resto desta grande biblioteca.
Os dataframes, incluindo os tibbles, podem ser tratados como listas de vetores coluna, por isso a indexação de listas recupera uma coluna específica.
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
Um dataframe também pode ser indexado com indexação ao estilo das matrizes.
tbl[c(2, 4), 1:2]
# A tibble: 2 × 2
#> languages created
#> <chr> <dbl>
#> 1 R 1993
#> 2 Julia 2012
No R moderno, com o ecossistema Tidyverse, as funções do dplyr são geralmente mais flexíveis e convenientes, e serão o foco do resto deste conceito.
Obtém uma única coluna com pull(), indicando o nome ou o número sequencial (números negativos contam da direita para a esquerda).
tbl |> pull(created)
#> [1] 1957 1993 1991 2012
Este é o mesmo resultado que tbl$created, mas usando uma função que se integra bem em pipelines.
Para obter várias colunas, a função adequada é select(), que é muito versátil.
Obtém (ou remove) colunas com base em propriedades do seu nome ou 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
São permitidos vários critérios, usando os operadores Boolean &, | e ! (e, ou, não).
Os nomes de coluna que são identificadores válidos em R não precisam de aspas dentro de um select().
Os nomes inválidos (por exemplo, os que incluem espaços) podem ser envolvidos em acentos graves, embora talvez seja melhor renomeá-los.
A função select() pode trabalhar com várias funções auxiliares para escolher nomes de colunas: starts_with, contains, num_range e várias outras.
O matches permite correspondências completas com expressões regulares.
Consulta a documentação para mais detalhes.
Tanto poder parece um exagero com o nosso dataframe de brinquedo sobre linguagens.
Felizmente, o tibble starwars está incluído no dplyr, o que nos dá algo maior para praticar.
# 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
Claramente, o dplyr oferece formas poderosas de selecionar colunas pelo nome.
Será que podemos fazer algo semelhante com os nomes das linhas?
Não!
Os dataframes tradicionais do R podem ter nomes de linhas, mas (depois de um histórico de erros e problemas de desempenho) os nomes de linhas não são permitidos nos tibbles.
Se quiseres nomes, coloca-os numa coluna <chr> (normalmente a coluna 1), usada como qualquer outra coluna.
Funções de importação como as_tibble() criam-na automaticamente ao importar dados com linhas nomeadas.
Se esta limitação dos nomes de linhas parecer estranhamente restritiva, lembra-te de que a maioria dos grandes sistemas de bases de dados trata as tabelas da mesma forma: Oracle, SQL Server, PostgreSQL, MySQL...
Obtém as linhas que correspondem a um critério com filter(), ou exclui-as com 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
Os critérios de filtragem podem ser arbitrariamente complexos, mas baseiam-se sempre no conteúdo das linhas.
Se soubermos os números das linhas, podemos usar várias funções 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
Para remover linhas duplicadas, usa distinct().
Primeira ressalva: o comportamento predefinido de copy-on-modify faz com que o tibble original permaneça normalmente inalterado.
A maioria das modificações é aplicada coluna a coluna.
Os nomes das colunas podem ser alterados com rename(newname = oldname), ou com rename_with() para aplicar uma função.
Um uso típico é limpar os nomes importados para os tornar mais fáceis de trabalhar em R, removendo espaços em branco e impondo um formato consistente para nomes relacionados.
Repara na sintaxe dentro de rename().
O conteúdo da coluna oldname fica ligado ao nome newname, daí a ordem.
A ordem das colunas pode ser alterada com relocate().
Por predefinição, a(s) coluna(s) indicada(s) são movidas para a(s) posição(ões) mais à esquerda, mas pode usar-se um argumento .before ou .after para um posicionamento mais 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
Para alterações maiores, mutate() permite-te:
NULL
É claro que mutate() é poderoso e potencialmente confuso, e uma razão para estarmos muito gratos pelo copy-on-modify.
Não há razão óbvia para nos preocuparmos com o índice de massa corporal das personagens de Star Wars, mas, por precaução:
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 queres operar sobre um subconjunto de colunas com funções como mutate(), a sequência select() |> mutate() do exemplo acima é uma opção.
Só as colunas selecionadas estarão no resultado.
Em alternativa, pode ser conveniente usar pick() dentro da chamada 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>
Só as colunas passadas a pick são usadas na mutação, mas todas as colunas são devolvidas.
As operações por linha são menos comuns para modificar um único tibble (a junção de vários tibbles será abordada num conceito posterior).
Uma exceção: arrange() ordena as linhas pelos valores de uma ou mais colunas.
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
Os dataframes, tradicionais ou tibbles, são centrais na forma como o R moderno é normalmente usado.
A maioria das funções do Tidyverse (não só as do dplyr) recebe tibbles como valores de entrada e/ou cria tibbles como resultado.
Este conceito foi apenas uma breve introdução, mal arranhando a superfície do que é possível.
Conceitos posteriores abordarão vários outros aspetos dos dataframes (dentro das limitações técnicas do Exercism).