Capítulo 6 Dataframes e outros Objetos

No R, tudo é um objeto. Sempre que chamamos uma função, tal como nos exemplos dos capítulos anteriores, ela nos retorna um objeto. Cada tipo ou classe de objeto terá uma série de propriedades diferentes. Por exemplo, um dataframe pode ser incrementado com novas colunas ou linhas. Uma coluna numérica de um dataframe pode interagir com outros valores numéricos através de operações de multiplicação, divisão e soma. Para colunas com textos, porém, tal propriedade não é válida, uma vez que não faz sentido somar um valor numérico a um texto ou dividir um texto por outro. Entretanto, a classe de texto tem outras propriedades, como a que permite procurar uma determinada sequência textual dentro de um texto maior, a manipulação de partes do texto e a substituição de caracteres específicos, dentre tantas outras possibilidades. Um dos aspectos mais importantes no trabalho com o R é o aprendizado das classes de objetos e as suas funcionalidades.

As classes básicas de objetos no R inclui valores numéricos, caracteres (texto), fatores, datas, entre vários outros casos. Na prática, porém, as classes básicas são armazenadas em estruturas de dados mais complexas, tal como dataframes e listas. Isso organiza e facilita o trabalho. Imagine realizar um estudo sobre as 63 ações que compõem o índice Ibovespa, onde a base de dados é composta por preços e volumes negociados ao longo de um ano. Caso fôssemos criar um vetor numérico de preços e de volumes para cada ação, teríamos uma quantidade de 126 objetos para lidar no nosso enviromnent. Apesar de ser possível trabalhar dessa forma, o código resultante seria desorganizado, difícil de entender e passível de uma série de erros.

Uma maneira mais simples de organizar os nossos dados é criar um objeto com o nome my_data e alocar todos os preços e volumes ali. Todas as informações necessárias para executar a pesquisa estariam nesse objeto, facilitando a importação e exportação dos dados. Esses objetos que armazenam outros objetos de classe básica constituem a classe de estrutura de dados. Nessa classificação, estão incluídas listas (list), matrizes (matrix) e tabelas (dataframes).

6.1 Dataframes

Traduzindo para o português, dataframe significa “estrutura ou organização de dados.” Grosso modo, um objeto da classe dataframe nada mais é do que uma tabela com linhas e colunas. Sem dúvida, o dataframe é o principal objeto utilizado no trabalho com o R e o mais importante de se estudar. Dados externos são, grande maioria dos casos, importados para o R no formato de tabelas. É na manipulação desses que gastará maior parte do tempo realizando a sua análise. Internamente, um dataframe é um tipo especial de lista, onde cada coluna é um vetor atômico com o mesmo número de elementos, porém com sua própria classe. Podemos organizar em um dataframe dados de texto juntamente com números, por exemplo.

Note que o formato tabular força a sincronização dos dados no sentido de linhas, isto é, cada caso de cada variável deve ser pareado com casos de outras variáveis. Apesar de simples, esse tipo de estruturação de dados é intuitiva e pode acomodar uma variedade de informações. Cada acréscimo de dados (informações) incrementa as linhas e cada novo tipo de informação incrementa as colunas da tabela.

Um dos pontos positivos na utilização do dataframe para a acomodação de dados é que funções de diferentes pacotes irão funcionar a partir dessa classe de objetos. Por exemplo, o pacote de manipulação de dados dplyr, assim como o pacote de criação de figuras ggplot2, funcionam a partir de um dataframe. Esse objeto, portanto, está no centro de uma série de funcionalidades do R e, sem dúvida, é uma classe de objeto extremamente importante para aprender a utilizar corretamente.

O objeto dataframe é uma das classes nativas do R e vem implementado no pacote base. Entretanto, o universe tidyverse oferece sua própria versão de um dataframe, chamada tibble, a qual é utilizada sistematicamente em todos pacotes do tidyverse. A conversão de um dataframe para tibble é interna e automática. O tibble possui propriedades mais flexíveis que dataframes nativos, facilitando de forma significativa o seu uso. Seguindo a nossa preferência para o tidyverse, a partir de agora iremos utilizar tibbles como representantes de dataframes.

6.1.1 Criando dataframes

A criação de um dataframe do tipo tibble ocorre a partir da função tibble. Note que a criação de um dataframe nativo ocorre com a função base::data.frame, enquanto a criação do tibble parte da função tibble::tibble ou dplyr::tibble. Para manter o código mais limpo, iremos dar preferência a dplyr::tibble e utilizar o nome dataframe para se referir a um tibble. Veja o exemplo a seguir, onde criamos uma tabela correspondente a dados financeiros de diferentes ações.

library(tibble)

# set tickers
ticker <- c(rep('ABEV3',4),
            rep('BBAS3', 4),
            rep('BBDC3', 4))

# set dates
ref_date <- as.Date(rep(c('2010-01-01', '2010-01-04',
                          '2010-01-05', '2010-01-06'),
                        3) )

# set prices
price <- c(736.67, 764.14, 768.63, 776.47,
           59.4  , 59.8  , 59.2  , 59.28,
           29.81 , 30.82 , 30.38 , 30.20)

# create tibble/dataframe
my_df <- tibble(ticker, ref_date , price)

# print it
print(my_df)
R> # A tibble: 12 x 3
R>    ticker ref_date   price
R>    <chr>  <date>     <dbl>
R>  1 ABEV3  2010-01-01 737. 
R>  2 ABEV3  2010-01-04 764. 
R>  3 ABEV3  2010-01-05 769. 
R>  4 ABEV3  2010-01-06 776. 
R>  5 BBAS3  2010-01-01  59.4
R>  6 BBAS3  2010-01-04  59.8
R>  7 BBAS3  2010-01-05  59.2
R>  8 BBAS3  2010-01-06  59.3
R>  9 BBDC3  2010-01-01  29.8
R> 10 BBDC3  2010-01-04  30.8
R> 11 BBDC3  2010-01-05  30.4
R> 12 BBDC3  2010-01-06  30.2

Observe que utilizamos a função rep para replicar e facilitar a criação dos dados do dataframe anterior. Assim, não é necessário repetir os valores múltiplas vezes. Destaca-se que, no uso dos dataframes, podemos salvar todos os nossos dados em um único objeto, facilitando o acesso e a organização do código resultante.

O conteúdo de dataframes também pode ser visualizado no próprio RStudio. Para isso, basta clicar no nome do objeto na aba environment, canto superior direito da tela. Após isso, um visualizador aparecerá na tela principal do programa. Essa operação é nada mais que uma chamada a função utils::View. Portanto, poderíamos visualizar o dataframe anterior executando o comando View(my_df).

6.1.2 Inspecionando um dataframe

Após a criação do dataframe, o segundo passo é conhecer o seu conteúdo. Particularmente, é importante tomar conhecimento dos seguintes itens em ordem de importância:

Número de linhas e colunas
O número de linhas e colunas da tabela resultante indicam se a operação de importação foi executada corretamente. Caso os valores forem diferentes do esperado, deve-se checar o arquivo de importação dos dados e se as opções de importação fazem sentido para o arquivo.
Nomes das colunas
É importante que a tabela importada tenha nomes que façam sentido e que sejam fáceis de acessar. Portanto, o segundo passo na inspeção de um dataframe é analisar os nomes das colunas e seus respectivos conteúdos. Confirme que cada coluna realmente apresenta um nome intuitivo.
Classes das colunas
Cada coluna de um dataframe tem sua própria classe. É de suma importância que as classes dos dados estejam corretamente especificadas. Caso contrário, operações futuras podem resultar em um erro. Por exemplo, caso um vetor de valores numéricos seja importado com a classe de texto (character), qualquer operação matemática nesse vetor irá resultar em um erro no R.
Existência de dados omissos (NA)
Devemos também verificar o número de valores NA (not available) nas diferentes colunas. Sempre que você encontrar uma grande proporção de valores NA na tabela importada, você deve descobrir o que está acontecendo e se a informação está sendo importada corretamente. Conforme mencionado no capítulo anterior, os valores NA são contagiosos e transformarão qualquer objeto que interagir com um NA, também se tornará um NA.

