Percursos
/
R
R
/
Exercícios
/
Árvores e Tibble-ations
Árvores e Tibble-ations

Árvores e Tibble-ations

Exercício de aprendizagem

Introdução

Noutras partes do currículo, já vimos vários tipos de dados com características diferentes.

  • Os vetores atómicos são unidimensionais e homogéneos em tipo.
  • As listas são unidimensionais e os seus elementos podem ser de tipos heterogéneos.
  • As matrizes e os arrays são multidimensionais e homogéneos.

Este Conceito vai abordar formas de guardar dados multidimensionais e heterogéneos. Na prática, a maioria dos dados do mundo real é assim, por isso estamos agora a chegar ao cerne da forma como o R é (sobretudo) usado na prática.

Variantes de dataframe

Ao longo das décadas, o R foi adicionando vários tipos de dados para lidar com dados tabulares.

Este currículo vai centrar-se sobretudo em tibbles, mas é útil conhecer as alternativas.

O data.frame

No R Base, um data.frame é uma list de vectors de igual comprimento. Podemos pensar nele como uma tabela retangular de dados, em que cada coluna é homogénea, mas cada linha pode (e costuma) conter 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

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.

O tibble

O design do data.frame é antigo.

Décadas de experiência, a par da mudança dos padrões de utilização do R, levaram a uma reformulação, criando uma alternativa modernizada no Tidyverse: os tibbles.

Em comparação com o R Base, os tibbles têm:

  • Valores predefinidos diferentes, para reduzir problemas comuns.
  • Menor propensão para forçar a conversão de tipos de dados durante a entrada.
  • Mensagens de erro mais numerosas e mais claras.
  • Formatos de apresentação diferentes, normalmente melhores.

Em suma, um tibble procura "fazer menos e queixar-se mais", também descrito como "preguiçoso e rabugento".

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 vai ajudar-te a criar código mais robusto. No entanto, o código e os dados legados são muito abundantes 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      

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.

Trabalhar com tibbles

Os tibbles são uma parte central do Tidyverse, por isso adiciona-os com library(tibble) ou library(tidyverse).

Criar um tibble

Da forma mais simples, podemos usar a função tibble() para juntar vetores coluna, tal como em data.frame(). Vimos um exemplo disto numa secção anterior.

Se for mais cómodo introduzir os valores linha a linha, a função correspondente é tribble().

Na prática, há dezenas de formas de criar tibbles, pois são o formato de saída predefinido de uma grande variedade de funções do Tidyverse.

Manipular um tibble

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 correspondente a usar é o dplyr.

Subconjuntos

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

Também se pode indexar um dataframe 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 cómodas e serão o foco do resto deste Conceito.

Operações por coluna

