Aprender a falar R

Programação
Introdução
Aprender a escrever os primeiros comandos e a ler os erros do R.

0 Aprender a Falar R

Neste capítulo vamos acompanhar o leitor—sim, tu!—no seu primeiro encontro com o R. É um encontro de negócios, portanto é bom que o leitor não tente aproveitar-se do R. Se já usou o R, mas nunca o usou regularmente, este será um reencontro; se já tiver bastante experiência, pode saltar para o capítulo seguinte.

Como Usar Este Capítulo

Este capítulo pressupõe que o R já está a funcionar. Se ainda não instalou o R, ou nunca o usou e precisa de ajuda para escolher um programa onde escrever código, comece por Instalar o R. Aí explicamos a diferença entre o R e um editor ou IDE, antes de avançarmos para a sintaxe.

  1. Abra o R numa janela, lado a lado com esta página.

  2. Execute primeiro este pequeno exemplo. Quando vir blocos de código:

# Um comentário é ignorado pelo R.
cat("Hello World!\n")
Hello World!

escreva no R, sem copiar e colar, exactamente o que está no bloco.

  1. Evite copiar e colar o código para o R: escrever os comandos ajuda a praticar a sintaxe. Copie apenas quando um erro persistir.

  2. Se o código que escreveu der erro, e o capítulo não disser que esse erro era esperado, volte a escrevê-lo e tente perceber onde se enganou.

  3. Se o erro persistir, copie e cole apenas esse bloco. Se mesmo assim voltar a dar erro, pode enviar-me a mensagem de erro para joao.santos@ispa.pt.

Nota: este capítulo foi inspirado pela metodologia de ensino da série Learn Code The Hard Way de Zed A. Shaw.

1 As Primeiras Palavras

Ao longo deste capítulo encontrará sucessivos blocos de código.

Podemos começar pelo exemplo anterior:

# Um comentário é ignorado pelo R.
cat("Hello World!\n")
Hello World!

Na primeira linha temos um comentário. Um comentário é uma linha começada por # que o R ignora, isto é, que não tenta executar. Podemos portanto escrever nas linhas começadas por # o que quisermos sem ter de falar em código. A segunda linha invoca uma função chamada cat(). A sequência de caracteres "Hello World!\n" é o seu primeiro e único argumento. Se nada disto estiver a fazer muito sentido, não faz mal. Pode ser a primeira vez que encontra estes termos e vamos voltar a eles várias vezes.

O objectivo dos comentários é explicar o código. Neste tutorial, o primeiro comentário tentará descrever o que o código faz através duma analogia culinária. O segundo dará informação específica sobre aquele comando. O último traduzirá a ideia para uma linguagem mais técnica/computacional.

# Peça ao chef R a receita chamada mean.
# Queremos perceber como funciona mean().
# help() mostra a página do manual de uma função.
help(mean)

Quando escrever os seus próprios scripts de análise, deixe também comentários que expliquem os passos importantes. Assuma que o código vai ser lido por outra pessoa, nem que essa pessoa seja apenas o seu eu do futuro.

Nestes dois primeiros tutoriais, os comentários de código estão em português para que a própria sintaxe do R seja a principal novidade. A partir dos tutoriais seguintes, os comentários passam para inglês. É uma escolha deliberada: grande parte da documentação, dos exemplos de código e da colaboração entre equipas usa inglês, e convém ganhar esse hábito aos poucos.

You mostly collaborate with yourself, and me-from-two-months-ago never responds to email.

(attributed to Mark Holder)

Pronto para descobrir o R?

Vamos a isso!

Costuma dizer-se que o R é uma linguagem interpretada. Numa explicação introdutória, podemos contrastar isso com uma linguagem compilada: num compilador tradicional, traduzimos primeiro um programa para uma forma que a máquina possa executar. Num interpretador, escrevemos uma expressão e o interpretador trata de a executar naquele momento.