Uma das funções mais recomendadas para se familiarizar com um dataframe é dplyr::glimpse. Essa mostra na tela o nome e a classe das colunas, além do número de linhas/colunas. Abusamos dessa função nos capítulos anteriores. Veja um exemplo simples a seguir:

library(dplyr)

# check content of my_df
glimpse(my_df)
R> Rows: 12
R> Columns: 3
R> $ ticker   <chr> "ABEV3", "ABEV3", "ABEV3", "ABEV3", "BBAS…
R> $ ref_date <date> 2010-01-01, 2010-01-04, 2010-01-05, 2010…
R> $ price    <dbl> 736.67, 764.14, 768.63, 776.47, 59.40, 59…

Em muitas situações, o uso de glimpse é suficiente para entender se o processo de importação de dados ocorreu de forma satisfatória. Porém, uma análise mais profunda é entender qual a variação de cada coluna nos dados importados. Aqui entra o papel da função base::summary:

# check variation my_df
summary(my_df)
R>     ticker             ref_date              price       
R>  Length:12          Min.   :2010-01-01   Min.   : 29.81  
R>  Class :character   1st Qu.:2010-01-03   1st Qu.: 30.71  
R>  Mode  :character   Median :2010-01-04   Median : 59.34  
R>                     Mean   :2010-01-04   Mean   :283.73  
R>                     3rd Qu.:2010-01-05   3rd Qu.:743.54  
R>                     Max.   :2010-01-06   Max.   :776.47

Note que summary interpreta cada coluna de forma diferente. Para o primeiro caso, coluna ticker, mostra apenas o tamanho do vetor. No caso de datas e valores numéricos, essa apresenta o máximo, mínimo, mediana e quartis. Por exemplo, uma observação extrema (outlier) poderia ser facilmente identificada na análise da saída textual de summary.

Toda vez que se deparar com um novo dataframe no R, pegue o hábito de verificar o seu conteúdo com funções dplyr::glimpse e base::summary. Assim, poderá perceber problemas de importação e/ou conteúdo dos arquivos lidos. Com experiência irás perceber que muitos erros futuros em código podem ser sanados por uma simples inspeção das tabelas importadas.

6.1.3 Operador de pipeline (%>%)

Uma característica importante do universo tidyverse é o uso extensivo do operador de pipeline, primeiro proposto por Bache and Wickham (2020) e definido pelo símbolo %>%. Esse comando permite que operações de dados sejam realizadas de forma sequencial e modular, como em uma tubulação, facilitando a otimização e legibilidade do código resultante.

Imagine uma situação onde temos três funções para aplicar nos dados salvos em um dataframe. Cada função depende da saída de outra função. Isso requer o encadeamento de suas chamadas. Usando o operador de pipeline, podemos escrever o procedimento de manipulação dataframe com o seguinte código:

my_tab <- my_df %>%
  fct1(arg1) %>%
  fct2(arg2) %>%
  fct3(arg3)

Usamos símbolo %>% no final de cada linha para vincular as operações. As funções fct* são operações realizadas em cada etapa. O resultado de cada linha é passado para a próxima função de forma sequencial. Assim, não há necessidade de criar objetos intermediários. Veja a seguir duas formas alternativas de realizar a mesma operação sem o operador de pipeline:

# version 1
my_tab <- fct3(fct2(fct1(my_df,
                         arg1),
                    arg2),
               arg1)

# version 2
temp1 <- fct1(my_df, arg1)
temp2 <- fct2(temp1, arg2)
my_tab <- fct3(temp1, arg3)

Observe como as alternativas formam um código com estrutura estranha e passível a erros. Provavelmente não deves ter notado, mas ambos os códigos possuem erros de digitação. Para o primeiro, o último arg1 deveria ser arg3 e, no segundo, a função fct3 está usando o dataframe temp1 e não temp2. Este exemplo deixa claro como o uso de pipelines torna o código mais elegante e legível. A partir de agora iremos utilizar o operador %>% de forma extensiva.

6.1.4 Acessando Colunas

Um objeto do tipo dataframe utiliza-se de diversos comandos e símbolos que também são usados em matrizes e listas. Para descobrir os nomes das colunas de um dataframe, temos duas funções: names ou colnames:

# check names of df
names(my_df)
R> [1] "ticker"   "ref_date" "price"
colnames(my_df)
R> [1] "ticker"   "ref_date" "price"

Ambas também podem ser usadas para modificar os nomes das colunas:

# set temp df
temp_df <- my_df

# check names
names(temp_df)
R> [1] "ticker"   "ref_date" "price"
# change names
names(temp_df) <- paste0('Col', 1:ncol(temp_df))

# check names
names(temp_df)
R> [1] "Col1" "Col2" "Col3"

Destaca-se que a forma de usar names é bastante distinta das demais funções do R. Nesse caso, utilizamos a função ao lado esquerdo do símbolo de assign (<-). Internamente, o que estamos fazendo é definindo um atributo do objeto temp_df, o nome de suas colunas.

Para acessar uma determinada coluna, podemos utilizar o nome da mesma:

# isolate columns of df
my_ticker <- my_df$ticker
my_prices <- my_df[['price']]

# print contents
print(my_ticker)
R>  [1] "ABEV3" "ABEV3" "ABEV3" "ABEV3" "BBAS3" "BBAS3" "BBAS3"
R>  [8] "BBAS3" "BBDC3" "BBDC3" "BBDC3" "BBDC3"
print(my_prices)
R>  [1] 736.67 764.14 768.63 776.47  59.40  59.80  59.20  59.28
R>  [9]  29.81  30.82  30.38  30.20

Note o uso do duplo colchetes ([[]]) para selecionar colunas. Vale apontar que, no R, um objeto da classe dataframe é representado internamente como uma lista, onde cada elemento é uma coluna. Isso é importante saber, pois alguns comandos de listas também funcionam para dataframes. Um exemplo é o uso de duplo colchetes ([[]]) para selecionar colunas por posição:

print(my_df[[2]])
R>  [1] "2010-01-01" "2010-01-04" "2010-01-05" "2010-01-06"
R>  [5] "2010-01-01" "2010-01-04" "2010-01-05" "2010-01-06"
R>  [9] "2010-01-01" "2010-01-04" "2010-01-05" "2010-01-06"

Para acessar linhas e colunas específicas de um dataframe, basta utilizar colchetes simples:

print(my_df[1:5,2])
R> # A tibble: 5 x 1
R>   ref_date  
R>   <date>    
R> 1 2010-01-01
R> 2 2010-01-04
R> 3 2010-01-05
R> 4 2010-01-06
R> 5 2010-01-01
print(my_df[1:5,c(1,2)])
R> # A tibble: 5 x 2
R>   ticker ref_date  
R>   <chr>  <date>    
R> 1 ABEV3  2010-01-01
R> 2 ABEV3  2010-01-04
R> 3 ABEV3  2010-01-05
R> 4 ABEV3  2010-01-06
R> 5 BBAS3  2010-01-01
print(my_df[1:5, ])
R> # A tibble: 5 x 3
R>   ticker ref_date   price
R>   <chr>  <date>     <dbl>
R> 1 ABEV3  2010-01-01 737. 
R> 2 ABEV3  2010-01-04 764. 
R> 3 ABEV3  2010-01-05 769. 
R> 4 ABEV3  2010-01-06 776. 
R> 5 BBAS3  2010-01-01  59.4

Essa seleção de colunas também pode ser realizada utilizando o nome das mesmas da seguinte forma:

print(my_df[1:3, c('ticker','price')])
R> # A tibble: 3 x 2
R>   ticker price
R>   <chr>  <dbl>
R> 1 ABEV3   737.
R> 2 ABEV3   764.
R> 3 ABEV3   769.

ou, pelo operador de pipeline e a função dplyr::select:

library(dplyr)

my.temp <- my_df %>%
  select(ticker, price) %>%
  glimpse()
R> Rows: 12
R> Columns: 2
R> $ ticker <chr> "ABEV3", "ABEV3", "ABEV3", "ABEV3", "BBAS3"…
R> $ price  <dbl> 736.67, 764.14, 768.63, 776.47, 59.40, 59.8…

6.1.5 Modificando um dataframe

