nome <- "Ana"
idade <- 23
resultado <- 3 + 2A Gramática do R
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 e serve também de referência. Pode lê-la por partes, sem ter de memorizar tudo. 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:
- variáveis, nomes e objectos;
- classes de dados;
- vectores e indexação;
- funções e argumentos;
- operações vectorizadas;
- pacotes.
As secções sobre funções como argumentos, expressões regulares, a família apply, os ciclos e controlo do fluxo e o tratamento de erros 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.
1 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.
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
Se quiser conservar o valor anterior para comparar o antes e o depois, crie um novo objecto antes de voltar a atribuir um valor ao mesmo nome.
idade_original <- idade
idade_no_proximo_ano <- idade + 1O 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 <- TRUEEvite 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 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.
2 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().
O R é uma linguagem dinamicamente tipada. Os objectos têm tipos e classes, mas normalmente não declaramos antecipadamente «resultado será sempre um número». O tipo pertence ao valor/objecto que está actualmente associado ao nome, e podemos associar esse mesmo nome a outro objecto mais tarde:
Isto é conveniente no trabalho interactivo e em scripts. Podemos começar por guardar um número em resultado, descobrir que afinal precisamos de conservar mais informação e passar a guardar uma lista, sem ter de abandonar um nome que continua a significar exactamente a mesma coisa no raciocínio: «o resultado desta etapa».
A flexibilidade tem um preço. O nome, por si só, não nos diz tudo. Ao encontrar resultado, precisamos de saber se naquele momento contém um número, texto, uma lista ou outro objecto. Podemos ler o código que o produziu, inspeccionar o objecto ou perguntar ao R com funções como class(), typeof() ou str().
O R tem tipos, embora não exija, em geral, uma declaração estática do tipo de cada nome. A associação entre um nome e um objecto pode mudar durante a execução. Pacotes e funções podem verificar tipos e classes e recusar valores que não sabem tratar.
Uma forma útil de organizar o que vem a seguir é distinguir entre os vectores atómicos, cujos elementos têm um tipo comum, e estruturas que organizam ou combinam objectos. numeric, character e logical são exemplos de tipos atómicos que encontraremos constantemente. Uma lista pode combinar objectos diferentes. Um data.frame é, essencialmente, uma lista de colunas com o mesmo comprimento. Uma matriz é um vector atómico ao qual foram acrescentadas dimensões. Esta divisão conserva a intuição dos tipos «simples» e «compostos», com uma distinção mais rigorosa.
factor mostra também por que razão tipo, classe e estrutura não são exactamente a mesma coisa no R: por baixo, um factor usa valores inteiros, mas a sua classe e os seus níveis dão-lhe o comportamento duma variável categórica. Estas distinções servem sobretudo como mapa para a referência que se segue. Para já, basta reconhecê-las.
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":
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. «A Terra é um planeta E uma estrela» é falso: uma parte verdadeira não chega. Já «vou ao cinema OU ao teatro», no OU do R, também é verdadeiro se for aos dois. O R não nos obriga a escolher o programa da noite.
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 <- TRUETRUE 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.
[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:
[1] baixo alto medio
Levels: baixo < medio < alto
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.
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".
Muitas funções devolvem NA quando os dados contêm valores em falta, a menos que lhes digamos explicitamente como os tratar.
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.
Também existe um tipo inteiro explícito, escrito com L:
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.
3 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():
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.
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%:
[1] FALSE TRUE FALSE TRUE TRUE
[1] "F" "F" "F"
Se houver valores em falta, escreva a regra explicitamente com is.na().
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.
$nome
[1] "Ana"
$idades
[1] 21 24 30
$aprovado
[1] TRUE
coisas$nome[1] "Ana"
coisas$idades[1] 21 24 30
Não é preciso dominar as listas para 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:
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:
Não é preciso usar esta regra deliberadamente para seguir os tutoriais. É útil reconhecê-la quando aparecer num aviso ou num resultado inesperado.
Matrizes
Uma matriz é uma estrutura rectangular, com linhas e colunas, em que todos os elementos têm o mesmo tipo. Tecnicamente, é um vector atómico com dimensões.
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
m[2, 3] # linha 2, coluna 3[1] 8
Ao contrário dum data.frame, uma matriz não pode ter uma classe diferente em cada coluna. Encontrará matrizes em algumas funções e resultados, mas para a maior parte das bases de dados deste curso trabalharemos sobretudo com data.frames.
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.
4 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:
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 =, podemos mudar a ordem dos argumentos. 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.
Se não precisamos de guardar v1, podemos escrever:
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»:
- criamos o vector;
- e depois transformamo-lo em números;
- e depois calculamos a média;
- 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 é:
[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.
Depois de dizer que vamos sobretudo usar funções dos outros, passei várias secções a ensinar a escrevê-las. Era para deixarem de parecer magia, não para as memorizar.
No R, return() não é obrigatório: 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)
?meanQuando 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.
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
Mergulhar Mais Fundo
Na primeira leitura, pode avançar para as operações vectorizadas e voltar aqui quando encontrar uma função que recebe outra função.
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. Para já, não é preciso escrever funções assim. 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.
Um paradigma de programação é uma forma geral de organizar e pensar o código: que unidades usamos, como descrevemos uma sequência de operações e como organizamos o estado do programa. Entre os paradigmas mais conhecidos estão a programação imperativa, a programação orientada a objectos e a programação funcional. A primeira descreve uma sequência de instruções e alterações de estado. A segunda organiza comportamento em torno de objectos. A terceira dá um papel central à composição e aplicação de funções.
Uma linguagem não tem de pertencer exclusivamente a um deles. O R é multi-paradigma e permite combinar ideias de vários estilos. Para um mapa mais amplo, a Wikipedia sobre paradigmas de programação é uma boa referência para explorar.
A palavra «paradigma» aparece também na filosofia da ciência. Thomas Kuhn tornou-a famosa para falar dos exemplares, práticas, valores e outros elementos partilhados por uma comunidade científica. Em ambos os casos há uma ideia próxima: modos de pensar e trabalhar que orientam a resolução de problemas. Para explorar esse parentesco na filosofia da ciência, veja Kuhn: Paradigmas e Revoluções.
Na programação funcional, as funções são tratadas como valores que podemos combinar e reutilizar. Uma propriedade central que torna isso possível é ter funções de primeira classe: no R, uma função é um objecto. Podemos atribuí-la a um nome, guardá-la numa lista, passá-la como argumento e devolvê-la como resultado doutra função.
Quando uma função recebe outra função ou devolve uma função, costuma chamar-se uma função de ordem superior. lapply(), sapply() e mapply() são exemplos que encontrará já nesta página. Isto permite separar o que fazer dos dados a que o queremos fazer: podemos escrever uma operação geral e fornecer-lhe a função concreta quando a usamos.
O estilo funcional vai além de tratar funções como valores: também tende a favorecer a composição de funções e a limitar alterações de estado e outros efeitos secundários. O R permite efeitos secundários e suporta outros paradigmas, pelo que não é uma linguagem funcional pura. Tratar funções como valores é, contudo, uma parte importante da programação em R. Ajuda-nos a perceber como funcionam a família apply e muitas outras ferramentas.
5 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
[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 * 2Error 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.
Uma expressão como x * 2 descreve uma operação sobre um vector inteiro. Nas operações aritméticas básicas sobre vectores numéricos, o R percorre os elementos em código compilado. Fazemos uma chamada para tratar o conjunto dos valores, em vez de escrever em R cada acesso a uma posição, multiplicação e atribuição do resultado. Esta redução do trabalho repetido de interpretação ajuda a explicar o ganho de velocidade.
No tutorial anterior vimos que chamar ao R uma linguagem «interpretada» é uma primeira aproximação. O R combina interpretação, bytecode e compilação JIT, e usa código compilado para muitas operações fundamentais. A vectorização aproveita esse trabalho já implementado. Ao escrevermos sqrt(x), por exemplo, podemos pensar nas raízes do vector inteiro e deixar a repetição por conta da função.
O código compilado pode ainda aproveitar SIMD (Single Instruction, Multiple Data): instruções do processador que aplicam a mesma operação a vários valores de uma vez. É uma optimização ao nível da CPU, escolhida na implementação e compilação do código. A expressão x * 2 continua a dizer-nos que a operação é elemento a elemento; para saber como o processador a executa, teríamos de examinar a implementação usada nessa instalação.
Na álgebra linear, operações como a multiplicação de matrizes podem recorrer a BLAS (Basic Linear Algebra Subprograms), um conjunto de rotinas especializadas nestes cálculos. Implementações optimizadas, como OpenBLAS, podem aproveitar SIMD e distribuir trabalho por vários núcleos. A documentação de instalação do R sobre BLAS explica as opções disponíveis. Aqui, a biblioteca ligada ao R e a operação executada determinam quais dessas optimizações entram em jogo.
Este trabalho sobre vectores e matrizes é central na computação científica, incluindo nas redes neuronais. Há por isso muito investimento em bibliotecas e hardware para o acelerar. Para quem está a aprender R, o hábito útil é mais imediato: procurar uma operação que exprima directamente o cálculo pretendido. x * 2 é mais fácil de ler do que um ciclo que duplica cada posição. Quando a rapidez se tornar um problema concreto, medimos o tempo com dados representativos antes de escolher outra implementação.
Para aprofundar a relação entre operações sobre vectores e código compilado, veja Advanced R, secção «Vectorise».
Coerção: Mudar a Classe
Funções que começam por as. costumam converter um objecto para outra classe.
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.
Reatribuição: Um Novo Valor a Partir do Anterior
Em programação é perfeitamente normal definir o novo valor dum objecto a partir do valor que esse mesmo objecto tinha antes:
x <- 10
x <- x + 5
x <- x / 3
x[1] 5
À primeira vista, x <- x + 5 pode parecer estranho a quem lê <- como se fosse o sinal de igualdade da matemática. A equação x = x + 5 seria impossível. Aqui, porém, não estamos a escrever uma equação. O R avalia primeiro a expressão à direita usando o valor actual de x e só depois associa x ao novo resultado. Conceptualmente:
x_novo <- x_anterior + 5
Há aqui uma forma intuitiva de auto-referência: definimos uma nova versão de x a partir da anterior. Em terminologia de programação, o nome habitual para isto é reatribuição (reassignment) ou actualização do estado, e não recursão no sentido técnico. Mais tarde, se encontrar «recursão» em programação, o termo refere-se normalmente a uma função que se chama a si própria, directa ou indirectamente.
6 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:
- instalar o pacote no computador;
- 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")Os pacotes ficam em pastas chamadas bibliotecas (libraries). Precisa de permissão de escrita na biblioteca onde vai instalar. Na consola, .libPaths() mostra as bibliotecas que esta sessão conhece; por defeito, install.packages() tenta instalar na primeira. Se essa pasta não permitir escrita, numa sessão interactiva o R pode propor criar uma biblioteca pessoal. Aceitar essa opção costuma ser suficiente num computador pessoal. Numa máquina da instituição, siga as indicações de quem a administra; não é preciso executar toda a análise como administrador para instalar pacotes pessoais. A ajuda de install.packages() explica a escolha da biblioteca e o argumento lib.
O RStudio também tem ferramentas para gerir pacotes no separador Packages. Pode instalar, actualizar ou remover pacotes por aí. Ao carregar em Install, o RStudio envia uma chamada a install.packages() para a consola. Vale a pena aprender os comandos de R que estão por trás destes botões: continuam a servir quando mudamos de editor ou trabalhamos apenas num terminal.
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.
Depois de carregar um pacote, usamos as suas funções directamente. Por exemplo, num script que lê ficheiros Excel:
# No início do script, com os restantes pacotes.
library(readxl)
# Na secção de importação dos dados.
dados <- read_excel("dados.xlsx")Este é o fluxo habitual que vamos usar: library(pacote) no início do script, depois funcao() onde precisamos dela.
Reconhecer pacote::funcao()
Também encontrará código que indica o pacote na própria chamada:
dados <- readxl::read_excel("dados.xlsx")Os dois pontos duplos :: dizem ao R de que pacote vem a função: aqui, «usa read_excel() do pacote readxl». Esta forma não exige uma chamada prévia a library(readxl), mas o pacote tem de estar instalado. As duas chamadas a read_excel() fazem a mesma leitura.
Convém reconhecer esta sintaxe na documentação e no código de outras pessoas. É também útil para resolver ambiguidades quando dois pacotes têm funções com o mesmo nome: dplyr::filter(), por exemplo, escolhe explicitamente a função de dplyr, e não a de outro pacote. Fora dessas situações, vamos preferir carregar os pacotes no início e chamar as funções directamente.
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.
7 Expressões Regulares
Mergulhar Mais Fundo
Na primeira leitura, retome a leitura de código. Para aprofundar, continue abaixo.
Uma expressão regular (regular expression, ou regex) é um padrão para procurar texto. É útil para encontrar nomes de colunas, identificar códigos ou separar partes de uma string. Esta secção pode esperar até precisar dela, por exemplo ao reformatar uma tabela.
Comecemos pela diferença entre procurar texto em qualquer posição e exigir uma correspondência com a string inteira. grepl() recebe um padrão e um vector de texto e devolve TRUE ou FALSE por elemento:
A primeira pesquisa encontra rt nos quatro nomes. A segunda aceita apenas os dois primeiros. Podemos ler o padrão por partes:
| Símbolo | Como Ler |
|---|---|
^ |
início da string |
$ |
fim da string |
rt_ |
estes caracteres, literalmente |
(easy|hard) |
um grupo com as alternativas easy ou hard
|
Dentro duma regex, | significa «ou». Não é o operador lógico entre duas expressões R que vimos antes: aqui faz parte do texto do padrão. Os parêntesis agrupam e capturam uma parte da correspondência. Algumas funções aproveitam essas capturas para extrair texto: em tidyr::pivot_longer(), por exemplo, os grupos de names_pattern alimentam as colunas indicadas em names_to. grepl() apenas testa se há correspondência; não devolve os grupos capturados.
Repetições e Caracteres Especiais
Para reconhecer identificadores como pp01, podemos escrever:
[1] TRUE TRUE FALSE FALSE FALSE
Esperamos TRUE apenas para os dois primeiros. [0-9] aceita um algarismo e {2} exige exactamente duas repetições. Outros padrões frequentes são + (uma ou mais repetições), * (zero ou mais) e ? (zero ou uma). Estes quantificadores aplicam-se ao elemento anterior, que pode ser um carácter, uma classe entre [] ou um grupo entre ().
Um ponto . significa «um carácter qualquer». Para procurar um ponto literal, precisamos de o proteger. Na string de R, escrevemos \\.: a primeira camada de interpretação é a string R e a segunda é a regex.
[1] TRUE FALSE FALSE
Só o primeiro nome termina em .csv. Se quiser apenas procurar texto literal em qualquer posição, pode evitar as regras de regex com fixed = TRUE:
grepl(".csv", ficheiros_regex, fixed = TRUE)[1] TRUE FALSE TRUE
Agora o primeiro e o terceiro nomes contêm o texto procurado. Este teste não exige que a extensão esteja no fim do nome.
Testar Antes de Usar
Um padrão que funciona num nome pode aceitar nomes que não queríamos. Experimente exemplos que devem passar e outros que devem falhar:
nomes_regex[1] "rt_easy" "rt_hard" "extra_rt_easy" "rt_medium"
grepl("^rt_(easy|hard)$", nomes_regex)[1] TRUE TRUE FALSE FALSE
Esperamos TRUE para os dois primeiros nomes e FALSE para os restantes. Os resultados seguem a ordem dos nomes: o padrão aceitou apenas os nomes que queríamos? Experimente alterar um nome ou o padrão e veja o que muda. Inspeccione também os nomes reais antes de usar um padrão para transformar uma tabela. Uma regex não detecta uma categoria que se esqueceu de incluir nas alternativas.
Para explorar um padrão com explicações visuais, use regex101 e escolha o motor PCRE2. Nesse editor escrevemos a regex directamente, por exemplo \.csv$, sem duplicar a barra como na string R. O R e os seus pacotes usam motores diferentes; por isso, confirme sempre o resultado na função de R que vai usar. A documentação de R em ?regex descreve a sintaxe suportada por base R. Se pedir ajuda a um colega ou a um LLM, forneça exemplos válidos e inválidos e indique a função de destino. Verifique a proposta com os mesmos testes, sem partilhar dados pessoais ou confidenciais.
8 A Família apply
Mergulhar Mais Fundo
Na primeira leitura, retome a leitura de código. Para aprofundar, continue abaixo.
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.
$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()
vapply() é uma variante em que declaramos antecipadamente a forma esperada do resultado. Por exemplo, se cada chamada deve devolver um único número:
Se uma chamada não respeitar essa forma, vapply() dá erro em vez de simplificar de maneira inesperada. Para os primeiros scripts, não é preciso usar esta função, 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 aos elementos correspondentes de vários argumentos.
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.
9 Controlo do Fluxo
Mergulhar Mais Fundo
Na primeira leitura, retome a leitura de código. Para aprofundar, continue abaixo.
Não é preciso 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():
[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.
Se precisamos das posições dum vector, seq_along() é uma forma segura de as obter:
[1] 6 8 10 12 14
Mas, neste exemplo, a operação já é vectorizada:
valores * 2[1] 6 8 10 12 14
Os ciclos for também têm o seu lugar e não são necessariamente lentos. Muito do conselho histórico para «evitar ciclos em R» vem de versões e padrões de código mais antigos. O R actual usa compilação de bytecode e, por defeito, JIT (just-in-time compilation) que pode compilar também ciclos executados no nível superior. Além disso, um ciclo bem escrito e com a memória necessária pré-alocada pode ser perfeitamente adequado.
A razão principal para preferirmos valores * 2 neste exemplo é mais simples: exprime directamente a operação que queremos fazer, é idiomático em R, é mais curto e costuma ser mais fácil de ler. Quando existe uma operação vectorizada que corresponde naturalmente ao problema, use-a. Para processos com estado, passos dependentes ou lógica sequencial, um for pode ser a solução mais clara.
Ao escrever código, privilegiamos primeiro a correcção e a legibilidade. Só vale a pena optimizar quando há uma necessidade concreta, por exemplo, quando a execução demora demasiado para a tarefa. Nesse caso, meça o desempenho e identifique onde está o custo antes de alterar o código. Uma alteração que parece mais rápida pode não o ser nos dados e no computador em causa.
Hadley Wickham desenvolve esta recomendação em Advanced R, capítulo «Improving performance». Leia a introdução: depois de identificar um ponto lento com profiling, definimos quanto tempo é aceitável para a execução, em vez de tentar eliminar todas as ineficiências. Os exemplos e as técnicas seguintes podem esperar até haver uma necessidade concreta.
Fonte: Hadley Wickham, Advanced R (2.ª ed.), versão online do autor. Se o capítulo não aparecer aqui, abra a ligação acima.
while
Um ciclo while repete código enquanto uma condição continuar verdadeira. É útil quando não sabemos à partida quantas repetições serão necessárias.
x <- 1
while (x < 100) {
x <- x * 2
}
x[1] 128
Ao contrário dum for, que normalmente percorre uma sequência conhecida, while volta a testar a condição antes de cada repetição. É importante que alguma coisa dentro do ciclo possa tornar a condição falsa. Caso contrário, criamos um ciclo infinito.
O R tem ainda break, que termina imediatamente o ciclo actual, e next, que salta o resto da iteração actual e passa à seguinte.
Existe também repeat, que repete indefinidamente até alguma instrução, em geral break, o terminar:
x <- 1
repeat {
x <- x * 2
if (x >= 100) {
break
}
}É uma construção perfeitamente válida da linguagem, embora apareça bastante menos no tipo de análise de dados que faremos aqui.
10 Referência Avançada: Erros
Mergulhar Mais Fundo
Na primeira leitura, retome a leitura de código. Para aprofundar, continue abaixo.
Esta secção pode ficar para mais tarde. Ao explorar uma análise na consola, podemos olhar para os dados e decidir o que experimentar a seguir. Quando escrevemos uma função para reutilizar ou uma tarefa que vai correr sem nós presentes, pode ser útil que o código indique por si próprio que algo correu mal ou merece atenção.
Interromper com stop()
stop() produz um erro e interrompe a avaliação:
Avisos e Mensagens
warning() produz um aviso, mas normalmente permite que a avaliação continue. message() serve para comunicar informação ao utilizador sem a tratar como erro ou aviso.
A distinção é substantiva: use um erro quando não é sensato continuar, um aviso quando é possível continuar mas há algo que o utilizador deve verificar, e uma mensagem para informação sobre o que o código está a fazer.
Verificar Expectativas com stopifnot()
stopifnot() recebe condições que esperamos que sejam verdadeiras. Se todas forem TRUE, não imprime nada; se alguma não for, produz um erro. É uma forma curta de registar uma expectativa em código que queremos reutilizar:
[1] 0 2 3
Aqui exigimos números finitos e não negativos. is.finite() rejeita valores em falta e infinitos; all() exige que a condição se cumpra em todos os elementos. Com uma entrada inadequada, a função pára antes de calcular a raiz:
raiz_real(c(4, -1))Error in `raiz_real()`:
! all(x >= 0) is not TRUE
Esperamos um erro que indique a condição que falhou. Com stop() podemos escrever uma mensagem mais explicativa; stopifnot() é mais compacto quando a própria condição já comunica o problema. Nenhum deles comprova que uma análise ou uma decisão científica esteja correcta.
Estas verificações são úteis, por exemplo, ao desenvolver funções ou testes automáticos. Não precisamos de transformar cada consulta da consola numa asserção: escrever um objecto ou pedir nrow() continua a ser suficiente para muitas verificações durante a preparação duma análise.
Normalmente, um erro interrompe a avaliação. Em processos automáticos pode ser útil tentar uma operação e decidir programaticamente o que fazer se ela falhar.
try() é a forma simples de impedir que um erro interrompa imediatamente uma sequência maior:
Para controlo mais explícito existe tryCatch(), que permite definir o que acontece perante diferentes condições:
Aqui, se a expressão produzir um erro, resultado recebe NA_real_ em vez de a execução parar. Só devemos escolher essa saída quando um valor em falta for uma resposta adequada à falha. Capturar um erro não o resolve: devolver NA sem investigar pode esconder um problema que afecte a análise. O sistema de conditions do R é bastante mais geral do que estes exemplos. Para já, basta reconhecer try() e tryCatch() quando os encontrar em código mais avançado.
11 O Que Convém Conseguir Ler Daqui para a Frente
O objectivo é começar a conseguir decompor código, mesmo sem se lembrar de todas as funções:
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$rtsselecciona 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 emresultados.
É essa capacidade de leitura que vai permitir avançar para análises mais complexas sem sentir que cada script começa noutra língua.
12 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.
Na primeira leitura, pode deixar os exercícios 8 a 10 para depois de consultar a família apply e o controlo do fluxo.
- 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.
Solução 1
- 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.
Solução 2
dados <- c(10000, 200, 10, 0.5, -1)
dados < 1
dados == 0.5
dados == 0.5 | dados == 200
dados > 10 & dados < 10000Cada expressão devolve um valor lógico por elemento.
- 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.
- Crie
f <- factor(c("10", "20", "10")). Compareas.numeric(f)comas.numeric(as.character(f))e explique o que aconteceu.
Solução 4
as.numeric(f) devolve os códigos internos dos níveis (1, 2, 1). Já as.numeric(as.character(f)) converte os rótulos numéricos, devolvendo 10, 20, 10.
- 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?
Solução 5
c() cria primeiro c(2, 4, 8). mean() recebe esse vector e devolve 4. Depois, round() recebe 4 e digits = 1, devolvendo 4.
- Comece com
x <- c("2", "4", "8"). Indique a classe dexe explique por quemean(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.
Solução 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. Depois, round() recebe esse resultado e devolve 4. A cadeia completa produz o mesmo resultado que round(mean(c(2, 4, 8)), digits = 1).
- Escreva uma função
multiplicar(x, por = 2)e teste argumentos posicionais, nomeados e o valor por defeito.
Solução 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- Crie uma lista com três vectores numéricos de tamanhos diferentes. Calcule a média de cada um com
lapply()esapply(). Compare os resultados.
Solução 8
Ambas calculam as médias. lapply() devolve uma lista e sapply() tenta simplificar para um vector.
- Use uma matriz pequena para comparar
apply(m, 1, sum)comapply(m, 2, sum). Explique o que significam1e2.
Solução 9
1 indica linhas e 2 colunas.
- Escreva um
forque produza o dobro de cada elemento dum vector. Depois escreva a versão vectorizada e explique qual escolheria neste caso.
Solução 10
Aqui escolheria a versão vectorizada: é mais curta e directa.
- 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 colocarialibrary(ggplot2).
Solução 11
A instalação prepara o ambiente e faz-se uma vez, separadamente na consola. Fica fora do script de análise. library(ggplot2) fica na secção inicial de dependências do script, antes da primeira utilização.
- Leia este código sem o executar e escreva tudo o que consegue inferir:
Solução 12
modelo guarda a fórmula rts ~ grupo. lm() recebe essa fórmula e a tabela dados, e 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.
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 consultar ?lm e ?summary para descobrir o que ainda não sabe.
- 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.
Solução 13
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.
Mais Exercícios
Estes exercícios dão mais oportunidades para praticar os conceitos da página. Tente resolvê-los antes de abrir as soluções.
Reatribuição
-
Crie um objecto numérico. Crie depois um segundo objecto cujo valor seja o dobro do primeiro, sem voltar a escrever o valor original. Finalmente, faça com que o segundo volte a ter o valor do primeiro, mas sem usar o nome do primeiro objecto nessa última linha.
Solução 14
v1 <- 990 v2 <- v1 * 2 v2 <- v2 / 2 v1[1] 990v2[1] 990A atribuição
v2 <- v2 / 2é outro exemplo de reatribuição: o novo valor dev2é calculado a partir do valor anterior dev2.
Comparações e Indexação
-
Depois de criar:
dados <- c(10000, 200, 10, 0.5, -1)descubra:
- que valores são inferiores a 1;
- que valores são iguais a 0.5;
- que valores são iguais a 0.5 ou a 200;
- que valores são superiores a 10 e inferiores a 10000.
Primeiro obtenha os vectores de
TRUEeFALSE. Depois use-os para obter os próprios valores que satisfazem cada condição.Solução 15
dados < 1[1] FALSE FALSE FALSE TRUE TRUEdados == 0.5[1] FALSE FALSE FALSE TRUE FALSEdados == 0.5 | dados == 200[1] FALSE TRUE FALSE TRUE FALSEdados > 10 & dados < 10000[1] FALSE TRUE FALSE FALSE FALSEdados[dados < 1][1] 0.5 -1.0dados[dados == 0.5][1] 0.5dados[dados == 0.5 | dados == 200][1] 200.0 0.5dados[dados > 10 & dados < 10000][1] 200
Vectores e Coerção
-
Crie um vector com números, outro com palavras e um terceiro que tente combinar números e palavras. Use
class()etypeof()para os inspeccionar. O que aconteceu ao vector misto? Explique porquê.A comparação com
typeof()é opcional; na primeira leitura, basta usarclass().Solução 16
[1] "numeric"class(palavras)[1] "character"class(misto)[1] "character"typeof(numeros)[1] "double"typeof(palavras)[1] "character"typeof(misto)[1] "character"misto[1] "1" "dois" "3" "quatro"Um vector atómico precisa dum tipo comum. Como o texto não pode ser representado como número sem perder informação, os números de
mistosão convertidos para texto.
Ler e Indexar Dados
-
Execute:
idade <- c(21, 32, 24, 46, 33, 20, 50, 39, 18, 20) grupo <- factor(c("A", "B", "A", "B", "B", "B", "A", "A", "B", "A")) rts <- c(1435, 4352, 2232, 2820, 4552, 6454, 6500, 1005, 9001, 2003) dados <- data.frame(idade = idade, grupo = grupo, rts = rts)Obtenha os
rtsdas primeiras cinco observações, as idades das observações 3, 5 e 7, as três primeiras linhas das colunasidadeerts, e explique a diferença entreidade * 2eidade[2] * 2.Solução 17
rts[1:5][1] 1435 4352 2232 2820 4552idade[c(3, 5, 7)][1] 24 33 50dados[1:3, c("idade", "rts")]idade rts 21 1435 32 4352 24 2232 idade * 2[1] 42 64 48 92 66 40 100 78 36 40idade[2] * 2[1] 64A primeira multiplicação é vectorizada e actua sobre todas as idades. A segunda actua apenas sobre o segundo elemento.
Procurar Ajuda
Ler Código Desconhecido
-
Sem precisar de saber ainda ajustar um modelo linear, diga tudo o que consegue inferir apenas pela gramática deste código:
Solução 19
modelo <- rts ~ grupocria um objecto chamadomodelo. Mesmo sem conhecer~, conseguimos ver que não há aqui uma chamada a uma função, porque não aparecem parêntesis de chamada.Na segunda linha,
lm()é uma função. Recebe pelo menos dois argumentos nomeados:formula, cujo valor é o objectomodelo, edata, cujo valor é o objectodados. O resultado é guardado emfit.Na terceira linha,
fité passado asummary()e o resultado dessa chamada é guardado emresultados. Se quisermos descobrir o quelm()ousummary()fazem exactamente, podemos consultar?lme?summary.
Sintaxe e Semântica
-
Pense em dois erros que tenha cometido enquanto resolvia os exercícios: um erro de sintaxe e um erro em que o código era válido mas não fazia aquilo que pretendia. Explique a diferença.
Solução 20
Um erro de sintaxe viola a gramática da linguagem, por exemplo:
mean(c(1, 2, 3)Falta fechar um parêntesis e o R não consegue interpretar correctamente a expressão.
Um erro semântico ou, mais precisamente neste contexto, um erro de lógica pode ser código perfeitamente válido que faz a operação errada:
A sintaxe diz respeito a conseguirmos escrever uma expressão válida na linguagem. Ainda temos de verificar se essa expressão representa a operação que pretendíamos.