Esta distinção é útil, mas a realidade é menos arrumada. Interpretar e compilar não são opostos absolutos. Um interpretador pode traduzir código para uma representação intermédia ou bytecode e executá-la numa máquina virtual. O R faz precisamente algum deste trabalho: inclui um compilador de bytecode e compilação JIT (just-in-time) para certas partes do código. Por sua vez, muito do próprio R e das bibliotecas que usa está implementado em linguagens compiladas, como C e Fortran.

Podemos imaginar várias camadas:

código R
   ↓
interpretador / bytecode / JIT
   ↓
código nativo e bibliotecas compiladas
   ↓
instruções de máquina
   ↓
CPU

Mesmo esta figura simplifica. «Assembly» é uma representação textual, legível por humanos, de instruções muito próximas da arquitectura do processador. Nem todos os programas passam por essa etapa como texto: um compilador pode gerar directamente código de máquina.

E, no fundo, o processador não lê palavras como mean, ADD ou MOV. Executa instruções codificadas como padrões de bits, estados binários que representamos convencionalmente por zeros e uns. Há muitas camadas de abstracção entre o nosso mean(x) e esses bits. É precisamente isso que torna possível programar sem pensar continuamente na electrónica do processador.

Por isso, dizer simplesmente «R é interpretado» é uma boa primeira aproximação, não uma descrição completa de tudo o que acontece até o hardware executar o programa. Mais tarde veremos uma consequência prática: operações vectorizadas, código compilado e bibliotecas de álgebra linear podem transferir trabalho para camadas muito mais próximas do hardware.

# Peça ao chef R a receita help.
# Queremos consultar a página do manual de help().
# Chame help() e indique help como tópico.
help(help)

# O atalho ? abre a mesma página de ajuda.
?help

2 Variáveis e Objectos

A palavra variável parece mais técnica do que é. Uma variável é, literalmente, alguma coisa que pode variar: pode assumir mais do que um valor. Se só pudesse ter sempre o mesmo valor, seria uma constante. A definição matemática torna esta ideia mais precisa, mas não a torna mais misteriosa.

Em estatística, uma variável costuma ser uma característica medida, manipulada ou usada no delineamento do estudo, como idade, temperatura ou condição experimental. Os seus valores podem variar entre unidades ou observações e o seu papel depende da pergunta e do delineamento.

Em programação, variável costuma designar um nome associado a um valor que pode mudar durante a execução. No R, esses valores são objectos e os nomes ficam associados a esses objectos. Para não misturarmos os dois sentidos, neste site vamos preferir objecto e nome de objecto quando estivermos a falar de programação. Noutros textos de programação, é habitual encontrar variável com este sentido informático.

Às vezes os dois sentidos coincidem. Um objecto pode guardar uma coluna que representa uma variável estatística. Outras vezes não: modelo, resultado ou media_idade podem ser objectos úteis no script sem serem variáveis do estudo.

# Guarde o resultado de 3 + 2 num objecto chamado variavel.
variavel <- 3 + 2

# O R aceita este nome; quem ler o código mais tarde pode não agradecer.
fadfadsfasdf <- 4

# Este identificador com acento é válido nesta sessão.
média <- 2
Aviso

Os nomes têm regras: um nome comum não pode começar por um algarismo e muitos sinais de pontuação têm significado próprio no R. NA, por exemplo, é um nome reservado. O R moderno pode aceitar letras Unicode, como as de média, mas o conjunto exacto de letras reconhecidas num identificador depende da configuração regional (locale).

No NRB, recomendamos nomes com letras do alfabeto inglês, sem cedilhas nem acentos (ASCII), em snake_case, como media_idade. Esta convenção facilita a portabilidade, a pesquisa e a colaboração.

Um registo de 1969 do ASCII apresenta-o como um código de 7 bits para intercâmbio entre sistemas de informação e comunicação. A sua tabela limitada servia esse propósito de intercâmbio. Já existiam outras codificações, pelo que este registo cobre apenas uma parte da história da computação.