Para criar novas colunas em um dataframe, basta utilizar a função mutate. Aqui iremos abusar do operador de pipeline (%>%) para sequenciar as operações:

library(dplyr)

# add columns with mutate
my_df <- my_df %>%
  mutate(ret = price/lag(price) -1,
         my_seq1 = 1:nrow(my_df),
         my_seq2 =  my_seq1 +9) %>%
  glimpse()
R> Rows: 12
R> Columns: 6
R> $ ticker   <chr> "ABEV3", "ABEV3", "ABEV3", "ABEV3", "BBAS…
R> $ ref_date <date> 2010-01-01, 2010-01-04, 2010-01-05, 2010…
R> $ price    <dbl> 736.67, 764.14, 768.63, 776.47, 59.40, 59…
R> $ ret      <dbl> NA, 0.037289424, 0.005875887, 0.010199966…
R> $ my_seq1  <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12
R> $ my_seq2  <dbl> 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 2…

Note que precisamos indicar o dataframe de origem dos dados, nesse caso o objeto my_df, e as colunas são definidas como argumentos em dplyr::mutate. Observe também que usamos a coluna price na construção de ret, o retorno aritmético dos preços. Um caso especial é a construção de my_seq2 com base em my_seq1, isto é, antes mesmo dela ser explicitamente calculada já é possível utilizar a nova coluna para criar outra. Vale salientar que a nova coluna deve ter exatamente o mesmo número de elementos que as demais. Caso contrário, o R retorna uma mensagem de erro.

A maneira mais tradicional, e comumente encontrada em código, para criar novas colunas é utilizar o símbolo $:

# add new column with base R
my_df$my_seq3 <- 1:nrow(my_df)

# check it
glimpse(my_df)
R> Rows: 12
R> Columns: 7
R> $ ticker   <chr> "ABEV3", "ABEV3", "ABEV3", "ABEV3", "BBAS…
R> $ ref_date <date> 2010-01-01, 2010-01-04, 2010-01-05, 2010…
R> $ price    <dbl> 736.67, 764.14, 768.63, 776.47, 59.40, 59…
R> $ ret      <dbl> NA, 0.037289424, 0.005875887, 0.010199966…
R> $ my_seq1  <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12
R> $ my_seq2  <dbl> 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 2…
R> $ my_seq3  <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12

Portanto, o operador $ vale tanto para acessar quanto para criar novas colunas.

Para remover colunas de um dataframe, basta usar dplyr::select com operador negativo para o nome das colunas indesejadas:

# removing columns
my_df_temp <- my_df %>%
  select(-my_seq1, -my_seq2, -my_seq3) %>%
  glimpse()
R> Rows: 12
R> Columns: 4
R> $ ticker   <chr> "ABEV3", "ABEV3", "ABEV3", "ABEV3", "BBAS…
R> $ ref_date <date> 2010-01-01, 2010-01-04, 2010-01-05, 2010…
R> $ price    <dbl> 736.67, 764.14, 768.63, 776.47, 59.40, 59…
R> $ ret      <dbl> NA, 0.037289424, 0.005875887, 0.010199966…

No uso de funções nativas do R, a maneira tradicional de remover colunas é alocar o valor nulo (NULL):

# set temp df
temp_df <- my_df

# remove cols
temp_df$price <- NULL
temp_df$ref_date  <- NULL

# check it
glimpse(temp_df)
R> Rows: 12
R> Columns: 5
R> $ ticker  <chr> "ABEV3", "ABEV3", "ABEV3", "ABEV3", "BBAS3…
R> $ ret     <dbl> NA, 0.037289424, 0.005875887, 0.010199966,…
R> $ my_seq1 <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12
R> $ my_seq2 <dbl> 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20…
R> $ my_seq3 <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12

6.1.6 Filtrando um dataframe

Uma operação bastante comum no R é filtrar linhas de uma tabela de acordo com uma ou mais condições. Por exemplo, caso quiséssemos apenas os dados da ação ABEV3, poderíamos utilizar a função dplyr::filter para filtrar a tabela:

library(dplyr)

# filter df for single stock
my_df_temp <- my_df %>%
  filter(ticker == 'ABEV3') %>%
  glimpse()
R> Rows: 4
R> Columns: 7
R> $ ticker   <chr> "ABEV3", "ABEV3", "ABEV3", "ABEV3"
R> $ ref_date <date> 2010-01-01, 2010-01-04, 2010-01-05, 2010-…
R> $ price    <dbl> 736.67, 764.14, 768.63, 776.47
R> $ ret      <dbl> NA, 0.037289424, 0.005875887, 0.010199966
R> $ my_seq1  <int> 1, 2, 3, 4
R> $ my_seq2  <dbl> 10, 11, 12, 13
R> $ my_seq3  <int> 1, 2, 3, 4

A função também aceita mais de uma condição. Veja a seguir onde filtramos os dados para 'ABEV3' em datas após ou igual a '2010-01-05':

library(dplyr)
# filter df for single stock and date
my_df_temp <- my_df %>%
  filter(ticker == 'ABEV3',
         ref_date >= as.Date('2010-01-05')) %>%
  glimpse()
R> Rows: 2
R> Columns: 7
R> $ ticker   <chr> "ABEV3", "ABEV3"
R> $ ref_date <date> 2010-01-05, 2010-01-06
R> $ price    <dbl> 768.63, 776.47
R> $ ret      <dbl> 0.005875887, 0.010199966
R> $ my_seq1  <int> 3, 4
R> $ my_seq2  <dbl> 12, 13
R> $ my_seq3  <int> 3, 4

Aqui utilizamos o símbolo == para testar uma igualdade. Iremos estudar mais profundamente a classe de testes lógicos no capítulo 7.

6.1.7 Ordenando um dataframe

Após a criação ou importação de um dataframe, pode-se ordenar seus componentes de acordo com os valores de alguma coluna. Um caso bastante comum em que é necessário realizar uma ordenação explícita é quando importamos dados financeiros em que as datas não estão em ordem crescente. Na grande maioria das situações, dados temporais devem estar ordenados de acordo com a antiguidade, isto é, dados mais recentes são alocados na última linha da tabela. Essa operação é realizada através do uso da função base::order ou dplyr::arrange.

Como exemplo, considere a criação de um dataframe com os valores a seguir:

library(tidyverse)

# set df
my_df <- tibble(col1 = c(4,1,2),
                col2 = c(1,1,3),
                col3 = c('a','b','c'))

# print it
print(my_df)
R> # A tibble: 3 x 3
R>    col1  col2 col3 
R>   <dbl> <dbl> <chr>
R> 1     4     1 a    
R> 2     1     1 b    
R> 3     2     3 c

A função order retorna os índices relativos à ordenação dos valores dados como entrada. Para o caso da primeira coluna de my_df, os índices dos elementos formadores do novo vetor, com seus valores ordenados em forma crescente, são:

idx <- order(my_df$col1)
print(idx)
R> [1] 2 3 1

Portanto, ao utilizar a saída da função order como indexador do dataframe, acaba-se ordenando o mesmo de acordo com os valores da coluna col1. Veja a seguir:

my_df_2 <- my_df[order(my_df$col1), ]
print(my_df_2)
R> # A tibble: 3 x 3
R>    col1  col2 col3 
R>   <dbl> <dbl> <chr>
R> 1     1     1 b    
R> 2     2     3 c    
R> 3     4     1 a

Essa operação de ordenamento também pode ser realizada levando em conta mais de uma coluna. Veja o exemplo a seguir, onde se ordena o dataframe pelas colunas col2 e col1.

idx <- order(my_df$col2, my_df$col1)
my_df_3 <- my_df[idx, ]
print(my_df_3)
R> # A tibble: 3 x 3
R>    col1  col2 col3 
R>   <dbl> <dbl> <chr>
R> 1     1     1 b    
R> 2     4     1 a    
R> 3     2     3 c

No tidyverse, a forma de ordenar dataframes é pelo uso da função arrange. No caso de ordenamento decrescente, encapsulamos o nome das colunas com desc:

# sort ascending, by col1 and col2
my_df <- my_df %>%
  arrange(col1, col2) %>%
  print()