Obtém uma única coluna com pull(), indicando o nome ou o número sequencial (números negativos para contar 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 é extremamente versátil. Obtém (ou descarta) colunas com base em propriedades do seu nome ou do seu 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      

# Use type of column
tbl |> select(where(is.numeric))
  # A tibble: 4 × 1
#>   created
#>     <dbl>
#> 1    1957
#> 2    1993
#> 3    1991
#> 4    2012

É possível usar vários critérios, com os operadores Boolean &, | e ! (and, or not).

Tanto poder parece um exagero com o nosso dataframe de brinquedo de linguagens. Felizmente, o tibble starwars está incluído no dplyr, o que nos dá algo maior para praticar.

# 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    

Operações por linha

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   

Modificar um tibble

Primeiro aviso: o comportamento predefinido copy-on-modify faz com que o tibble original normalmente fique inalterado.

A maioria das modificações é aplicada por coluna.

Os nomes das colunas podem ser alterados com rename(newname = oldname), ou com rename_with() para aplicar uma função. Repara na sintaxe dentro de rename(). O conteúdo da coluna oldname é associado ao nome newname, daí a ordem.

A ordem das colunas pode ser alterada com relocate(). As colunas especificadas são movidas para a posição mais à esquerda por predefinição, 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, o mutate() permite-te:

  • Criar novas colunas que são funções de colunas existentes.
  • Substituir uma coluna existente, criando uma nova coluna com o mesmo nome.
  • Eliminar uma coluna, atribuindo-lhe o valor NULL.

Claramente, o mutate() é poderoso, potencialmente confuso e um motivo para estarmos muito gratos pelo copy-on-modify.

Não há nenhuma razão óbvia para nos preocuparmos com o índice de massa corporal das personagens de Star Wars, mas, só 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

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: 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     

Instruções

Um grande pomar de cerejeiras tem geralmente um pequeno inventário de árvores excedentes que gostaria de oferecer como madeira. Isto ajuda a manter o pomar saudável e, ao mesmo tempo, proporciona receita extra. Pediram-te que ajudasses a programar algumas funcionalidades para ajudar no processo de seleção de árvores quando um potencial cliente telefona.

Note

A biblioteca incorporada datasets tem muitos conjuntos de dados de exemplo, e este exercício usa o conjunto de dados trees. Para chamar o conjunto de dados, basta usar o nome trees.

library(datasets) # loading is optional

trees |> head(3)
#>    A data.frame: 3 × 3 	
#>   Girth	Height	Volume
#>	 <dbl>	 <dbl>	 <dbl>
#> 1   8.3	    70	  10.3  
#> 2   8.6	    65	  10.3
#> 3   8.8	    63	  10.2

Repara nas três colunas: Girth, Height e Volume.

O conjunto de dados está inicialmente na forma de um data.frame, mas podes querer trabalhar com ele como um tibble.

1. Renomeia uma coluna

Infelizmente, houve um erro administrativo que nunca foi corrigido e uma coluna do conjunto de dados chama-se Girth, mas na verdade é o Diameter.

Define um data.frame/tibble chamado tree_data que corrige o nome da coluna Girth em trees para Diameter.

head(tree_data, 3)
#>     A tibble: 3 × 3 
#> Diameter	 Height	Volume
#>    <dbl>	  <dbl>	 <dbl>
#>      8.3	     70	  10.3
#>      8.6	     65	  10.3
#>      8.8	     63	  10.2

2. Acrescenta as colunas de perímetro e peso

O perímetro (ou seja, a circunferência) continua a ser uma medição útil, e o mesmo se passa com o peso da madeira. O perímetro calcula-se facilmente com πD, em que D é o diâmetro. O peso de toda a madeira pode ser calculado por ρV, em que ρ = 35 é a densidade da madeira de cerejeira e V é o volume da madeira.

Define a função girth_n_weight(data, rnd_digits), que recebe um dataframe e argumentos numéricos. A função deve devolver um novo dataframe com as duas colunas adicionais: Girth e Weight, arredondadas ao número de algarismos apropriado indicado por rnd_digits.

girth_n_weight(tree_data, 1) |> head(3)
#>           A tibble: 3 × 5 
#> Diameter  Height	 Volume	 Girth	Weight
#>    <dbl>	  <dbl>	  <dbl>	 <dbl>	 <dbl>
#>      8.3	     70	   10.3	  26.1	 360.5
#>      8.6	     65	   10.3	  27.0	 360.5
#>      8.8	     63	   10.2	  27.6	 357.0

3. Cópia do conjunto de dados do pomar

Para cada potencial cliente, o pomar guarda uma cópia especial do conjunto de dados para ajudar na venda. Este conjunto de dados tem as colunas mais relevantes, Weight e Height, movidas para a frente e está ordenado por Weight.

Define a função orchard_copy(data, important_cols), que recebe um dataframe como valor de entrada. Deve devolver um novo dataframe com Weight e Height movidas para a frente e as linhas ordenadas por Weight.

tree_data |> girth_n_weight() |> orchard_copy() |> head(3)
#>            A tibble: 3 × 5 
#> Weight	Height	Diameter	Volume	Girth
#>  <dbl>	 <dbl>	   <dbl>	 <dbl>	<dbl>
#>    357	    63	       9	    10	   28
#>    360	    70	       8	    10	   26
#>    360	    65	       9	    10	   27

4. Cópia do conjunto de dados do cliente

Cada potencial cliente recebe uma versão pessoal do conjunto de dados, com base nas suas preferências. Para isso, o cliente pode especificar: a altura mínima, a altura máxima e o peso máximo de qualquer árvore individual.

Define a função customer_copy(data, min_height, max_height, max_weight). Devolve uma cópia do cliente do dataframe com as colunas Height, Weight, Diameter e Girth e com as linhas que estão dentro dos limites.

tree_data |> 
    girth_n_weight(1) |> 
    orchard_copy() |>
    customer_copy(65, 75, 1500) |> 
    head(3)
#>        A tibble: 3 × 4 
#> Height	Weight	Diameter	Girth
#>  <dbl>	 <dbl>	   <dbl>	<dbl>
#>     70	 360.5	     8.3	 26.1
#>     65	 360.5	     8.6	 27.0
#>     66	 546.0	    11.0	 34.6
Editar via GitHub A ligação abre numa nova janela ou separador
R Exercism

Estás pronto para começar Árvores e Tibble-ations?

Inscreve-te no Exercism para aprenderes e dominares R com 21 conceitos111 exercícios, e mentoria humana real, tudo grátis.