O Unicode foi desenvolvido mais tarde para dar uma codificação comum a sistemas de escrita muito mais numerosos e reduzir incompatibilidades entre codificações. No R actual, a definição dos identificadores continua a depender do locale, embora inclua letras acentuadas em muitas configurações. As operações com ficheiros também tratam caminhos com codificações marcadas, mas a conversão depende do sistema operativo e da codificação disponível.

Assim, média e dados_açores.csv podem funcionar. Preferimos media e dados_acores.csv quando os nomes terão de atravessar sistemas de ficheiros, terminais, editores e computadores diferentes. O texto guardado nos dados deve conservar os acentos de que precisa.

Por convenção, usamos <- para fazer atribuições. O operador = também pode atribuir em alguns contextos, mas aparece frequentemente a separar argumentos nomeados numa função. Enquanto aprendemos, <- torna mais fácil distinguir as duas situações.

Devemos também evitar nomes que já tenham um propósito no R. Por exemplo, um dado em falta no R é representado por NA.

# O R não permite atribuir desta forma a este nome reservado.
NA <- 2
Error in `NA <- 2`:
! invalid (do_set) left-hand side to assignment
# Combine vários valores num vector.
dados <- c(3, 2, 4, 6, 3, 2, 4, 1)

# Combine um vector que já existe com mais um valor.
dados <- c(dados, variavel)

# Compare um nome existente com um nome mal escrito, que dá erro.
fadfadsfasdf
[1] 4
fadfasdfasdf
Error:
! object 'fadfasdfasdf' not found
# Voltar a atribuir ao mesmo objecto substitui o valor anterior.
dados <- "teste"

# dados contém agora apenas a palavra teste.
dados
[1] "teste"

É como fazer uma mistura numa taça onde já estava outra coisa. Se quisermos usar a mesma taça, temos de deitar fora o que lá estava.

Se quisermos guardar o conteúdo dessa taça, temos de ir buscar outra. Da mesma forma, se não queremos alterar o valor dum objecto, podemos criar um novo.

# Mantenha dados inalterado e crie outro objecto.
novos_dados <- c(100, 100, 200, 154, 300)

# Guarde a média de novos_dados.
resultados <- mean(novos_dados)

O que faz a função/receita mean()? Usemos a função help() para descobrir.

help(mean)

Vemos que mean() calcula a média do objecto que lhe dermos.

# resultados contém agora a média de novos_dados.
resultados
[1] 170.8

Mas afinal o que são estes “argumentos” que pomos nas funções?

As funções são como receitas e normalmente levam vários ingredientes. Os argumentos das funções são esses ingredientes.

Muitas vezes só temos de especificar um ou dois. Para os restantes, a função já tem valores por defeito.

É como pedirmos a um chef para fazer um bitoque de vaca. Temos de dizer, por exemplo, o ponto da carne. Tirando isso, ele fará o resto do bitoque como costuma fazer. Se quisermos outro molho ou um ovo a cavalo, temos de pedir.

Se o R fizesse bitoques, seria algo assim:

# Esta função é deliberadamente inventada.
# Para pedir a carne mal passada, indicaríamos esse valor no argumento cooked_to.
bitoque(cooked_to = "medium-rare")
Error in `bitoque()`:
! could not find function "bitoque"
# egg = TRUE pediria um ovo a cavalo.
bitoque(cooked_to = "medium-rare", egg = TRUE)
Error in `bitoque()`:
! could not find function "bitoque"

Como viu, o R deu erro porque não tem essa função. Mesmo que reescreva o código ou copie e cole, continuará sem conseguir fazer um bitoque. Concluímos portanto que o R, infelizmente, não faz bitoques.

Vamos então ver outros exemplos do que ele faz.

# Calcule uma média aparada.
resultados_robustas <- mean(x = novos_dados, trim = 0.2)

Podemos usar o output de uma função como argumento de outra. Por exemplo, c() cria o vector que mean() recebe:

outra_media <- mean(c(500, 300, 600, 300, 400))