R> # A tibble: 3 x 3
R>    col1  col2 col3 
R>   <dbl> <dbl> <chr>
R> 1     1     1 b    
R> 2     2     3 c    
R> 3     4     1 a
# sort descending, col1 and col2
my_df <- my_df %>%
  arrange(desc(col1), desc(col2)) %>%
  print()
R> # A tibble: 3 x 3
R>    col1  col2 col3 
R>   <dbl> <dbl> <chr>
R> 1     4     1 a    
R> 2     2     3 c    
R> 3     1     1 b

O resultado prático no uso de arrange é o mesmo de order. Um dos seus benefícios é a possibilidade de encadeamento de operações através do uso do pipeline.

6.1.8 Combinando e Agregando dataframes

Em muitas situações de análise de dados será necessário juntar dataframes distintos em um único objeto. Tabelas diferentes são importadas no R e, antes de analisar os dados, precisamos combinar as informações em um único objeto. Nos casos mais simples, onde as tabelas a serem agregadas possuem o mesmo formato, nós as juntamos de acordo com as linhas, verticalmente, ou colunas, horizontalmente. Para esse fim, temos as funções dplyr::bind_rows e dlyr::bind_cols no tidyverse e base::rbind e base::cbind nas funções nativas do R. Observe o exemplo a seguir.

library(dplyr)

# set dfs
my_df_1 <- tibble(col1 = 1:5,
                  col2 = rep('a', 5))

my_df_2 <- tibble(col1 = 6:10,
                  col2 = rep('b', 5),
                  col3 = rep('c', 5))

# bind by row
my_df <- bind_rows(my_df_1, my_df_2) %>%
  glimpse()
R> Rows: 10
R> Columns: 3
R> $ col1 <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
R> $ col2 <chr> "a", "a", "a", "a", "a", "b", "b", "b", "b", …
R> $ col3 <chr> NA, NA, NA, NA, NA, "c", "c", "c", "c", "c"

Note que, no exemplo anterior, os nomes das colunas são os mesmos. De fato, a função dplyr::bind_rows procura os nomes iguais em ambos os objetos para fazer a junção dos dataframes corretamente. As colunas que não ocorrem em ambos objetos, tal como col3 no exemplo, saem como NA no objeto final. Já para o caso de bind_cols, os nomes das colunas devem ser diferentes, porém o número de linhas deve ser o mesmo.

# set dfs
my_df_1 <- tibble(col1 = 1:5, col2 = rep('a', 5))
my_df_2 <- tibble(col3 = 6:10, col4 = rep('b', 5))

# bind by column
my_df <- bind_cols(my_df_1, my_df_2) %>%
  glimpse()
R> Rows: 5
R> Columns: 4
R> $ col1 <int> 1, 2, 3, 4, 5
R> $ col2 <chr> "a", "a", "a", "a", "a"
R> $ col3 <int> 6, 7, 8, 9, 10
R> $ col4 <chr> "b", "b", "b", "b", "b"

Para casos mais complexos, onde a junção deve ser realizada de acordo com algum índice tal como uma data, é possível juntar dataframes diferentes com o uso das funções da família dplyr::join* tal como dplyr::inner_join dplyr::left_join, dplyr::full_join, entre outras. A descrição de todas elas não cabe aqui. Iremos descrever apenas o caso mais provável, inner_join. Essa combina os dados, mantendo apenas os casos onde existe o índice em ambos.

# set df
my_df_1 <- tibble(date = as.Date('2016-01-01')+0:10,
                  x = 1:11)

my_df_2 <- tibble(date = as.Date('2016-01-05')+0:10,
                  y = seq(20,30, length.out = 11))

Note que os dataframes criados possuem uma coluna em comum, date. A partir desta coluna que agregamos as tabelas com inner_join:

# aggregate tables
my_df <- inner_join(my_df_1, my_df_2)
R> Joining, by = "date"
glimpse(my_df)
R> Rows: 7
R> Columns: 3
R> $ date <date> 2016-01-05, 2016-01-06, 2016-01-07, 2016-01-…
R> $ x    <int> 5, 6, 7, 8, 9, 10, 11
R> $ y    <dbl> 20, 21, 22, 23, 24, 25, 26

O R automaticamente verifica a existência de colunas com mesmo nome nos dataframes e realiza a junção por essas. Caso quiséssemos juntar dataframes onde os nomes das colunas para utilizar o índice não são iguais, temos duas soluções: modificar os nomes das colunas ou então utilizar argumento by em dplyr::inner_join. Veja a seguir:

# set df
my_df_3 <- tibble(ref_date = as.Date('2016-01-01')+0:10,
                  x = 1:11)

my_df_4 <- tibble(my_date = as.Date('2016-01-05')+0:10,
                  y = seq(20,30, length.out = 11))

# join by my_df_3$ref_date and my_df_4$my_date
my_df <- inner_join(my_df_3, my_df_4,
                    by = c('ref_date' = 'my_date'))

glimpse(my_df)
R> Rows: 7
R> Columns: 3
R> $ ref_date <date> 2016-01-05, 2016-01-06, 2016-01-07, 2016…
R> $ x        <int> 5, 6, 7, 8, 9, 10, 11
R> $ y        <dbl> 20, 21, 22, 23, 24, 25, 26

Para o caso de uso da função nativa de agregação de dataframes, base::merge, temos que indicar explicitamente o nome da coluna com argumento by:

# aggregation with base R
my_df <- merge(my_df_1, my_df_2, by = 'date')

glimpse(my_df)
R> Rows: 7
R> Columns: 3
R> $ date <date> 2016-01-05, 2016-01-06, 2016-01-07, 2016-01-…
R> $ x    <int> 5, 6, 7, 8, 9, 10, 11
R> $ y    <dbl> 20, 21, 22, 23, 24, 25, 26

Note que, nesse caso, o dataframe resultante manteve apenas as informações compartilhadas entre ambos os objetos, isto é, aquelas linhas onde as datas em date eram iguais. Esse é o mesmo resultado quando no uso do dplyr::inner_join.

As demais funções de agregação de tabelas – left_join, right_join, outer_join e full_join – funcionam de forma muito semelhante a inner_join, exceto na escolha da saída. Por exemplo, full_join retorna todos os casos/linhas entre tabela 1 e 2, incluindo aqueles onde não tem o índice compartilhado. Para estes casos, a coluna do índice sairá como NA. Veja o exemplo a seguir:

# set df
my_df_5 <- tibble(ref_date = as.Date('2016-01-01')+0:10,
                  x = 1:11)

my_df_6 <- tibble(ref_date = as.Date('2016-01-05')+0:10,
                  y = seq(20,30, length.out = 11))

# combine with full_join
my_df <- full_join(my_df_5, my_df_6)
R> Joining, by = "ref_date"
# print it
print(my_df)
R> # A tibble: 15 x 3
R>    ref_date       x     y
R>    <date>     <int> <dbl>
R>  1 2016-01-01     1    NA
R>  2 2016-01-02     2    NA
R>  3 2016-01-03     3    NA
R>  4 2016-01-04     4    NA
R>  5 2016-01-05     5    20
R>  6 2016-01-06     6    21
R>  7 2016-01-07     7    22
R>  8 2016-01-08     8    23
R>  9 2016-01-09     9    24
R> 10 2016-01-10    10    25
R> 11 2016-01-11    11    26
R> 12 2016-01-12    NA    27
R> 13 2016-01-13    NA    28
R> 14 2016-01-14    NA    29
R> 15 2016-01-15    NA    30

6.1.9 Extensões ao dataframe

Como já foi relatado nos capítulos anteriores, um dos grandes benefícios no uso do R é a existência de pacotes para lidar com os problemas específicos dos usuários. Enquanto um objeto tabular do tipo tibble é suficiente para a maioria dos casos, existem benefícios no uso de uma classe alternativa. Ao longo do tempo, diversas soluções foram disponibilizadas por desenvolvedores.

