A Gramática do R

Programação
Referência guiada
Uma referência completa sobre objectos, tipos, vectores, funções, pipes, pacotes e controlo do fluxo.
Autores
Afiliações

João O. Santos

ISPA

Cristina Mendonça

ISPA; WJCR

Vitória Melita

FP-UL

Mariona Pascual Peñas

UIB

João Raposo

ISPA

Marta Barros

FP-UL

0 A Gramática do R

No tutorial anterior começámos a aprender algumas palavras em R. Aqui vamos aprender um pouco mais da gramática: como se guardam valores, como se combinam, como se fazem perguntas aos dados e, sobretudo, como se lê uma chamada a uma função sem ela parecer uma fórmula secreta.

Esta página é deliberadamente comprida. Não foi feita para ser memorizada nem para ter de ser lida do princípio ao fim duma só vez. É também uma página de referência. Quando encontrar num script um factor(), um |>, um [ ou uma função com argumentos que já não se lembra de como ler, volte à secção correspondente.

Se está a começar, vale a pena perceber primeiro estas partes:

As secções sobre a família apply, ciclos e controlo do fluxo podem esperar. Estão aqui porque mais tarde será útil ter um sítio onde voltar a procurá-las.

Tal como no tutorial anterior, os comentários dentro do código estão em português. A partir do próximo tutorial passam para inglês. É uma transição deliberada. Primeiro queremos que a leitura da sintaxe do R seja a única novidade. Depois começamos também a ganhar o hábito de escrever comentários que sejam fáceis de partilhar em documentação e projectos internacionais.

Variáveis, Nomes e Objectos

A palavra variável tem dois usos importantes nestes materiais. Em ambos está a ideia de algo que pode assumir valores diferentes, mas o papel do termo não é o mesmo em estatística e em programação.

Em estatística, uma variável é uma característica que pode variar entre unidades ou observações e que costuma ter um papel na pergunta ou no delineamento do estudo. Idade, temperatura, condição experimental, variável resposta e preditor são exemplos deste uso.

Em programação, também se chama variável a um nome ao qual atribuímos um valor e cujo valor pode mudar durante a execução. No R, falamos muito em objectos: os valores e estruturas que existem na sessão são objectos, e os nomes permitem-nos referir esses objectos. Assim, noutros textos de programação poderá encontrar variável onde aqui dizemos objecto ou nome de objecto. Os termos estão relacionados, mas não são perfeitamente sinónimos.

Para evitar misturar os dois sentidos enquanto aprendemos estatística e R ao mesmo tempo, no NRB vamos preferir variável quando falamos do estudo e objecto ou nome de objecto quando falamos de programação. Isto é uma convenção pedagógica, não uma regra da informática. Um objecto do R pode guardar uma variável estatística, mas também pode guardar um modelo, um resultado, uma função ou qualquer outra coisa que não tenha um papel no delineamento do estudo.

Criar e Alterar Objectos

O estilo usado no NRB para atribuição é <-. À esquerda fica o nome do objecto e à direita o valor que queremos guardar.

nome <- "Ana"
idade <- 23
resultado <- 3 + 2

Para ver o valor, basta escrever o nome do objecto:

nome
[1] "Ana"
idade
[1] 23
resultado
[1] 5

Também podemos usar print() explicitamente:

print(resultado)
[1] 5

Se voltarmos a atribuir um valor ao mesmo nome, o valor anterior é substituído.

idade <- 23
idade
[1] 23
idade <- 24
idade
[1] 24

Isto é útil, mas convém pensar antes de voltar a guardar um valor com o mesmo nome quando queremos conservar o anterior. Se quiser comparar o antes e o depois, crie um novo objecto.

idade_original <- idade
idade_no_proximo_ano <- idade + 1

O R também permite usar = para atribuição em muitos contextos. Neste site preferimos <-, porque = aparece muito frequentemente dentro das chamadas a funções para dar valores a argumentos nomeados:

media <- mean(x = dados, na.rm = TRUE)

As duas utilizações são válidas, mas <- torna visualmente diferentes a atribuição dum objecto e a especificação dum argumento.

Nomes Que se Conseguem Ler Depois

Um bom nome diz-nos o que está guardado sem precisarmos de procurar cinco linhas acima. Preferimos snake_case:

media_idade <- 31.4
tempo_resposta_ms <- 812
condicao_experimental <- TRUE

Evite espaços e sinais com significado próprio no R. Um nome comum também não pode começar por um algarismo.

# Bom exemplo
idade_1 <- 23
base_de_dados <- "dados.csv"

# Estes não são nomes sintacticamente normais
# 1idade <- 23
# base de dados <- "dados.csv"

O R moderno aceita muitos caracteres Unicode nos nomes, incluindo letras com acentos. Mesmo assim, o estilo do NRB prefere nomes ASCII em snake_case, como media_idade, porque tendem a ser mais portáveis e mais fáceis de procurar e partilhar entre ferramentas. O texto guardado nos objectos pode, naturalmente, ter acentos, espaços e pontuação.

mensagem <- "O texto pode ter acentos, espaços e pontuação."
mensagem
[1] "O texto pode ter acentos, espaços e pontuação."

Evite ainda reutilizar nomes reservados como NA, TRUE, FALSE e NULL.

Classes de Dados

O tipo de informação que um objecto guarda determina o que faz sentido fazer com ele. Podemos multiplicar números. Não faz sentido multiplicar a palavra "azul" por três.

Para a maior parte do trabalho neste curso, a pergunta prática é «que classe tem este objecto?». Podemos respondê-la com class().

numero <- 3.5
texto <- "3.5"
verdadeiro <- TRUE

class(numero)
[1] "numeric"
class(texto)
[1] "character"
class(verdadeiro)
[1] "logical"

numeric: Números

Os números que escrevemos normalmente são numeric.

inteiro <- 20
negativo <- -3
decimal <- 0.2

class(inteiro)
[1] "numeric"
class(negativo)
[1] "numeric"
class(decimal)
[1] "numeric"

Com eles podemos fazer as operações matemáticas habituais:

inteiro + 2
[1] 22
inteiro * 3
[1] 60
inteiro / 4
[1] 5
inteiro^2
[1] 400
sqrt(inteiro)
[1] 4.472136

character: Texto

Texto fica entre aspas e pertence à classe character. Uma sequência de caracteres também é frequentemente chamada string.

letra <- "A"
palavra <- "livro"
frase <- "Esta string contém várias palavras."

class(letra)
[1] "character"
class(palavra)
[1] "character"
class(frase)
[1] "character"

Repare na diferença entre o número 3.5 e o texto "3.5":

class(3.5)
[1] "numeric"
class("3.5")
[1] "character"

O segundo parece um número a quem o lê, mas para o R continua a ser texto. Essa diferença aparece muitas vezes depois de importarmos dados.

logical: Verdadeiro e Falso

Um valor lógico pode ser TRUE ou FALSE. Estes valores aparecem sempre que fazemos uma comparação.

10 > 3
[1] TRUE
10 == 3
[1] FALSE
10 != 3
[1] TRUE

Os operadores mais comuns são:

Operador Pergunta
== é igual?
!= é diferente?
> é maior?
< é menor?
>= é maior ou igual?
<= é menor ou igual?
! negar
& E
| OU

& exige que as duas condições sejam verdadeiras. | exige que pelo menos uma seja verdadeira.

idade <- 21
tem_consentimento <- TRUE

idade >= 18 & tem_consentimento
[1] TRUE
idade < 18 | !tem_consentimento
[1] FALSE

Se isto ainda parecer um pouco abstracto, retenha apenas a ideia de que uma comparação devolve TRUE ou FALSE. Na secção de indexação lógica vamos usar esses valores para escolher observações.

O R reconhece T e F como abreviaturas, mas esses nomes podem ser redefinidos:

T <- FALSE
F <- TRUE

TRUE e FALSE são reservados e não sofrem esse problema. Por isso, escreva-os por extenso.

factor: Categorias com Níveis

Um factor é particularmente útil em análise de dados porque representa uma variável categórica com um conjunto de níveis.

condicao <- factor(
  c("controlo", "experimental", "controlo"),
  levels = c("controlo", "experimental")
)

class(condicao)
[1] "factor"
levels(condicao)
[1] "controlo"     "experimental"
condicao
[1] controlo     experimental controlo    
Levels: controlo experimental

Ao princípio, a diferença entre character e factor pode não parecer muito importante. Torna-se clara quando começarmos a ajustar modelos: o R usa os níveis do factor para representar os grupos e construir contrastes.

Também podemos definir uma ordem quando as categorias têm uma ordem substantiva:

stress <- factor(
  c("baixo", "alto", "medio"),
  levels = c("baixo", "medio", "alto"),
  ordered = TRUE
)
stress
[1] baixo alto  medio
Levels: baixo < medio < alto
AvisoConverter Factores para Números

Há uma armadilha importante que vale a pena conhecer cedo. Um factor tem níveis internos. as.numeric() aplicado directamente pode devolver os códigos desses níveis em vez dos números escritos nas etiquetas.

f <- factor(c("10", "20", "10"))
as.numeric(f)
[1] 1 2 1
[1] 10 20 10

Por isso, antes de converter um factor, veja os níveis e confirme o significado dos valores.

Valores em Falta: NA

NA representa um valor em falta. Não é zero e não é o texto "NA".

idades <- c(21, 24, NA, 31)
idades
[1] 21 24 NA 31
is.na(idades)
[1] FALSE FALSE  TRUE FALSE

Muitas funções devolvem NA quando os dados contêm valores em falta, a menos que lhes digamos explicitamente como os tratar.

mean(idades)
[1] NA
mean(idades, na.rm = TRUE)
[1] 25.33333

na.rm = TRUE significa «remover os NA para esta operação». Não significa que os valores em falta desapareceram do objecto original.

Para aprender a analisar dados, class() é normalmente a pergunta mais útil. typeof() descreve a representação interna dum objecto e pode dar uma resposta diferente.

x <- factor(c("A", "B"))
class(x)
typeof(x)

Também existe um tipo inteiro explícito, escrito com L:

class(3)
class(3L)
typeof(3)
typeof(3L)

E NULL representa a ausência dum objecto/componente, não um valor em falta dentro dum vector:

length(NULL)

Estas distinções são importantes em programação mais avançada, mas não são pré-requisitos para compreender o resto desta página. Quando estiver em dúvida num script de análise, comece por class() e por olhar para os valores.

Vectores e Indexação

Um vector é uma sequência de valores. As colunas das nossas bases de dados são, na prática, muitas vezes vectores: um vector de idades, um vector de respostas, um vector de grupos.

Criamos um vector com c():

idade <- c(21, 32, 24, 46, 33)
genero <- c("M", "F", "M", "F", "F")
rts <- c(1435, 4352, 2232, 2820, 4552)

Num vector atómico, os elementos partilham um tipo comum. Se combinarmos números e texto, o R converte os números para texto para conseguir guardar tudo no mesmo vector.

mistura <- c(1, 2, "tres")
mistura
[1] "1"    "2"    "tres"
class(mistura)
[1] "character"

Escolher Posições

O R começa a contar posições em 1.

idade[1]
[1] 21
rts[3]
[1] 2232

Podemos pedir um intervalo de posições:

idade[2:4]
[1] 32 24 46

Ou posições específicas:

rts[c(1, 3, 5)]
[1] 1435 2232 4552

Também podemos excluir posições:

idade[-2]
[1] 21 24 46 33

Indexação Lógica

Aqui os valores TRUE e FALSE começam a mostrar para que servem. Uma comparação feita a um vector devolve uma resposta lógica para cada elemento.

idade >= 30
[1] FALSE  TRUE FALSE  TRUE  TRUE

Podemos usar esse resultado dentro dos parêntesis rectos. O R conserva as posições TRUE e deixa de fora as FALSE.

idade[idade >= 30]
[1] 32 46 33

Podemos combinar condições:

idade[idade >= 20 & idade < 40]
[1] 21 32 24 33

E procurar elementos que pertençam a um conjunto com %in%:

genero %in% c("F", "Outro")
[1] FALSE  TRUE FALSE  TRUE  TRUE
genero[genero %in% c("F", "Outro")]
[1] "F" "F" "F"

Se houver valores em falta, escreva a regra explicitamente com is.na().

idade_com_na <- c(21, NA, 35, 17)
idade_com_na[!is.na(idade_com_na) & idade_com_na >= 18]
[1] 21 35

Este padrão é central no R: fazemos uma pergunta que devolve TRUE/FALSE e usamos essa resposta para seleccionar valores.

Listas

Uma lista é diferente dum vector atómico: consegue guardar objectos de classes e tamanhos diferentes.

coisas <- list(
  nome = "Ana",
  idades = c(21, 24, 30),
  aprovado = TRUE
)

coisas
$nome
[1] "Ana"

$idades
[1] 21 24 30

$aprovado
[1] TRUE
coisas$nome
[1] "Ana"
coisas$idades
[1] 21 24 30

Não precisa de dominar listas já. Elas tornam-se importantes porque muitas funções devolvem resultados complexos sob a forma de listas e porque lapply() trabalha naturalmente com elas.

O R consegue repetir um vector mais curto numa operação:

c(1, 2, 3, 4) + c(10, 20)

Isto chama-se reciclagem. Pode ser útil, mas também pode esconder erros se os comprimentos não forem os que esperávamos. Quando os comprimentos não são múltiplos compatíveis, o R costuma produzir um aviso:

c(1, 2, 3, 4, 5) + c(10, 20)

Não é preciso usar esta regra deliberadamente para seguir os tutoriais. É útil reconhecê-la quando aparecer num aviso ou num resultado inesperado.

Uma Primeira Referência a data.frames

Um data.frame é uma tabela: cada coluna pode ter uma classe diferente, mas as colunas têm o mesmo número de linhas.

dados <- data.frame(
  idade = idade,
  genero = genero,
  rts = rts
)

dados
idade genero rts
21 M 1435
32 F 4352
24 M 2232
46 F 2820
33 F 4552

Podemos inspeccionar a estrutura e as dimensões:

head(dados)
idade genero rts
21 M 1435
32 F 4352
24 M 2232
46 F 2820
33 F 4552
str(dados)
'data.frame':   5 obs. of  3 variables:
 $ idade : num  21 32 24 46 33
 $ genero: chr  "M" "F" "M" "F" ...
 $ rts   : num  1435 4352 2232 2820 4552
nrow(dados)
[1] 5
ncol(dados)
[1] 3
dim(dados)
[1] 5 3
names(dados)
[1] "idade"  "genero" "rts"   

A notação [linhas, colunas] é uma extensão natural da indexação que já vimos:

dados[1, ]
idade genero rts
21 M 1435
dados[, 2]
[1] "M" "F" "M" "F" "F"
dados[1:3, c("idade", "rts")]
idade rts
21 1435
32 4352
24 2232
dados$idade
[1] 21 32 24 46 33

Há muito mais a dizer sobre tabelas, selecção e combinação de dados. Isso fica na referência dedicada aos data.frames. Aqui basta reter que os mesmos princípios de vectores, índices e operações lógicas vão reaparecer quando trabalharmos com colunas e linhas.

Funções e Argumentos

As funções são uma das partes mais importantes da gramática do R. Podemos pensar numa função como uma receita: recebe argumentos e produz um resultado.

Já usámos várias:

mean(c(2, 4, 6))
[1] 4
sd(c(2, 4, 6))
[1] 2
round(3.14159, digits = 2)
[1] 3.14

Saber ler estas chamadas é mais importante, no início, do que saber escrever funções nossas. Mesmo assim, vamos criar algumas muito simples porque isso ajuda a perceber o que está dentro da caixa.

Uma Função sem Argumentos

hello <- function() {
  return("Hello world!")
}

hello()
[1] "Hello world!"

A função chama-se hello porque guardámos uma função num objecto com esse nome. Os parêntesis vazios dizem-nos que não espera argumentos.

Se tentarmos dar-lhe um argumento, o R protesta:

hello("Ana")
Error in `hello()`:
! unused argument ("Ana")

Um Argumento Obrigatório

x_menos_um <- function(x) {
  resultado <- x - 1
  return(resultado)
}

x_menos_um(10)
[1] 9
x_menos_um(x = 10)
[1] 9

x é obrigatório: a função precisa dum valor a que possa subtrair 1.

x_menos_um()
Error in `x_menos_um()`:
! argument "x" is missing, with no default

Também precisa dum valor com o qual essa operação faça sentido:

x_menos_um("a")
Error in `x - 1`:
! non-numeric argument to binary operator

Vários Argumentos, Posição e Nome

x_menos_y <- function(x, y) {
  resultado <- x - y
  return(resultado)
}

x_menos_y(20, 5)
[1] 15
x_menos_y(x = 20, y = 5)
[1] 15
x_menos_y(y = 5, x = 20)
[1] 15

Quando não escrevemos os nomes, o R associa os valores pela posição. Quando escrevemos x = e y =, a ordem deixa de ser necessária. Em funções com muitos argumentos, nomear os menos óbvios torna o código muito mais fácil de ler.

Argumentos com Valores por Defeito

Podemos dar a y um valor por defeito:

x_menos_y <- function(x, y = 1) {
  resultado <- x - y
  return(resultado)
}

x_menos_y(10)
[1] 9
x_menos_y(10, y = 3)
[1] 7

x continua a ser obrigatório. y passa a ser opcional.

É exactamente isto que encontramos em funções como mean():

args(mean)
function (x, ...) 
NULL

Alguns argumentos têm valores por defeito e só os alteramos quando precisamos.

Objectos como Argumentos

Podemos passar directamente um valor ou um objecto que contém esse valor.

x_menos_y(10, 5)
[1] 5
v1 <- 10
v2 <- 5
x_menos_y(v1, v2)
[1] 5

Para o R, a chamada recebe os valores a que v1 e v2 se referem naquele momento.

O Resultado duma Função como Argumento Doutra

Também podemos usar o resultado duma função dentro de outra.

v1 <- c(1, 3)
mean(v1)
[1] 2

Se não precisamos de guardar v1, podemos escrever:

mean(c(1, 3))
[1] 2

A chamada de dentro, c(1, 3), é avaliada primeiro. O vector que produz passa depois a ser o argumento de mean().

Isto é importante porque muito código R consiste precisamente em passar resultados de uma operação para a seguinte.

Pipes: Ler uma Sequência da Esquerda para a Direita

Quando começamos a encadear várias funções, os parêntesis ficam difíceis de seguir:

round(mean(as.numeric(c("1", "2", "6", "9"))), digits = 1)
[1] 4.5

O pipe nativo |> permite escrever a mesma sequência na ordem em que a pensamos:

c("1", "2", "6", "9") |>
  as.numeric() |>
  mean() |>
  round(digits = 1)
[1] 4.5

Leia |> como «e depois»:

  1. criamos o vector;
  2. e depois transformamo-lo em números;
  3. e depois calculamos a média;
  4. e depois arredondamos.

Por defeito, o resultado à esquerda entra como primeiro argumento da função à direita. Assim:

x |> mean(na.rm = TRUE)

corresponde, conceptualmente, a:

mean(x, na.rm = TRUE)

Não precisamos de usar pipes em todas as chamadas. Uma chamada curta como mean(idade) já é perfeitamente legível. O pipe é útil quando há uma sequência clara de passos.

Poderá encontrar %>% em código mais antigo ou em projectos que usam magrittr/tidyverse. Cumpre muitas vezes o mesmo papel pedagógico de encadear operações, mas não é exactamente o mesmo operador nem tem todas as mesmas regras.

Nos materiais novos do NRB preferimos o pipe nativo |>. Se encontrar %>% num script antigo, leia-o primeiro como «passar este resultado para a operação seguinte» e consulte a documentação quando a chamada usar funcionalidades específicas de magrittr.

Porque Escrevemos Funções Nossas?

No início vamos passar muito mais tempo a usar funções escritas por outras pessoas do que a escrever funções. Criar uma função nossa torna-se útil quando um procedimento se repete ou quando queremos dar um nome claro a uma sequência de passos.

Imaginemos duas escalas que precisam de ser invertidas. A regra geral é:

inverter_escala <- function(x, minimo, maximo) {
  minimo + maximo - x
}

stress <- c(0, 1, 3, 5)
bem_estar <- c(1, 20, 50, 100)

inverter_escala(stress, minimo = 0, maximo = 5)
[1] 5 4 2 0
inverter_escala(bem_estar, minimo = 1, maximo = 100)
[1] 100  81  51   1

Agora quem lê o script vê inverter_escala() e percebe o que está a ser feito sem ter de reconstruir a conta todas as vezes. É isto que queremos dizer quando falamos em abstracção: esconder detalhes que já estão resolvidos para podermos pensar no passo que interessa naquele momento.

Não. No R, uma função devolve por defeito o valor da última expressão avaliada. Esta função:

dobro <- function(x) {
  x * 2
}

é suficiente. Usámos return() nos primeiros exemplos porque torna explícita a ideia de «resultado da função». Em funções curtas, é comum deixar que a última expressão seja devolvida naturalmente.

Ler a Ajuda duma Função

Ninguém memoriza todos os argumentos de todas as funções. help() e ? existem precisamente para isso.

help(mean)
?mean

Quando abrir uma página de ajuda, procure sobretudo:

  • Usage: como se escreve a chamada;
  • Arguments: o que significa cada argumento;
  • Value: o que a função devolve;
  • Examples: exemplos de uso.

Quando encontrar num script uma função que não conhece, tente primeiro ler a chamada. Os nomes dos objectos e dos argumentos já dão pistas.

modelo <- lm(rts ~ grupo, data = dados)
resultados <- summary(modelo)

Mesmo antes de saber estatística suficiente para interpretar lm(), já podemos perceber que:

  • lm() é uma função;
  • recebe uma fórmula e uma tabela chamada dados;
  • o resultado foi guardado em modelo;
  • summary() recebe esse modelo e produz outro resultado.

Depois podemos abrir ?lm e confirmar o que ainda não sabemos. Aprender a ler código desta forma é uma das competências mais úteis que estes primeiros tutoriais podem dar.

Funções como Argumentos de Outras Funções

No R, uma função também pode ser passada como argumento.

sem_na <- function(x, fun) {
  fun(x, na.rm = TRUE)
}

valores <- c(2, 4, NA, 8)
sem_na(valores, mean)
[1] 4.666667
sem_na(valores, sd)
[1] 3.05505

Isto pode parecer estranho à primeira leitura. Não precisa de escrever funções assim já. O importante é reconhecer a ideia porque ela reaparece na família apply: damos à função os dados e também a função que queremos aplicar.

Operações Vectorizadas

Uma das características mais úteis do R é que muitas operações são vectorizadas. Se x contém vários números, x * 2 multiplica cada um deles por dois.

x <- c(1, 5, 10, -2)
x * 2
[1]  2 10 20 -4
x + 1
[1]  2  6 11 -1
sqrt(abs(x))
[1] 1.000000 2.236068 3.162278 1.414214

Não precisamos de escrever um ciclo para estas operações elementares.

Operações Matemáticas

x <- 10
x + 1
[1] 11
x * 2
[1] 20
x / 100
[1] 0.1
x^2
[1] 100
sqrt(x)
[1] 3.162278
log(x)
[1] 2.302585
exp(x)
[1] 22026.47

Se o objecto contiver texto, a mesma operação pode deixar de fazer sentido:

x <- "10"
x * 2
Error in `x * 2`:
! non-numeric argument to binary operator

Operações Lógicas Também São Vectorizadas

x <- c(2, 3, 4, 5)
x > 3
[1] FALSE FALSE  TRUE  TRUE
x == 4
[1] FALSE FALSE  TRUE FALSE
x[x > 3]
[1] 4 5

É a mesma lógica que usámos na indexação. A comparação produz um vector de TRUE/FALSE e depois usamos esse vector para seleccionar elementos.

Coerção: Mudar a Classe

Funções que começam por as. costumam converter um objecto para outra classe.

x <- "3"
class(x)
[1] "character"
x <- as.numeric(x)
class(x)
[1] "numeric"
x * 2
[1] 6

Outros exemplos comuns são as.character(), as.factor() e as.logical(). Uma conversão só é útil quando o valor original pode ser interpretado de forma sensata na nova classe. Antes de converter uma coluna inteira, olhe para alguns valores.

Redefinir um Objecto a Partir do Valor Actual

É perfeitamente normal usar um objecto no lado direito duma atribuição ao próprio objecto:

x <- 10
x <- x + 5
x <- x / 3
x
[1] 5

O R calcula primeiro a expressão à direita usando o valor antigo e só depois guarda o novo resultado em x. Isto é apenas uma nova atribuição. Não é recursão.

Pacotes

O R pode ganhar novas funções através de pacotes. Um pacote é um conjunto de código, documentação e outros recursos que alguém preparou para reutilização.

Há duas acções diferentes que convém não confundir:

  1. instalar o pacote no computador;
  2. carregar o pacote numa sessão de R.

Instalar: Uma Tarefa de Preparação, Não uma Linha da Análise

Um pacote só precisa de ser instalado quando ainda não está disponível no computador (ou quando estamos deliberadamente a actualizar/recriar o ambiente). A instalação não pertence ao script de análise.

Na consola, uma vez:

# Execute isto na consola, não no script de análise.
install.packages("ggplot2")

Depois disso, o script deve assumir que as dependências do projecto já estão instaladas. Se um projecto precisar de automatizar a preparação dum ambiente, essa instalação pertence a um script de setup/instalação separado, não ao script que importa, transforma e analisa os dados.

Carregar: Isto Pertence ao Script

library() torna as funções dum pacote disponíveis na sessão. Carregue os pacotes usados pelo script uma vez, numa secção própria no início, antes da primeira utilização, e mantenha essa lista organizada.

Não espalhe chamadas a library() pelo meio da análise só no ponto em que se lembrou de cada pacote. Quem voltar a executar o script deve conseguir ver as dependências logo no início.

Também podemos chamar uma função sem anexar o pacote inteiro:

readxl::read_excel("dados.xlsx")

A forma pacote::funcao() é especialmente útil quando usamos poucas funções dum pacote ou quando há funções com o mesmo nome em pacotes diferentes.

AvisoUma Regra de Estilo Que Deve Sobreviver ao Copy-Paste

Nunca ponha isto a meio dum script de análise:

install.packages("algum_pacote")

Um script de análise deve poder ser executado repetidamente sem tentar alterar a instalação de R da máquina. Instalar dependências é preparação do ambiente. Carregar e usar dependências faz parte da análise.

Estas são tarefas de manutenção da instalação, não da análise:

update.packages()
remove.packages("nome_do_pacote")

Num projecto que precise de versões exactamente reproduzíveis, a gestão de dependências deve ser tratada de forma explícita pelo projecto. Não tente resolver isso espalhando install.packages() pelos scripts.

A Família apply

Esta secção pode ser lida agora ou usada como referência mais tarde. A ideia comum é simples: dar uma função e pedir ao R que a aplique repetidamente a vários elementos.

lapply(): Aplicar uma Função e Obter uma Lista

lapply(X, FUN) aplica FUN a cada elemento de X e devolve sempre uma lista.

amostras <- list(
  local_a = c(2, 4, 6),
  local_b = c(10, 12),
  local_c = c(1, 1, 2, 2)
)

lapply(amostras, mean)
$local_a
[1] 4

$local_b
[1] 11

$local_c
[1] 1.5

Também funciona muito bem com as colunas dum data.frame quando a mesma função faz sentido para todas:

numericos <- data.frame(
  idade = c(21, 32, 24),
  rts = c(500, 620, 570)
)

lapply(numericos, mean)
$idade
[1] 25.66667

$rts
[1] 563.3333

sapply(): Tentar Simplificar o Resultado

sapply() faz uma operação semelhante, mas tenta devolver uma estrutura mais simples quando consegue.

sapply(amostras, mean)
local_a local_b local_c 
    4.0    11.0     1.5 

Aqui obtemos um vector em vez duma lista. É cómodo quando estamos a explorar. Em código reutilizável, lembre-se de que a forma exacta do resultado pode mudar consoante aquilo que a função devolver.

vapply() é uma variante em que declaramos antecipadamente a forma esperada do resultado. Por exemplo, se cada chamada deve devolver um único número:

vapply(amostras, mean, numeric(1))

Se uma chamada não respeitar essa forma, vapply() dá erro em vez de simplificar de maneira inesperada. Não precisa desta função para os primeiros scripts, mas é uma boa opção em código que deve ser robusto.

apply(): Linhas ou Colunas duma Matriz

apply() trabalha sobre margens duma matriz ou array. MARGIN = 1 significa linhas e MARGIN = 2 colunas.

m <- matrix(1:12, nrow = 3)
m
     [,1] [,2] [,3] [,4]
[1,]    1    4    7   10
[2,]    2    5    8   11
[3,]    3    6    9   12
apply(m, 1, sum)
[1] 22 26 30
apply(m, 2, mean)
[1]  2  5  8 11

Num data.frame numérico, lapply() costuma ser mais directo para aplicar uma função a cada coluna. apply() pode converter um data.frame para matriz. Se houver colunas de classes diferentes, essa conversão pode mudar os tipos. Por isso, não use apply() mecanicamente só porque viu data.frame + «todas as colunas».

mapply(): Vários Argumentos em Paralelo

mapply() aplica uma função usando elementos correspondentes de vários argumentos.

mapply(sum, c(1, 2, 3), c(10, 20, 30))
[1] 11 22 33

Podemos ler isto como três chamadas: sum(1, 10), sum(2, 20) e sum(3, 30).

No código baseado no tidyverse poderá encontrar purrr::map() e variantes como map_dbl(). A ideia é semelhante à de lapply(): aplicar uma função a cada elemento. Esta página ensina primeiro as ferramentas da linguagem base para que consiga ler código R independentemente do conjunto de pacotes usado.

Controlo do Fluxo

Não precisa de dominar esta secção para começar a analisar dados. Volte aqui quando precisar de fazer coisas diferentes consoante uma condição ou repetir uma operação que não fica clara com vectorização ou com uma função de ordem superior.

if e else

if escolhe um caminho com base numa única condição lógica.

x <- 4

if (x > 0) {
  mensagem <- "positivo"
} else {
  mensagem <- "zero ou negativo"
}

mensagem
[1] "positivo"

Para uma decisão elemento a elemento num vector existe ifelse():

x <- c(-2, 0, 3)
ifelse(x > 0, yes = "positivo", no = "não positivo")
[1] "não positivo" "não positivo" "positivo"    

if controla o caminho do código. ifelse() faz outra coisa: devolve um valor para cada elemento duma condição vectorizada.

for

Um ciclo for percorre os elementos duma sequência.

for (letra in c("A", "B", "C")) {
  print(letra)
}
[1] "A"
[1] "B"
[1] "C"

Se precisamos das posições dum vector, seq_along() é uma forma segura de as obter:

valores <- c(3, 4, 5, 6, 7)
resultado <- numeric(length(valores))

for (i in seq_along(valores)) {
  resultado[i] <- valores[i] * 2
}

resultado
[1]  6  8 10 12 14

Mas, neste exemplo, a operação já é vectorizada:

valores * 2
[1]  6  8 10 12 14

Isso não torna os ciclos «maus». Use a construção que exprime melhor a lógica. Para operações simples elemento a elemento, a vectorização costuma ser mais curta e legível. Para processos com estado, passos dependentes ou lógica sequencial, um for pode ser a solução mais clara.

O Que Convém Conseguir Ler Daqui para a Frente

Se esta página cumpriu o seu trabalho, não precisa de se lembrar de todas as funções. Deve, isso sim, começar a conseguir decompor código:

resultados <- dados$rts |>
  as.numeric() |>
  log() |>
  mean(na.rm = TRUE)

Mesmo que não saiba ainda porque alguém quer logaritmizar estes valores, pode ler a gramática:

  • dados$rts selecciona uma coluna;
  • as.numeric() converte os valores para números;
  • log() transforma-os;
  • mean(..., na.rm = TRUE) calcula a média ignorando valores em falta;
  • <- guarda o resultado em resultados.

É essa capacidade de leitura que vai permitir avançar para análises mais complexas sem sentir que cada script começa noutra língua.

1 Exercícios

Não é necessário fazer todos de seguida. Use-os também para voltar a uma secção quando perceber que já não se lembra bem dum conceito.

  1. Crie três objectos: um número, uma frase e um valor lógico. Use class() para confirmar cada um e altere depois o valor dum deles.
  2. Crie dados <- c(10000, 200, 10, 0.5, -1). Descubra quais os valores inferiores a 1; iguais a 0.5; iguais a 0.5 ou 200; superiores a 10 e inferiores a 10000.
  3. Crie um vector com cinco números e seleccione: a primeira posição; as posições 2 a 4; as posições 1, 3 e 5; todos os valores superiores à média.
  4. Crie f <- factor(c("10", "20", "10")). Compare as.numeric(f) com as.numeric(as.character(f)) e explique o que aconteceu.
  5. Leia, palavra por palavra, round(mean(c(2, 4, 8)), digits = 1). Qual função é executada primeiro? Que resultado passa para a função seguinte?
  6. Comece com x <- c("2", "4", "8"). Indique a classe de x e explique por que mean(x) não calcula a média numérica pretendida. Depois use |> para converter os valores em números, calcular a média e arredondá-la a uma casa decimal. Diga que resultado passa de cada etapa para a seguinte.
  7. Escreva uma função multiplicar(x, por = 2) e teste argumentos posicionais, nomeados e o valor por defeito.
  8. Crie uma lista com três vectores numéricos de tamanhos diferentes. Calcule a média de cada um com lapply() e sapply(). Compare os resultados.
  9. Use uma matriz pequena para comparar apply(m, 1, sum) com apply(m, 2, sum). Explique o que significam 1 e 2.
  10. Escreva um for que produza o dobro de cada elemento dum vector. Depois escreva a versão vectorizada e explique qual escolheria neste caso.
  11. Imagine que um colega colocou install.packages("ggplot2") a meio dum script de análise. Explique por que razão a instalação deve estar fora desse script e onde colocaria library(ggplot2).
  12. Leia este código sem o executar e escreva tudo o que consegue inferir:
modelo <- rts ~ grupo
ajuste <- lm(formula = modelo, data = dados)
resultado <- summary(ajuste)
  1. Considere idades <- c(21, NA, 17, 30). Seleccione as idades não omissas iguais ou superiores a 18. Calcule a média dessas idades e explique por que razão os valores em falta requerem atenção na condição e no cálculo.

Mesmo sem saber interpretar o modelo, consegue reconhecer:

  • uma fórmula guardada em modelo;
  • uma chamada a lm() com argumentos nomeados;
  • o resultado dessa chamada guardado em ajuste;
  • summary() a receber esse objecto;
  • o novo resultado guardado em resultado.

O passo seguinte seria ?lm e ?summary para descobrir o que ainda não sabe.

Clique para ver as soluções
  1. numero <- 3.5
    frase <- "olá"
    logico <- TRUE
    class(numero)
    class(frase)
    class(logico)
    numero <- 4
  2. dados <- c(10000, 200, 10, 0.5, -1)
    dados < 1
    dados == 0.5
    dados == 0.5 | dados == 200
    dados > 10 & dados < 10000
    Cada expressão devolve um valor lógico por elemento.
  3. x <- c(3, 8, 2, 9, 5)
    x[1]
    x[2:4]
    x[c(1, 3, 5)]
    x[x > mean(x)]
  4. as.numeric(f) devolve os códigos internos dos níveis (1, 2, 1); as.numeric(as.character(f)) converte os rótulos numéricos, devolvendo 10, 20, 10.
  5. c() cria primeiro c(2, 4, 8). mean() recebe esse vector e devolve 4; round() recebe 4 e digits = 1, devolvendo 4.
  6. x <- c("2", "4", "8")
    class(x)  # "character"
    
    x |>
      as.numeric() |>  # c(2, 4, 8)
      mean() |>        # 4
      round(digits = 1)
    x contém texto, pelo que primeiro o convertemos em números. O pipe passa o vector convertido a mean(), que devolve 4; round() recebe esse resultado e devolve 4. A cadeia completa produz o mesmo resultado que round(mean(c(2, 4, 8)), digits = 1).
  7. multiplicar <- function(x, por = 2) x * por
    multiplicar(3, 4)           # posicional: 12
    multiplicar(x = 3, por = 4) # nomeado: 12
    multiplicar(3)              # por defeito: 6
  8. medidas <- list(c(2, 4), c(1, 3, 8), c(10, 20, 30, 40))
    lapply(medidas, mean)
    sapply(medidas, mean)
    Ambas calculam as médias; lapply() devolve uma lista e sapply() tenta simplificar para um vector.
  9. m <- matrix(1:6, nrow = 2)
    apply(m, 1, sum)  # somas por linha
    apply(m, 2, sum)  # somas por coluna
    1 indica linhas e 2 colunas.
  10. x <- c(2, 4, 6)
    dobro <- numeric(length(x))
    for (i in seq_along(x)) dobro[i] <- 2 * x[i]
    dobro_vectorizado <- 2 * x
    Aqui escolheria a versão vectorizada: é mais curta e directa.
  11. A instalação é configuração do ambiente, feita uma vez, não parte da análise. Instale-se separadamente na consola; library(ggplot2) fica na secção inicial de dependências do script, antes da primeira utilização.
  12. modelo guarda a fórmula rts ~ grupo. lm() recebe essa fórmula e a tabela dados; o resultado fica em ajuste. summary(ajuste) produz um resumo guardado em resultado. A fórmula sugere rts como resposta e grupo como preditor; sem executar o código ou conhecer os dados, não se inferem estimativas nem se confirma que os nomes existem.
  13. idades <- c(21, NA, 17, 30)
    idades[!is.na(idades) & idades >= 18]
    # [1] 21 30
    
    media_adultos <- mean(idades[!is.na(idades) & idades >= 18])
    media_adultos
    # [1] 25.5
    Uma comparação com NA pode devolver NA, pelo que excluímos primeiro os valores omissos com !is.na(). A média é calculada apenas sobre os valores seleccionados; não é necessário na.rm = TRUE, porque a selecção já retirou o NA.