Chamadas encastradas curtas são fáceis de ler. Quando a sequência cresce, podemos criar objectos intermédios para inspeccionar, reutilizar ou depurar cada passo.

3 Ler uma Sequência com o Pipe Nativo

O pipe nativo permite escrever uma sequência de transformações de cima para baixo:

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

Em inglês, pipe significa «tubo»: |> transporta o valor produzido à esquerda para a função seguinte. Podemos lê-lo como «e depois». Por defeito, esse valor ocupa o primeiro argumento da função à direita.

A cadeia anterior corresponde à chamada encastrada abaixo:

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

A documentação oficial de |> descreve o caso simples como uma transformação sintáctica: ao analisar x |> f(y), o R constrói f(x, y). Para já, basta conseguir comparar a cadeia com a chamada encastrada acima.

Usamos o pipe nativo do R, disponível desde a versão 4.1 e sem necessidade de pacotes. Poderá encontrar %>% em código baseado no tidyverse, mas é outro operador. O guia de estilo do tidyverse para pipes também recomenda |> para código novo e desaconselha cadeias quando há objectos intermédios a que vale a pena dar nomes.

Escolhemos a organização que torna o cálculo mais claro:

  • encastramos expressões curtas;
  • criamos objectos intermédios para inspeccionar ou reutilizar resultados;
  • usamos |> em sequências com uma direcção clara.
NotaO Que É o Estilo de Código?

O estilo de código reúne escolhas sobre espaços, indentação, nomes e mudanças de linha. Não altera o cálculo, mas pode poupar bastante tempo a quem tenta percebê-lo.

  • Num projecto colaborativo, respeitamos primeiro o estilo que já existe.
  • Uma equipa pode adoptar um guia, como o Tidyverse style guide.
  • Onde o projecto ou o guia nada disserem, desenvolvemos preferências com a prática.

A referência sobre funções desenvolve estas ideias. Aí veremos com mais calma como usar outputs como argumentos e como passar funções como argumentos de outras funções.

O R é uma linguagem formal.

Uma linguagem formal define símbolos e regras para construir expressões válidas. Ao contrário do que fazemos numa conversa, o R não tenta adivinhar que expressão queríamos escrever.

Podemos cometer um erro de sintaxe: escrever uma expressão que não respeita as regras gramaticais do R. No exemplo seguinte fechamos parêntesis com um parêntesis recto.

# O R não consegue interpretar esta expressão.
mean(novos_dados]
Error in parse(text = input): <text>:2:17: unexpected ']'
1: # O R não consegue interpretar esta expressão.
2: mean(novos_dados]
                   ^

Também podemos escrever uma expressão sintacticamente válida que tenta usar um nome inexistente.

# A chamada é sintacticamente válida, mas meam() não existe.
meam(novos_dados)
Error in `meam()`:
! could not find function "meam"

O R tem uma função para calcular a média, mas chama-se mean() e não meam(). O interpretador, o chef que o R traz consigo, não corrigiu o nosso lapso: tentou encontrar a receita meam() e informou-nos de que esse objecto não existia. Este é um erro de nome ou de avaliação, não um erro de sintaxe.

Em conclusão, temos de estar atentos às receitas que escrevemos. Como se diz na tropa: o material tem sempre razão.

Temos então uma boa e uma má notícia. A má notícia é que temos de ter cuidado a escrever código. A boa é que, quando nos enganamos, o R costuma deixar pistas na mensagem de erro. Num script com muitas linhas, essa mensagem também pode indicar a linha onde o problema foi encontrado. Depois ainda temos de a ler, claro.

Como um pequeno erro pode impedir a execução dum script, é boa ideia testar o código à medida que o escrevemos. O carácter interactivo do R torna isto particularmente fácil: podemos experimentar uma expressão na consola, ver o resultado imediatamente e só depois acrescentar ao script a versão que queremos conservar.