Por exemplo, é muito comum trabalharmos com dados exclusivamente numéricos que são indexados ao tempo. Isto é, situações onde cada informação pertence a um índice temporal - um objeto da classe data/tempo. As linhas dessa tabela representam um ponto no tempo, enquanto as colunas indicam variáveis numéricas de interesse. Nesse caso, faria sentido representarmos os nossos dados como objetos do tipo xts (Ryan and Ulrich 2020). O grande benefício dessa opção é que a agregação e a manipulação de variáveis em função do tempo é muito fácil. Por exemplo, podemos transformar dados de frequência diária para a frequência semanal com apenas uma linha de comando. Além disso, diversas outras funções reconhecem automaticamente que os dados são indexados ao tempo. Um exemplo é a criação de uma figura com esses dados. Nesse caso, o eixo horizontal da figura é automaticamente organizado com as datas.

Veja um caso a seguir, onde carregamos os dados anteriores como um objeto xts:

library(xts)

# set data
ticker <- c('ABEV3', 'BBAS3','BBDC3')

date <- as.Date(c('2010-01-01', '2010-01-04',
                  '2010-01-05', '2010-01-06'))

price_ABEV3 <- c(736.67, 764.14, 768.63, 776.47)
price_BBAS3 <- c(59.4, 59.8, 59.2, 59.28)
price_BBDC3 <- c(29.81, 30.82, 30.38, 30.20)

# build matrix
my_mat <- matrix(c(price_BBDC3, price_BBAS3, price_ABEV3),
                 nrow = length(date) )

# set xts object
my_xts <- xts(my_mat,
              order.by = date)

# set correct colnames
colnames(my_xts) <- ticker

# check it!
print(my_xts)
R>            ABEV3 BBAS3  BBDC3
R> 2010-01-01 29.81 59.40 736.67
R> 2010-01-04 30.82 59.80 764.14
R> 2010-01-05 30.38 59.20 768.63
R> 2010-01-06 30.20 59.28 776.47

O código anterior pode dar a impressão de que o objeto my_xts é semelhante a um dataframe, porém, não se engane. Por estar indexado a um vetor de tempo, objeto xts pode ser utilizado para uma série de procedimentos temporais, tal como uma agregação por período temporal. Veja o exemplo a seguir, onde agregamos duas variáveis de tempo através do cálculo de uma média a cada semana.

N <- 500

my_mat <- matrix(c(seq(1, N), seq(N, 1)), nrow=N)

my_xts <- xts(my_mat, order.by = as.Date('2016-01-01')+1:N)

my_xts.weekly.mean <- apply.weekly(my_xts, mean)

print(head(my_xts.weekly.mean))
R>             X.1   X.2
R> 2016-01-03  1.5 499.5
R> 2016-01-10  6.0 495.0
R> 2016-01-17 13.0 488.0
R> 2016-01-24 20.0 481.0
R> 2016-01-31 27.0 474.0
R> 2016-02-07 34.0 467.0

Em Finanças e Economia, as agregações com objetos xts são extremamente úteis quando se trabalha com dados em frequências de tempo diferentes. Por exemplo, é muito comum que se agregue dados de transação no mercado financeiro em alta frequência para intervalos maiores. Assim, dados que ocorrem a cada segundo são agregados para serem representados de 15 em 15 minutos. Esse tipo de procedimento é facilmente realizado no R através da correta representação dos dados como objetos xts. Existem diversas outras funcionalidades desse pacote. Encorajo os usuários a ler o manual e aprender o que pode ser feito.

Indo além, existem diversos outros tipos de dataframes customizados. Por exemplo, o dataframe proposto pelo pacote data.table (Dowle and Srinivasan 2021) prioriza o tempo de operação nos dados e o uso de uma notação compacta para acesso e processamento. O tibbletime (Vaughan and Dancho 2020) é uma versão orientada pelo tempo para tibbles. Caso o usuário esteja necessitando realizar operações de agregação de tempo, o uso deste pacote é fortemente recomendado.

6.1.10 Outras Funções Úteis

head - Retorna os primeiros n elementos de um dataframe.

my_df <- tibble(col1 = 1:5000, col2 = rep('a', 5000))
head(my_df, 5)
R> # A tibble: 5 x 2
R>    col1 col2 
R>   <int> <chr>
R> 1     1 a    
R> 2     2 a    
R> 3     3 a    
R> 4     4 a    
R> 5     5 a

tail - Retorna os últimos n elementos de um dataframe.

tail(my_df, 5)
R> # A tibble: 5 x 2
R>    col1 col2 
R>   <int> <chr>
R> 1  4996 a    
R> 2  4997 a    
R> 3  4998 a    
R> 4  4999 a    
R> 5  5000 a

complete.cases - Retorna um vetor lógico que testa se as linhas contêm apenas valores existentes e nenhum NA.

my_x <- c(1:5, NA, 10)
my_y <- c(5:10, NA)
my_df <- tibble(my_x, my_y)

print(my_df)
R> # A tibble: 7 x 2
R>    my_x  my_y
R>   <dbl> <int>
R> 1     1     5
R> 2     2     6
R> 3     3     7
R> 4     4     8
R> 5     5     9
R> 6    NA    10
R> 7    10    NA
R> [1]  TRUE  TRUE  TRUE  TRUE  TRUE FALSE FALSE
R> [1] 6 7

na.omit - Retorna um dataframe sem as linhas onde valores NA são encontrados.

print(na.omit(my_df))
R> # A tibble: 5 x 2
R>    my_x  my_y
R>   <dbl> <int>
R> 1     1     5
R> 2     2     6
R> 3     3     7
R> 4     4     8
R> 5     5     9

unique - Retorna um dataframe onde todas as linhas duplicadas são eliminadas e somente os casos únicos são mantidos.

my_df <- tibble(col1 = c(1,1,2,3,3,4,5),
                col2 = c('A','A','A','C','C','B','D'))

print(my_df)
R> # A tibble: 7 x 2
R>    col1 col2 
R>   <dbl> <chr>
R> 1     1 A    
R> 2     1 A    
R> 3     2 A    
R> 4     3 C    
R> 5     3 C    
R> 6     4 B    
R> 7     5 D
print(unique(my_df))
R> # A tibble: 5 x 2
R>    col1 col2 
R>   <dbl> <chr>
R> 1     1 A    
R> 2     2 A    
R> 3     3 C    
R> 4     4 B    
R> 5     5 D

6.2 Listas

Uma lista (list) é uma classe de objeto extremamente flexível e já tivemos contato com ela nos capítulos anteriores. Ao contrário de vetores atômicos, a lista não apresenta restrição alguma em relação aos tipos de elementos nela contidos. Podemos agrupar valores numéricos com caracteres, fatores com datas e até mesmo listas dentro de listas. Quando agrupamos vetores, também não é necessário que os mesmos tenham um número igual de elementos. Além disso, podemos dar um nome a cada elemento. Essas propriedades fazem da lista o objeto mais flexível para o armazenamento e estruturação de dados no R. Não é acidental o fato de que listas são muito utilizadas como retorno de funções.

6.2.1 Criando Listas

Uma lista pode ser criada através do comando base::list, seguido por seus elementos separados por vírgula:

library(dplyr)

# create list
my_l <- list(c(1, 2, 3),
             c('a', 'b'),
             factor('A', 'B', 'C'),
             tibble(col1 = 1:5))

# use base::print
print(my_l)
R> [[1]]
R> [1] 1 2 3
R> 
R> [[2]]
R> [1] "a" "b"
R> 
R> [[3]]
R> [1] <NA>
R> Levels: C
R> 
R> [[4]]
R> # A tibble: 5 x 1
R>    col1
R>   <int>
R> 1     1
R> 2     2
R> 3     3
R> 4     4
R> 5     5
# use dplyr::glimpse
glimpse(my_l)
R> List of 4
R>  $ : num [1:3] 1 2 3
R>  $ : chr [1:2] "a" "b"
R>  $ : Factor w/ 1 level "C": NA
R>  $ : tibble [5 × 1] (S3: tbl_df/tbl/data.frame)
R>   ..$ col1: int [1:5] 1 2 3 4 5

Note que juntamos no mesmo objeto um vetor atômico numérico, outro de texto, um fator e um tibble. A apresentação de listas com o comando print é diferente dos casos anteriores. Os elementos são separados verticalmente e os seus índices aparecem com duplo colchete ([[ ]]). Conforme será explicado logo a seguir, é dessa forma que os elementos de uma lista são armazenados e acessados.

Assim como para os demais tipos de objeto, os elementos de uma lista também podem ter nomes, o que facilita o entendimento e a interpretação das informações do problema em análise. Por exemplo, considere o caso de uma base de dados com informações sobre determinada ação negociada na bolsa. Nesse caso, podemos definir uma lista como:

# set named list
my_named_l <- list(ticker = 'TICK4',
                   market = 'Bovespa',
                   df_prices = tibble(P = c(1,1.5,2,2.3),
                                      ref_date = Sys.Date()+0:3))

# check content
glimpse(my_named_l)
R> List of 3
R>  $ ticker   : chr "TICK4"
R>  $ market   : chr "Bovespa"
R>  $ df_prices: tibble [4 × 2] (S3: tbl_df/tbl/data.frame)
R>   ..$ P       : num [1:4] 1 1.5 2 2.3
R>   ..$ ref_date: Date[1:4], format: "2021-03-02" ...

Toda vez que for trabalhar com listas, facilite a sua vida ao nomear todos os elementos de forma intuitiva. Isso facilita o acesso e evito possíveis erros no código.

6.2.2 Acessando os Elementos de uma Lista

Os elementos de uma lista podem ser acessados através do uso de duplo colchete ([[ ]]), tal como em:

# accessing elements from list
print(my_named_l[[2]])
R> [1] "Bovespa"
print(my_named_l[[3]])
R> # A tibble: 4 x 2
R>       P ref_date  
R>   <dbl> <date>    
R> 1   1   2021-03-02
R> 2   1.5 2021-03-03
R> 3   2   2021-03-04
R> 4   2.3 2021-03-05

Também é possível acessar os elementos com um colchete simples ([ ]), porém, tome cuidado com essa operação, pois o resultado não vai ser o objeto em si, mas uma outra lista. Esse é um equívoco muito fácil de passar despercebido, resultando em erros no código. Veja a seguir:

# set list
my_l <- list('a',
             c(1,2,3),
             factor('a','b'))

# check classes
class(my_l[[2]])
R> [1] "numeric"
class(my_l[2])
R> [1] "list"

Caso tentarmos somar um elemento a my_l[2], teremos uma mensagem de erro:

my_l[2] + 1
R> Error in my_l[2] + 1: non-numeric argument to binary operator

Esse erro ocorre devido ao fato de que uma lista não tem operador de soma. Para corrigir, basta utilizar o duplo colchete, tal como em my_l[[2]]+1. O acesso a elementos de uma lista com colchete simples somente é útil quando estamos procurando uma sublista dentro de uma lista maior. No exemplo anterior, caso quiséssemos obter o primeiro e o segundo elemento da lista my_l, usaríamos:

# set new list
my_new_l <- my_l[c(1,2)]

# check contents
print(my_new_l)
R> [[1]]
R> [1] "a"
R> 
R> [[2]]
R> [1] 1 2 3
class(my_new_l)
R> [1] "list"

No caso de listas com elementos nomeados, os mesmos podem ser acessados por seu nome através do uso do símbolo $ tal como em my_named_l$df_prices ou [['nome']], tal como em my_named_l[['df_prices']]. Em geral, essa é uma forma mais eficiente e recomendada de interagir com os elementos de uma lista. Como regra geral no uso do R, sempre dê preferência ao acesso de elementos através de seus nomes, seja em listas, vetores ou dataframes. Isso evita erros, pois, ao modificar os dados e adicionar algum outro objeto na lista, é possível que o ordenamento interno mude e, portanto, a posição de determinado objeto pode acabar sendo modificada.

Saiba que a ferramenta de autocomplete do RStudio também funciona para listas. Para usar, digite o nome da lista seguido de $ e aperte tab. Uma caixa de diálogo com todos os elementos disponíveis na lista irá aparecer. A partir disso, basta selecionar apertando enter.

Veja os exemplos a seguir, onde são apresentadas as diferentes formas de se acessar uma lista.

# different ways to access a list
my_named_l$ticker
my_named_l$price
my_named_l[['ticker']]
my_named_l[['price']]

Vale salientar que também é possível acessar diretamente os elementos de um vetor que esteja dentro de uma lista através de colchetes encadeados. Veja a seguir:

# accessing elements of a vector in a list
my_l <- list(c(1,2,3),
             c('a', 'b'))

print(my_l[[1]][2])
R> [1] 2
print(my_l[[2]][1])
R> [1] "a"

Tal operação é bastante útil quando interessa apenas um elemento dentro de um objeto maior criado por alguma função.

6.2.3 Adicionando e Removendo Elementos de uma Lista

A remoção, adição e substituição de elementos de uma lista também são procedimentos fáceis. Para adicionar ou substituir, basta definir um novo objeto na posição desejada da lista:

# set list
my_l <- list('a', 1, 3)
glimpse(my_l)
R> List of 3
R>  $ : chr "a"
R>  $ : num 1
R>  $ : num 3
# add new elements to list
my_l[[4]] <- c(1:5)
my_l[[2]] <- c('b')

# print result
glimpse(my_l)
R> List of 4
R>  $ : chr "a"
R>  $ : chr "b"
R>  $ : num 3
R>  $ : int [1:5] 1 2 3 4 5

A operação também é possível com o uso de nomes e operador $:

# set list
my_l <- list(elem1 = 'a', name1=5)

# set new element
my_l$name2 <- 10
glimpse(my_l)
R> List of 3
R>  $ elem1: chr "a"
R>  $ name1: num 5
R>  $ name2: num 10

Para remover elementos de uma lista, basta definir o elemento para o símbolo reservado NULL (nulo):

# set list
my_l <- list(text = 'b', num1 = 2, num2 = 4)
glimpse(my_l)
R> List of 3
R>  $ text: chr "b"
R>  $ num1: num 2
R>  $ num2: num 4
# remove elements
my_l[[3]] <- NULL
glimpse(my_l)
R> List of 2
R>  $ text: chr "b"
R>  $ num1: num 2
my_l$num1 <- NULL
glimpse(my_l)
R> List of 1
R>  $ text: chr "b"

Outra maneira de retirar elementos de uma lista é utilizando um índice negativo para os elementos indesejados. Observe a seguir, onde eliminamos o segundo elemento de uma lista:

# set list
my_l <- list(a = 1, b = 'texto')

# remove second element
glimpse(my_l[[-2]])
R>  num 1

Assim como no caso de vetores atômicos, essa remoção também pode ser realizada por condições lógicas. Veja a seguir:

# set list
my_l <- list(1, 2, 3, 4)

# remove elements by condition
my_l[my_l > 2] <- NULL
glimpse(my_l)
R> List of 2
R>  $ : num 1
R>  $ : num 2

Porém, note que esse atalho só funciona porque todos os elementos de my_l são numéricos.

6.2.4 Processando os Elementos de uma Lista

Um ponto importante a ser destacado a respeito de listas é que os seus elementos podem ser processados e manipulados individualmente através de funções específicas. Este é um tópico particular de programação com o R, mas que vale a apresentação aqui.

Por exemplo, imagine uma lista com vetores numéricos de diferentes tamanhos, tal como a seguir:

# set list
my_l_num <- list(c(1, 2, 3),
                 seq(1:50),
                 seq(-5, 5, by = 0.5))

Caso quiséssemos calcular a média de cada elemento de my_l_num e apresentar o resultado na tela como um vetor, poderíamos fazer isso através de um procedimento simples, processando cada elemento individualmente:

# calculate mean of vectors
mean_1 <- mean(my_l_num[[1]])
mean_2 <- mean(my_l_num[[2]])
mean_3 <- mean(my_l_num[[3]])

# print it
print(c(mean_1, mean_2, mean_3))
R> [1]  2.0 25.5  0.0

O código anterior funciona, porém não é recomendado devido sua falta de escabilidade. Isto é, caso aumentássemos o volume de dados ou objetos, o código não funcionaria corretamente. Se, por exemplo, tivéssemos um quarto elemento em my_l_num e quiséssemos manter essa estrutura do código, teríamos que adicionar uma nova linha mean_4 <- mean(my_l_num[[4]]) e modificar o comando de saída na tela para print(c(mean_1, mean_2, mean_3, mean_4)).