Quando surge um erro, lemos a mensagem, tentamos perceber o que está mal, fazemos uma alteração e voltamos a executar. Se continuar a falhar, repetimos. Uma alteração de cada vez. Assim conseguimos perceber o que corrigiu o problema, em vez de mudar várias coisas ao mesmo tempo até o erro desaparecer sem sabermos porquê.

AvisoA Consola Não É o Script

A consola é onde conversamos directamente com o interpretador: escrevemos uma expressão e o R avalia-a imediatamente. Um script é um ficheiro de texto que guarda uma sequência de instruções para podermos voltar a executá-la, partilhá-la e reproduzir a análise.

É perfeitamente normal usar a consola como rascunho enquanto trabalhamos. O problema aparece quando o resultado final só funciona por causa de coisas que executámos manualmente e que nunca ficaram registadas no script.

Editores como o RStudio tornam muito fácil executar uma linha ou selecção fora de ordem. Isso é útil para explorar e testar código, mas também permite criar sem querer objectos que uma linha posterior usa antes de o script os criar. O código parece funcionar na nossa sessão e falha quando outra pessoa o executa desde o princípio.

Por isso, um script terminado deve poder ser executado de cima para baixo, por ordem, numa sessão nova do R. Esta é a prova importante. Teste-o assim regularmente.

Durante o trabalho, saltar entre linhas é útil. No fim, o script tem de conter tudo aquilo de que depende, pela ordem em que é necessário. Nas disciplinas em que usamos estes materiais, código que dependa do estado anterior da consola ou duma ordem de execução não registada pode ser penalizado na avaliação. Outra pessoa deve conseguir reproduzir a análise. E o nosso eu do futuro também.

Se ficarmos presos, podemos procurar a mensagem de erro e ver como outras pessoas diagnosticaram problemas semelhantes.

O processo de detectar e corrigir erros no código chama-se debugging.

Há, no entanto, um tipo de erro mais difícil. O R avisa-nos quando não consegue analisar uma expressão ou encontrar uma função. Não consegue avisar-nos sempre que o código executou perfeitamente aquilo que escrevemos, mas aquilo que escrevemos não era o que queríamos fazer. A estes podemos chamar erros semânticos.

# Um erro semântico óbvio.
media_novos_dados <- sum(novos_dados) / novos_dados

Neste caso, o R não dá erro, mas o resultado não é a média que pretendíamos.

# Compare o nosso cálculo com mean().
media_novos_dados == mean(novos_dados)
[1] FALSE FALSE FALSE FALSE FALSE

O resultado é FALSE: o nosso cálculo não coincide com mean().

Qual será o problema?

A média é a soma das observações dividida pelo número de observações. Nós dividimos a soma pelas próprias observações. Agora que encontrámos o erro, podemos corrigi-lo.

# length() conta os elementos do vector.
# Use-a para corrigir o cálculo.
media_novos_dados <- sum(novos_dados) / length(novos_dados)

# Compare o cálculo corrigido com a média calculada pelo R.
media_novos_dados == mean(novos_dados)
[1] TRUE

Agora o R devolve TRUE: o nosso resultado coincide com a média calculada por mean().

Nem todos os casos serão tão óbvios, nem teremos sempre uma função do R com a qual comparar o resultado. Às vezes podemos até usar correctamente uma função para responder à pergunta errada.

É por isso que os erros semânticos precisam de nós.

Então o R é pior do que o SPSS e os outros programas?

Não. Nesses programas também podemos escolher um teste quando queríamos outro, seleccionar a variável errada ou alterar um valor sem querer. Como no R escrevemos os passos em vez de apenas clicar em menus, ficamos com um registo que podemos voltar a ler e testar. Isso pode até tornar alguns erros mais fáceis de encontrar.

Se está a começar, uma boa estratégia é tentar primeiro replicar no R análises simples que já fez noutros programas. Assim aprende a linguagem sem ter de aprender ao mesmo tempo uma análise completamente nova.

Agora que já percebemos o funcionamento básico do R, resta praticar: escrever receitas, ler as que outras pessoas escreveram e, aos poucos, aprender a falar R.