Uma maneira mais fácil, elegante e inteligente seria utilizar a função sapply. Nela, basta indicar o nome do objeto de tipo lista e a função que queremos utilizar para processar cada elemento. Internamente, os cálculos são realizados automaticamente. Veja a seguir:

# using sapply
my_mean <- sapply(my_l_num, mean)

# print result
print(my_mean)
R> [1]  2.0 25.5  0.0

O uso da função sapply é preferível por ser mais compacto e eficiente do que a alternativa – a criação de mean_1, mean_2 e mean_3. Note que o primeiro código, com médias individuais, só funciona para uma lista com três elementos. A função sapply, ao contrário, funcionaria da mesma forma em listas de qualquer tamanho. Caso tivéssemos mais elementos, nenhuma modificação seria necessária no código anterior, o que o torna extensível a chegada de novos dados.

Essa visão e implementação de código voltado a procedimentos genéricos é um dos lemas para tornar o uso do R mais eficiente. A regra é simples: sempre escreva códigos que sejam adaptáveis a chegada de novos dados. Em inglês, isso é chamado de regra DRY (don’t repeat yourself). Caso você esteja repetindo códigos e abusando do control + c/control + v, como no exemplo anterior, certamente existe uma solução mais elegante e flexível que poderia ser utilizada. No R, existem diversas outras funções da família apply para esse objetivo. Essas funções serão explicadas com maiores detalhes no capítulo 8.

6.2.5 Outras Funções Úteis

unlist - Retorna os elementos de uma lista em um único vetor atômico.

my_named_l <- list(ticker = 'XXXX4',
                   price = c(1,1.5,2,3),
                   market = 'Bovespa')
my_unlisted <- unlist(my_named_l)
print(my_unlisted)
R>    ticker    price1    price2    price3    price4    market 
R>   "XXXX4"       "1"     "1.5"       "2"       "3" "Bovespa"
class(my_unlisted)
R> [1] "character"

as.list - Converte um objeto para uma lista, tornando cada elemento um elemento da lista.

my_x <- 10:13
my_x_as_list <- as.list(my_x)
print(my_x_as_list)
R> [[1]]
R> [1] 10
R> 
R> [[2]]
R> [1] 11
R> 
R> [[3]]
R> [1] 12
R> 
R> [[4]]
R> [1] 13

names - Retorna ou define os nomes dos elementos de uma lista. Assim como para o caso de nomear elementos de um vetor atômico, usa-se a função names alocada ao lado esquerdo do símbolo <-.

my_l <- list(value1 = 1, value2 = 2, value3 = 3)
print(names(my_l))
R> [1] "value1" "value2" "value3"
my_l <- list(1,2,3)
names(my_l) <- c('num1', 'num2', 'num3')
print(my_l)
R> $num1
R> [1] 1
R> 
R> $num2
R> [1] 2
R> 
R> $num3
R> [1] 3

6.3 Matrizes

Como você deve lembrar das aulas de matemática, uma matriz é uma representação bidimensional de diversos valores, arranjados em linhas e colunas. O uso de matrizes é uma poderosa maneira de representar dados numéricos em duas dimensões e, em certos casos, funções matriciais podem simplificar operações matemáticas complexas.

No R, matrizes são objetos com duas dimensões, onde todos os elementos devem ser da mesma classe. Além disso, as linhas e colunas também podem ter nomes. Assim como para listas e dataframes, isso facilita a interpretação e contextualização dos dados.

Um claro exemplo do uso de matrizes em Finanças seria a representação dos preços de diferentes ações ao longo do tempo. Nesse caso, teríamos as linhas representando as diferentes datas e as colunas representando cada ativo, tal como a seguir:

Data ABEV3 BBAS3 BBDC3
2010-01-01 736.67 59.4 29.81
2010-01-04 764.14 59.8 30.82
2010-01-05 768.63 59.2 30.38
2010-01-06 776.47 59.28 30.20

A matriz anterior poderia ser criada da seguinte forma no R:

# create matrix
data <- c(736.67, 764.14, 768.63, 776.47,
          59.4, 59.8, 59.2, 59.28,
          29.81, 30.82, 30.38, 30.20)

my_mat <- matrix(data, nrow = 4, ncol = 3)

# set names of cols and rows
colnames(my_mat) <- c('ABEV3', 'BBAS3', 'BBDC3')
rownames(my_mat) <- c('2010-01-01', '2010-01-04',
                      '2010-01-05', '2010-01-06')

print(my_mat)
R>             ABEV3 BBAS3 BBDC3
R> 2010-01-01 736.67 59.40 29.81
R> 2010-01-04 764.14 59.80 30.82
R> 2010-01-05 768.63 59.20 30.38
R> 2010-01-06 776.47 59.28 30.20

Observe que, na construção da matriz, definimos o número de linhas e colunas explicitamente com os argumentos nrow = 4 e ncol = 3. Já os nomes das linhas e colunas são definidos pelos comandos colnames e rownames. Destaca-se, novamente, que a forma de utilizá-los é bastante distinta das demais funções do R. Nesse caso, utilizamos a função ao lado esquerdo do símbolo de assign (<-). Poderíamos, porém, recuperar os nomes das linhas e colunas com as mesmas funções. Veja a seguir:

colnames(my_mat)
R> [1] "ABEV3" "BBAS3" "BBDC3"
rownames(my_mat)
R> [1] "2010-01-01" "2010-01-04" "2010-01-05" "2010-01-06"

No momento em que temos essa nossa matriz criada, podemos utilizar todas as suas propriedades numéricas. Um exemplo simples é o cálculo do valor de um portfólio de investimento ao longo do tempo. Caso um investidor tenha 200 ações da ABEV3, 300 da BBAS3 e 100 da BBDC3, o valor do seu portfólio ao longo do tempo poderá ser calculado assim:

\[V _t = \sum _{i=1} ^{3} N _i P_{i,t}\]

Onde Ni é o número de ações compradas para ativo i e Pit é o preço da ação i na data t. Essa é uma operação bastante simples de ser efetuada com uma multiplicação matricial. Traduzindo o procedimento para o R, temos:

my.w <- as.matrix(c(200, 300, 100), nrow = 3)
my_port <- my_mat %*% my.w
print(my_port)
R>              [,1]
R> 2010-01-01 168135
R> 2010-01-04 173850
R> 2010-01-05 174524
R> 2010-01-06 176098

Nesse último exemplo, utilizamos o símbolo %*%, o qual permite a multiplicação matricial entre os objetos. O objeto my_port indica o valor desse portfólio ao longo das datas, resultando em um leve lucro para o investidor.

Um ponto importante a ressaltar é que uma matriz no R não precisa necessariamente ser composta por valores. É possível, também, criar matrizes de caracteres (texto). Veja o exemplo a seguir:

my_mat_char <- matrix(rep(c('a','b','c'), 3) ,
                      nrow = 3,
                      ncol = 3)
print(my_mat_char)
R>      [,1] [,2] [,3]
R> [1,] "a"  "a"  "a" 
R> [2,] "b"  "b"  "b" 
R> [3,] "c"  "c"  "c"

Essa flexibilidade dos objetos matriciais possibilita a fácil representação e visualização de seus dados em casos específicos.

6.3.1 Selecionando Valores de uma Matriz

Tal como no caso dos vetores atômicos, também é possível selecionar pedaços de uma matriz através de indexação. Uma diferença aqui é que os objetos de matrizes possuem duas dimensões, enquanto vetores possuem apenas uma.22 Essa dimensão extra requer a indexação não apenas de linhas mas também de colunas. Os elementos de uma matriz podem ser acessados pela notação [i,j], onde i representa a linha e j a coluna. Veja o exemplo a seguir:

my_mat <- matrix(1:9, nrow = 3)
print(my_mat)
R>      [,1] [,2] [,3]
R> [1,]    1    4    7
R> [2,]    2    5    8
R> [3,]    3    6    9
print(my_mat[1,2])
R> [1] 4

Para selecionar colunas ou linhas inteiras, basta deixar o índice vazio, tal como no exemplo a seguir:

print(my_mat[ , 2])
R> [1] 4 5 6
print(my_mat[1, ])
R> [1] 1 4 7

Observe que a indexação retorna um vetor atômico da classe dos dados. Caso quiséssemos que o pedaço da matriz mantivesse a sua classe e orientação vertical ou horizontal, poderíamos forçar essa conversão pelo uso de matrix:

print(as.matrix(my_mat[ ,2]))
R>      [,1]
R> [1,]    4
R> [2,]    5
R> [3,]    6
print(matrix(my_mat[1, ], nrow=1))
R>      [,1] [,2] [,3]
R> [1,]    1    4    7

Pedaços da matriz também podem ser selecionados via indexadores. Caso quiséssemos uma matriz formada a partir da seleção dos elementos da segunda linha e primeira coluna até a terceira linha e segunda coluna, usaríamos o seguinte código:

print(my_mat[2:3,1:2])
R>      [,1] [,2]
R> [1,]    2    5
R> [2,]    3    6

Por fim, o uso de testes lógicos para selecionar valores de matrizes também é possível. Veja a seguir:

my_mat <- matrix(1:9, nrow = 3)
print(my_mat >5)
R>       [,1]  [,2] [,3]
R> [1,] FALSE FALSE TRUE
R> [2,] FALSE FALSE TRUE
R> [3,] FALSE  TRUE TRUE
print(my_mat[my_mat >5])
R> [1] 6 7 8 9

6.3.2 Outras Funções Úteis

as.matrix - Transforma dados para a classe matrix.

my_mat <- as.matrix(1:5)
print(my_mat)
R>      [,1]
R> [1,]    1
R> [2,]    2
R> [3,]    3
R> [4,]    4
R> [5,]    5

t - Retorna a transposta da matriz.

my_mat <- matrix(seq(10,20, length.out = 6), nrow = 3)
print(my_mat)
R>      [,1] [,2]
R> [1,]   10   16
R> [2,]   12   18
R> [3,]   14   20
print(t(my_mat))
R>      [,1] [,2] [,3]
R> [1,]   10   12   14
R> [2,]   16   18   20

rbind - Retorna a junção (cola) vertical de matrizes, orientando-se pelas linhas.

my_mat_1 <- matrix(1:5, nrow = 1)
print(my_mat_1)
R>      [,1] [,2] [,3] [,4] [,5]
R> [1,]    1    2    3    4    5
my_mat_2 <- matrix(10:14, nrow = 1)
print(my_mat_2)
R>      [,1] [,2] [,3] [,4] [,5]
R> [1,]   10   11   12   13   14
my.rbind.mat <- rbind(my_mat_1, my_mat_2)
print(my.rbind.mat)
R>      [,1] [,2] [,3] [,4] [,5]
R> [1,]    1    2    3    4    5
R> [2,]   10   11   12   13   14

cbind - Retorna a junção (cola) horizontal de matrizes, orientando-se pelas colunas.

my_mat_1 <- matrix(1:4, nrow = 2)
print(my_mat_1)
R>      [,1] [,2]
R> [1,]    1    3
R> [2,]    2    4
my_mat_2 <- matrix(10:13, nrow = 2)
print(my_mat_2)
R>      [,1] [,2]
R> [1,]   10   12
R> [2,]   11   13
my_cbind_mat <- cbind(my_mat_1, my_mat_2)
print(my_cbind_mat)
R>      [,1] [,2] [,3] [,4]
R> [1,]    1    3   10   12
R> [2,]    2    4   11   13

6.4 Exercícios


Q.1

Utilizando função dplyr::tibble, crie um dataframe chamado my_df com uma coluna chamada x contendo uma sequência de -100 a 100 e outra coluna chamada y com o valor da coluna x adicionada de 5. Para a tabela my_df, qual a quantidade de valores na coluna x maiores que 10 e menores que 25?



Resposta:

A solução é 14. Para chegar no resultado anterior, deves executar o código abaixo. Para isso, abra um novo script no RStudio (Control+shift+N), copie e cole o código, e rode o script inteiro apertando Control+Shift+Enter ou por linha com Control+Enter.
my_df <- dplyr::tibble(x = -100:100, 
                        y = x + 5)
# solution
my_sol <- sum((my_df$x > 10)&(my_df$x < 25))

Q.2

Crie uma nova coluna em my_df chamada cumsum_x, contendo a soma cumulativa de x (função cumsum). Desta nova coluna, quantos valores são maiores que -3500?



Resposta:

A solução é 89. Para chegar no resultado anterior, deves executar o código abaixo. Para isso, abra um novo script no RStudio (Control+shift+N), copie e cole o código, e rode o script inteiro apertando Control+Shift+Enter ou por linha com Control+Enter.
my_df <- dplyr::tibble(x = -100:100, 
                       y = x + 5)

# solution
my_df$cumsum_x <- cumsum(my_df$x)

# solution
my_sol <- sum(my_df$cumsum_x > -3500)

Q.3

Use função dplyr::filter e o operador de pipeline para filtrar my_df, mantendo apenas as linhas onde o valor da coluna y é maior que 0. Qual o número de linhas da tabela resultante?



Resposta:

A solução é 105. Para chegar no resultado anterior, deves executar o código abaixo. Para isso, abra um novo script no RStudio (Control+shift+N), copie e cole o código, e rode o script inteiro apertando Control+Shift+Enter ou por linha com Control+Enter.
my_df <- dplyr::tibble(x = -100:100, 
                       y = x + 5)

# solution
library(tidyverse)

my_df2 <- my_df %>%
  filter(y > 0)

# solution
my_sol <- nrow(my_df2)

Q.4

Caso não o tenha feito, repita os exercícios 1, 2 e 3 utilizando as funções do tidyverse e o operador de pipeline.


Q.5

Utilize pacote BatchGetSymbols para baixar dados da ação do Facebook (FB), de 2010-01-01 até 2020-12-31. Caso o investidor tivesse comprado 1000 USD em ações do Facebook no primeiro dia dos dados e mantivesse o investimento até hoje, qual seria o valor do seu portfolio?


Resposta:

A solução é R$7.111. Para chegar no resultado anterior, deves executar o código abaixo. Para isso, abra um novo script no RStudio (Control+shift+N), copie e cole o código, e rode o script inteiro apertando Control+Shift+Enter ou por linha com Control+Enter.

Q.6

Use função adfeR::get_data_file e readr::read_csv para importar os dados do arquivo grunfeld.csv. Em seguida, utilize funções dplyr::glimpse para descobrir o número de linhas nos dados importados.



Resposta:

A solução é 200. Para chegar no resultado anterior, deves executar o código abaixo. Para isso, abra um novo script no RStudio (Control+shift+N), copie e cole o código, e rode o script inteiro apertando Control+Shift+Enter ou por linha com Control+Enter.
my_file <- adfeR::get_data_file('grunfeld.csv')

df_grunfeld <- read_csv(my_file, 
                        col_types = cols())

# solution
glimpse(df_grunfeld)

my_sol <- nrow(df_grunfeld)

Q.7

Crie um objeto do tipo lista com três dataframes em seu conteúdo, df1, df2 e df3. O conteúdo e tamanho dos dataframes é livre. Utilize função sapply para descobrir o número de linhas e colunas em cada dataframe.

A solução pode ser encontrada pelo código abaixo. Para rodar, abra um novo script no RStudio (Control+shift+N), copie e cole o código, e rode o script apertando Control+Shift+Enter.
df1 <- dplyr::tibble(x = 1:10)
df2 <- dplyr::tibble(y = runif(100))
df3 <- dplyr::tibble(z = rnorm(150),
                     m = rnorm(150))

my_l <- list(df1, df2, df3)

my_fct <- function(df_in) {
  out <- c('nrows' = nrow(df_in), 
           'ncols' = ncol(df_in))
  return(out)
}

tab <- sapply(my_l, my_fct)
tab

Q.8

Utilizando o R, crie uma matrix identidade (valor 1 na diagonal, zero em qualquer outro) de tamanho 3X3. Dica: use função diag para definir a diagonal da matrix.

A solução pode ser encontrada pelo código abaixo. Para rodar, abra um novo script no RStudio (Control+shift+N), copie e cole o código, e rode o script apertando Control+Shift+Enter.
my_size <- 3
M_identity <- matrix(0, 
                     nrow = my_size, 
                     ncol = my_size)

# solution
diag(M_identity) <- 1

print(M_identity)