3  Introdução à Linguagem R

R é uma das linguagens mais populares para estatística e análise de dados. Neste capítulo, estudamos a sua origem histórica, as suas sintaxes fundamentais, o seu sistema de tipos, as estruturas de dados básicas, os primeiros passos de programação (controle de fluxo e funções) e, por fim, o ecossistema tidyverse para manipulação moderna de dados. O objetivo é que, ao final, o leitor seja capaz de ler, escrever e raciocinar sobre código R com a mesma naturalidade com que lê uma demonstração matemática.

3.1 O que é o R?

A linguagem R é um ambiente computacional de código aberto voltado especificamente para a computação estatística, análise e visualização de dados (R Core Team 2024). Criada inicialmente por Ross Ihaka e Robert Gentleman na Universidade de Auckland, na Nova Zelândia (Ihaka e Gentleman 1996), o R é uma implementação de código aberto da linguagem S, originalmente desenvolvida pelos laboratórios Bell da AT&T. Ao longo das décadas, o R evoluiu para se tornar um padrão global tanto na academia quanto na indústria de tecnologia.

3.1.1 Uma Breve História: do S ao R

A história do R começa na década de 1970, nos Bell Laboratories, o mesmo centro de pesquisa que deu origem ao sistema operacional Unix e à linguagem C. Lá, John Chambers e colaboradores desenvolveram a linguagem S, cujo objetivo declarado era “transformar ideias em software, de forma rápida e fiel” — isto é, permitir que o estatístico passasse de uma pergunta sobre os dados a uma resposta computacional sem ter de escrever longos programas em Fortran. A versão conhecida como New S foi descrita por Becker, Chambers, e Wilks (1988) e introduziu muitas das ideias que reconhecemos hoje no R: funções como objetos de primeira classe, vetores como estrutura básica e um ambiente interativo de análise. Em 1998, John Chambers recebeu o prêmio Software System Award da ACM pela linguagem S, que, segundo a premiação, “mudou para sempre a forma como as pessoas analisam, visualizam e manipulam dados”.

No início dos anos 1990, Ihaka e Gentleman começaram a escrever um interpretador inspirado no S, mas com uma semântica de escopo emprestada da linguagem Scheme (um dialeto de Lisp). O nome “R” é, ao mesmo tempo, uma brincadeira com a letra anterior (“S”) e uma referência às iniciais dos dois autores. O artigo que apresentou a linguagem à comunidade foi publicado em 1996 (Ihaka e Gentleman 1996). Em 1997 formou-se o R Core Team, grupo responsável pelo desenvolvimento do núcleo da linguagem, e em 29 de fevereiro de 2000 foi lançada a versão 1.0.0. Desde então, a linguagem é mantida pela R Foundation for Statistical Computing, sediada em Viena.

Ano Marco
1976 Início do desenvolvimento do S nos Bell Labs (John Chambers e colaboradores)
1988 Publicação do New S Language (Becker, Chambers, e Wilks 1988)
1993 Primeiros anúncios públicos do R por Ihaka e Gentleman
1996 Artigo “R: A Language for Data Analysis and Graphics” (Ihaka e Gentleman 1996)
1997 Criação do R Core Team e do CRAN
2000 Lançamento do R 1.0.0
2011 Lançamento do RStudio (IDE)
2016 Consolidação do tidyverse como coleção de pacotes
2021 Introdução do pipe nativo no R 4.1.0

Essa herança explica várias peculiaridades da linguagem que estudaremos neste capítulo: o R é, ao mesmo tempo, uma linguagem funcional (herdada do Scheme), vetorizada (herdada do S e da tradição de álgebra matricial) e orientada a objetos de maneira pouco convencional (os sistemas S3, S4 e, mais recentemente, R6 e S7). Para uma discussão aprofundada do desenho da linguagem, recomenda-se Chambers (2008) e Wickham (2019).

3.1.2 R como Software Livre

Um dos aspectos mais importantes do R é o seu licenciamento como Software Livre (sob a licença GNU GPL). A filosofia do Software Livre foi idealizada por Richard Stallman, fundador da Free Software Foundation (FSF) e criador do projeto GNU, que frequentemente trabalha em conjunto com o kernel Linux (criado por Linus Torvalds). Para que um software seja considerado livre, ele deve garantir aos seus usuários quatro liberdades fundamentais:

  1. A liberdade de executar o programa como desejar, para qualquer propósito.
  2. A liberdade de estudar como o programa funciona e adaptá-lo às suas necessidades (o que exige acesso ao código-fonte).
  3. A liberdade de redistribuir cópias do programa para ajudar o próximo.
  4. A liberdade de melhorar o programa e distribuir suas melhorias para o público, beneficiando toda a comunidade.

O grande sucesso do R e sua ampla adoção devem-se fortemente a essa filosofia, e aos seguintes pilares:

  1. Comunidade Ativa e Colaborativa: Devido às liberdades do software livre, milhares de pacotes foram desenvolvidos por colaboradores do mundo todo e disponibilizados no CRAN (Comprehensive R Archive Network).
  2. Capacidade de Visualização: Excelentes sistemas gráficos integrados e pacotes especializados de alta qualidade, como o ggplot2.
  3. Flexibilidade: Facilidade na manipulação, limpeza e modelagem estatística de dados estruturados e complexos.

Do ponto de vista científico, o fato de o código-fonte ser aberto tem uma consequência importante: qualquer pesquisador pode auditar exatamente como um estimador, um teste ou um algoritmo está implementado. Isso é essencial para a reprodutibilidade, tema que será retomado no Capítulo de Documentos Dinâmicos com Markdown e RMarkdown.

3.1.3 R como Linguagem Interpretada

O R é uma linguagem interpretada: os comandos são lidos, analisados e executados um a um por um programa chamado interpretador, sem uma etapa prévia de compilação para código de máquina. Esse funcionamento é frequentemente descrito pelo ciclo REPL (Read–Eval–Print Loop):

  1. Read — o interpretador lê a expressão digitada (por exemplo, 2 + 3);
  2. Eval — avalia a expressão no ambiente corrente;
  3. Print — imprime o resultado ([1] 5);
  4. Loop — volta a aguardar a próxima expressão.

A vantagem é a interatividade, fundamental para a análise exploratória; a desvantagem é que laços explícitos escritos em R puro tendem a ser mais lentos do que o código equivalente em C ou Fortran. É justamente por isso que a linguagem incentiva fortemente a vetorização, como veremos adiante: muitas funções vetorizadas do R são, internamente, laços escritos em C.


3.2 O Ambiente de Desenvolvimento: RStudio

Embora o R possa ser executado diretamente no terminal, o desenvolvimento é feito majoritariamente no RStudio, o principal ambiente de desenvolvimento integrado (Integrated Development Environment, IDE) para R. O RStudio é dividido estruturalmente em quatro painéis essenciais:

  • Editor de Scripts (Superior Esquerdo): Local para escrever seus scripts que serão salvos em arquivos .R ou .Rmd.
  • Console (Inferior Esquerdo): Onde o interpretador do R de fato roda. Você pode digitar comandos diretamente aqui para execução imediata.
  • Ambiente / Histórico (Superior Direito): Lista todas as variáveis, matrizes, data frames e objetos carregados na memória do seu computador.
  • Arquivos, Plots, Pacotes e Ajuda (Inferior Direito): Painel multiuso onde você visualiza gráficos gerados, instala novos pacotes e lê os arquivos de ajuda das funções do R.

Alguns atalhos de teclado economizam muito tempo no dia a dia: Ctrl + Enter executa a linha (ou seleção) corrente do script; Ctrl + Shift + M insere o operador pipe; Alt + - insere o operador de atribuição <-; Ctrl + Shift + C comenta/descomenta linhas; e Tab aciona o autocompletar.

3.2.1 Projetos do RStudio e Diretório de Trabalho

Todo processo R possui um diretório de trabalho (working directory), que é a pasta a partir da qual caminhos relativos de arquivos são resolvidos. Ele pode ser consultado com getwd() e alterado com setwd(). Entretanto, o uso de setwd("C:/Users/fulano/...") no início de scripts é considerado uma má prática, pois torna o código dependente do computador de quem o escreveu.

A solução recomendada é organizar cada análise em um Projeto do RStudio (arquivo .Rproj). Ao abrir um projeto, o diretório de trabalho é automaticamente definido como a pasta raiz do projeto, de modo que caminhos relativos como dados/vendas.csv funcionam em qualquer máquina. Uma estrutura típica de projeto é:

meu_projeto/
|-- meu_projeto.Rproj
|-- dados/
|   |-- brutos/        # dados originais, nunca editados à mão
|   `-- processados/   # dados gerados pelos scripts
|-- R/                 # funções auxiliares
|-- scripts/           # scripts de análise numerados (01_limpeza.R, 02_modelo.R)
|-- figuras/
`-- relatorio.qmd

Essa organização combina naturalmente com o controle de versão via Git, discutido no Capítulo de Controle de Versão com Git e GitHub.

3.2.2 Obtendo Ajuda

Saber pedir ajuda ao próprio R é uma das competências mais valiosas para o iniciante. As principais formas são:

?mean                  # abre a documentação da função mean
help("mean")           # equivalente ao comando acima
??"regression"         # busca o termo em toda a documentação instalada
help.search("linear model")
example(mean)          # executa os exemplos da documentação
args(lm)               # mostra os argumentos de uma função
vignette("dplyr")      # abre um tutorial longo (vinheta) de um pacote
apropos("^read")       # lista objetos cujo nome casa com a expressão regular

Toda página de ajuda segue a mesma estrutura: Description, Usage, Arguments, Details, Value, References, See Also e Examples. Ler a seção Value — que descreve o que a função retorna — evita grande parte dos erros de iniciantes.


3.3 Fundamentos da Linguagem

3.3.1 Atribuição de Variáveis

No R, o operador padrão de atribuição de valor a uma variável é a seta para a esquerda (<-). Embora o sinal de igualdade comum (=) funcione em muitos contextos, a boa prática e o padrão da comunidade recomendam o uso do operador seta:

# Criando variáveis e atribuindo valores
idade <- 25
nome <- "Alice"
usuario_ativo <- TRUE

O R é uma linguagem dinamicamente tipada, o que significa que você não precisa declarar se a variável é numérica, texto ou lógica. O próprio interpretador identifica o tipo do dado.

A distinção entre <- e = não é apenas estética. Dentro da chamada de uma função, = é usado para associar argumentos por nome, enquanto <- realiza uma atribuição no ambiente de quem chama:

mean(x = c(1, 2, 3))   # 'x' é o nome do argumento; nenhuma variável x é criada
mean(x <- c(1, 2, 3))  # cria a variável x no ambiente global e passa seu valor
x
#> [1] 1 2 3

Nomes de objetos podem conter letras, dígitos, . e _, mas não podem começar com dígito nem com _. O R diferencia maiúsculas de minúsculas (Idade e idade são objetos distintos). Recomenda-se o estilo snake_case (taxa_juros, media_amostral), conforme o guia de estilo do tidyverse.

3.3.2 Tipos Atômicos de Dados

Todo valor em R é armazenado em um vetor; mesmo o número 25 é, na verdade, um vetor de comprimento 1. Os vetores atômicos possuem um único tipo (type), consultado por typeof(). Os seis tipos atômicos são:

Tipo (typeof) Exemplo Descrição
logical TRUE, FALSE, NA Valores lógicos (booleanos)
integer 25L, 1:10 Inteiros de 32 bits (o sufixo L força o tipo)
double 3.14, 25, 1e-8 Números reais em ponto flutuante de dupla precisão
complex 2+3i Números complexos
character "Alice", 'R' Cadeias de caracteres (strings)
raw as.raw(255) Bytes brutos (uso raro)

Os tipos integer e double são conjuntamente chamados de numéricos (is.numeric() retorna TRUE para ambos). Note que um literal como 25 é double, e não integer:

typeof(25)      #> [1] "double"
typeof(25L)     #> [1] "integer"
typeof(1:3)     #> [1] "integer"
typeof("25")    #> [1] "character"
class(25)       #> [1] "numeric"

3.3.2.1 Representação em Ponto Flutuante

Os valores double seguem o padrão IEEE 754 de dupla precisão: um número é representado como

\[ x = (-1)^s \times (1.f)_2 \times 2^{e - 1023}, \]

em que \(s\) é um bit de sinal, \(f\) é a mantissa com 52 bits e \(e\) é o expoente com 11 bits. Consequentemente, há cerca de 15 a 16 dígitos decimais significativos, e muitos números decimais simples não possuem representação exata em base 2. O exemplo clássico é:

0.1 + 0.2 == 0.3
#> [1] FALSE
print(0.1 + 0.2, digits = 17)
#> [1] 0.30000000000000004
isTRUE(all.equal(0.1 + 0.2, 0.3))   # compara com tolerância
#> [1] TRUE
.Machine$double.eps                 # épsilon da máquina
#> [1] 2.220446e-16

O épsilon da máquina \(\varepsilon = 2^{-52} \approx 2{,}22 \times 10^{-16}\) é a menor quantidade tal que \(1 + \varepsilon \neq 1\) na aritmética do computador. Por isso, comparações de igualdade entre números reais devem ser feitas com uma tolerância, isto é, verificando se \(|a - b| < \delta\) para um \(\delta\) pequeno — que é o que all.equal() faz.

Exemplo resolvido: por que 0,1 não é representável. Para escrever \(0{,}1\) em base 2, multiplicamos repetidamente a parte fracionária por 2 e anotamos a parte inteira obtida:

Passo Operação Parte inteira (bit) Parte fracionária
1 \(0{,}1 \times 2 = 0{,}2\) 0 0,2
2 \(0{,}2 \times 2 = 0{,}4\) 0 0,4
3 \(0{,}4 \times 2 = 0{,}8\) 0 0,8
4 \(0{,}8 \times 2 = 1{,}6\) 1 0,6
5 \(0{,}6 \times 2 = 1{,}2\) 1 0,2
6 \(0{,}2 \times 2 = 0{,}4\) 0 0,4 (repete o passo 2)

A partir do passo 6 o processo entra em ciclo, de modo que \[ 0{,}1 = (0{,}0\overline{0011})_2 = (1{,}\overline{1001})_2 \times 2^{-4}, \] uma dízima periódica em base 2. Como a mantissa tem apenas 52 bits, a dízima é arredondada, e o valor efetivamente armazenado é ligeiramente maior que \(0{,}1\):

sprintf("%.20f", 0.1)
#> [1] "0.10000000000000000555"

Esse é o mesmo fenômeno que ocorre com \(1/3 = 0{,}333\ldots\) em base 10: um número com representação finita em uma base pode ter representação infinita em outra. Somente frações cujo denominador é uma potência de 2 (como \(0{,}5\), \(0{,}25\) e \(0{,}375\)) são representadas exatamente.

3.3.2.2 Erro de Arredondamento e Espaçamento entre Números

Os números de ponto flutuante não estão igualmente espaçados na reta real. No intervalo \([2^k, 2^{k+1})\) há exatamente \(2^{52}\) números representáveis, separados pela distância \(2^{k-52}\) (a chamada unit in the last place, ou ulp). Assim, o espaçamento cresce com a magnitude do número, enquanto o espaçamento relativo permanece aproximadamente constante. Esse fato é formalizado pelo modelo padrão da aritmética de ponto flutuante (Goldberg 1991; Higham 2002): se \(\mathrm{fl}(x)\) denota o número de máquina mais próximo de \(x\), então \[ \mathrm{fl}(x) = x\,(1 + \delta), \qquad |\delta| \le u = \frac{\varepsilon}{2} = 2^{-53} \approx 1{,}11 \times 10^{-16}, \] em que \(u\) é a unidade de arredondamento. Além disso, o padrão IEEE 754 garante que cada operação elementar (\(+\), \(-\), \(\times\), \(/\) e \(\sqrt{\cdot}\)) é calculada como se fosse exata e depois arredondada: \(\mathrm{fl}(a \circ b) = (a \circ b)(1 + \delta)\), com \(|\delta| \le u\).

Uma consequência curiosa é que, a partir de \(2^{53} \approx 9{,}007 \times 10^{15}\), o espaçamento entre doubles consecutivos passa a ser 2, e os inteiros deixam de ser todos representáveis:

2^53 == 2^53 + 1        #> [1] TRUE
sqrt(2)^2 == 2          #> [1] FALSE
isTRUE(all.equal(sqrt(2)^2, 2))   #> [1] TRUE
all.equal(1, 1.1)       #> [1] "Mean relative difference: 0.1"
seq(0, 1, by = 0.1)[4] - 0.3      #> [1] 5.551115e-17

Note que all.equal() retorna TRUE ou uma descrição da diferença (e não FALSE); por isso, em condições de if, deve-se sempre envolvê-la em isTRUE(). A tolerância padrão é relativa e vale aproximadamente \(1{,}5 \times 10^{-8}\), isto é, \(\sqrt{\varepsilon}\).

3.3.2.3 Limites da Representação: Overflow e Underflow

Como o expoente tem 11 bits, há um maior e um menor número positivo representáveis. Valores acima do máximo produzem Inf (overflow) e valores muito próximos de zero são arredondados para 0 (underflow). Os inteiros de 32 bits têm limites bem mais modestos, e o overflow inteiro produz NA com um aviso:

Quantidade Valor em R Aproximação
Maior double finito .Machine$double.xmax \(1{,}797693 \times 10^{308}\)
Menor double normalizado .Machine$double.xmin \(2{,}225074 \times 10^{-308}\)
Menor double subnormal 2^-1074 \(4{,}940656 \times 10^{-324}\)
Maior inteiro (integer) .Machine$integer.max \(2^{31} - 1 = 2\,147\,483\,647\)
Épsilon da máquina .Machine$double.eps \(2{,}220446 \times 10^{-16}\)
1e308 * 10                    #> [1] Inf
.Machine$integer.max + 1L     #> [1] NA
#> Warning message: NAs produced by integer overflow
prod(rep(0.01, 200))          #> [1] 0       (underflow: o valor exato é 1e-400)
sum(log(rep(0.01, 200)))      #> [1] -921.034

O último exemplo tem enorme importância em Estatística. A verossimilhança de uma amostra independente, \(L(\theta) = \prod_{i=1}^n f(x_i; \theta)\), é um produto de muitos números menores que 1 e sofre underflow rapidamente: para 1.000 observações normais padrão simuladas, prod(dnorm(y)) retorna exatamente 0, enquanto a log-verossimilhança sum(dnorm(y, log = TRUE)) vale cerca de \(-1454\), um número perfeitamente representável. É por isso que, na prática, maximiza-se o logaritmo da verossimilhança, e as funções de densidade do R possuem o argumento log = TRUE.

O truque log-sum-exp

Às vezes é preciso calcular \(\log \sum_i e^{a_i}\) a partir de valores \(a_i\) muito negativos (por exemplo, log-probabilidades). Calcular log(sum(exp(a))) diretamente produz -Inf, pois cada \(e^{a_i}\) sofre underflow. A identidade \[ \log \sum_{i} e^{a_i} = m + \log \sum_{i} e^{a_i - m}, \qquad m = \max_i a_i, \] resolve o problema, pois o maior termo da nova soma é \(e^0 = 1\). Para a <- c(-1000, -1001, -1002), a forma ingênua retorna -Inf, enquanto max(a) + log(sum(exp(a - max(a)))) retorna \(-999{,}5924\).

3.3.2.4 Cancelamento Catastrófico e Estabilidade Numérica

O erro de arredondamento de uma única operação é minúsculo, mas certos algoritmos o amplificam. O caso mais perigoso é a subtração de dois números quase iguais, chamada de cancelamento catastrófico: se \(a\) e \(b\) já carregam erros relativos da ordem de \(u\), a diferença \(a - b\) pode ter erro relativo da ordem de \(u \cdot |a| / |a - b|\), que é enorme quando \(a \approx b\). Dizemos que um algoritmo é numericamente estável quando não amplifica de maneira desnecessária os erros de arredondamento.

Exemplo resolvido: duas fórmulas para a variância. Algebricamente, a variância amostral admite duas expressões equivalentes: \[ s^2 = \frac{1}{n-1}\sum_{i=1}^n (x_i - \bar{x})^2 \qquad \text{e} \qquad s^2 = \frac{1}{n-1}\left(\sum_{i=1}^n x_i^2 - n\bar{x}^2\right). \] A segunda, chamada de fórmula “de uma passada”, é tentadora porque permite acumular \(\sum x_i\) e \(\sum x_i^2\) em um único percurso pelos dados. Considere, porém, \(\mathbf{x} = (10^9 + 4,\ 10^9 + 7,\ 10^9 + 13,\ 10^9 + 16)\), como medições com um grande deslocamento comum (offset). À mão: \(\bar{x} = 10^9 + 10\), os desvios são \((-6, -3, 3, 6)\), a soma de quadrados é \(36 + 9 + 9 + 36 = 90\) e, portanto, \(s^2 = 90/3 = 30\).

Na fórmula de uma passada, cada \(x_i^2 \approx 10^{18}\). Como \(2^{59} < 10^{18} < 2^{60}\), o espaçamento entre doubles nessa região é \(2^{59 - 52} = 128\): cada quadrado é armazenado com um erro que pode chegar a 64 — maior que a própria resposta exata, 90. A subtração final \(\sum x_i^2 - n\bar{x}^2\) cancela os 18 dígitos iniciais e deixa apenas ruído:

x <- c(1e9 + 4, 1e9 + 7, 1e9 + 13, 1e9 + 16)
n <- length(x)
(sum(x^2) - n * mean(x)^2) / (n - 1)    # fórmula de uma passada
#> [1] 0
sum((x - mean(x))^2) / (n - 1)          # fórmula de duas passadas
#> [1] 30
var(x)
#> [1] 30

O resultado 0 é completamente errado (em Python, como veremos no próximo capítulo, a mesma fórmula chega a produzir uma variância negativa). A fórmula de duas passadas, usada internamente por var(), primeiro subtrai a média — eliminando o deslocamento comum — e só depois eleva ao quadrado números pequenos. Chan, Golub, e LeVeque (1983) mostram que, sendo \(S = \sum (x_i - \bar{x})^2\) e \(\kappa = \|\mathbf{x}\|_2 / \sqrt{S}\) o número de condição do problema, o erro relativo da fórmula de uma passada pode ser da ordem de \(n\kappa^2 u\), enquanto o da fórmula de duas passadas é da ordem de \(nu + n^2\kappa^2u^2\). Aqui, \(\kappa \approx 2 \times 10^9/\sqrt{90} \approx 2{,}1 \times 10^8\) e \(n\kappa^2 u \approx 20\): o erro relativo pode chegar a 2.000%. Quando é preciso processar os dados em uma única passada (por exemplo, em fluxos contínuos), usa-se o algoritmo de Welford (1962), que atualiza a média e a soma de quadrados de forma estável a cada nova observação — ele será implementado no próximo capítulo.

Equivalência algébrica não é equivalência numérica

Duas fórmulas matematicamente idênticas podem produzir resultados muito diferentes no computador. Sempre que possível, use as funções nativas (var(), sd(), lm(), solve()), que foram escritas por especialistas em análise numérica, e desconfie de fórmulas “de atalho” encontradas em livros de cálculo manual.

3.3.2.5 Coerção de Tipos

Como um vetor atômico só pode ter um tipo, quando combinamos valores de tipos diferentes o R aplica uma coerção implícita para o tipo mais “flexível”, segundo a hierarquia

\[ \texttt{logical} \;\prec\; \texttt{integer} \;\prec\; \texttt{double} \;\prec\; \texttt{complex} \;\prec\; \texttt{character}. \]

c(TRUE, 2L)        #> [1] 1 2          (logical -> integer)
c(1L, 2.5)         #> [1] 1.0 2.5      (integer -> double)
c(1, "a")          #> [1] "1" "a"      (double -> character)
c(TRUE, "a")       #> [1] "TRUE" "a"

Na coerção de logical para numérico, TRUE vira 1 e FALSE vira 0. Essa regra é extremamente útil na prática: se x é um vetor lógico, então sum(x) conta quantos elementos são verdadeiros e mean(x) fornece a proporção de verdadeiros. Formalmente, para \(x_i \in \{0, 1\}\),

\[ \bar{x} = \frac{1}{n}\sum_{i=1}^{n} x_i = \frac{\#\{i : x_i = 1\}}{n}. \]

notas <- c(8.5, 4.0, 7.2, 5.9, 9.1)
aprovado <- notas >= 6
sum(aprovado)    #> [1] 3   (número de aprovados)
mean(aprovado)   #> [1] 0.6 (proporção de aprovados)

A coerção explícita é feita pelas funções da família as.*(). Quando a conversão é impossível, o resultado é NA, acompanhado de um aviso:

as.numeric("3.14")      #> [1] 3.14
as.integer(3.9)         #> [1] 3   (trunca em direção a zero)
as.logical(c("TRUE", "T", "sim"))
#> [1]  TRUE  TRUE    NA
as.numeric("abc")
#> [1] NA
#> Warning message: NAs introduced by coercion

3.3.3 Operadores Básicos

O R suporta as operações fundamentais nativamente:

  • Aritméticos: Adição (+), Subtração (-), Multiplicação (*), Divisão (/), Potenciação (^ ou **), Resto da divisão (%%) e Divisão inteira (%/%).
  • Relacionais (Comparativos): Igual a (==), Diferente de (!=), Maior que (>), Menor que (<), Maior ou igual (>=), Menor ou igual (<=).
  • Lógicos: E lógico (&), OU lógico (|), Negação (!).

Os operadores %% e %/% satisfazem a identidade da divisão euclidiana: para \(a\) e \(b \neq 0\),

\[ a = b \cdot (a \,\%/\%\, b) + (a \,\%\%\, b). \]

Por exemplo, 17 %/% 5 vale 3 e 17 %% 5 vale 2, pois \(17 = 5 \cdot 3 + 2\). Um uso comum de %% é testar paridade: x %% 2 == 0.

Há ainda os operadores lógicos de curto-circuito && e ||, que operam apenas sobre valores escalares e avaliam o segundo operando somente se necessário. Eles devem ser usados em condições de if, enquanto & e | são vetorizados e adequados para filtrar vetores:

x <- c(1, 5, 10)
x > 2 & x < 8        #> [1] FALSE  TRUE FALSE   (elemento a elemento)
length(x) > 0 && x[1] > 0   # curto-circuito: seguro em condições

A precedência dos operadores segue a convenção matemática: ^ antes de - unário, depois * e /, depois + e -, depois comparações e, por último, operadores lógicos. Uma armadilha conhecida é -2^2, que vale -4 (e não 4), e 1:n-1, que equivale a (1:n) - 1, pois : tem precedência maior que - binário.


3.4 Vetorização e Reciclagem

3.4.1 Operações Vetorizadas

Uma operação é vetorizada quando é aplicada elemento a elemento a todo o vetor, sem que o programador escreva um laço. Se \(\mathbf{x} = (x_1, \dots, x_n)\) e \(\mathbf{y} = (y_1, \dots, y_n)\), então x + y retorna \((x_1 + y_1, \dots, x_n + y_n)\) e log(x) retorna \((\log x_1, \dots, \log x_n)\). Essa é a forma idiomática de programar em R: é mais curta, mais legível e muito mais rápida.

Como exemplo, considere o cálculo da variância amostral

\[ s^2 = \frac{1}{n-1}\sum_{i=1}^{n}(x_i - \bar{x})^2 . \]

Para \(\mathbf{x} = (2, 4, 4, 5, 10)\) temos \(\bar{x} = 25/5 = 5\); os desvios são \((-3, -1, -1, 0, 5)\); os quadrados são \((9, 1, 1, 0, 25)\), cuja soma é \(36\); logo \(s^2 = 36/4 = 9\) e \(s = 3\). Em R, a fórmula se traduz quase literalmente, sem nenhum laço:

x <- c(2, 4, 4, 5, 10)
n <- length(x)
media <- sum(x) / n
s2 <- sum((x - media)^2) / (n - 1)
s2          #> [1] 9
var(x)      #> [1] 9   (função nativa, mesmo resultado)
sqrt(s2)    #> [1] 3

Para comparação, a versão com laço explícito é mais longa e, para vetores grandes, sensivelmente mais lenta:

soma <- 0
for (i in seq_along(x)) {
  soma <- soma + (x[i] - media)^2
}
soma / (n - 1)   #> [1] 9

3.4.2 A Regra da Reciclagem

Quando uma operação vetorizada envolve vetores de comprimentos diferentes, o R aplica a regra da reciclagem (recycling rule): o vetor mais curto é repetido até atingir o comprimento do mais longo. Formalmente, se \(\mathbf{x}\) tem comprimento \(n\) e \(\mathbf{y}\) tem comprimento \(m \le n\), o resultado \(\mathbf{z}\) de x + y tem comprimento \(n\) e

\[ z_i = x_i + y_{\,((i-1) \bmod m) + 1}, \qquad i = 1, \dots, n. \]

O caso mais comum é a operação entre um vetor e um escalar (\(m = 1\)), como x * 2. Vejamos um caso com \(m = 2\):

c(1, 2, 3, 4, 5, 6) + c(10, 20)
#> [1] 11 22 13 24 15 26

Passo a passo: o vetor c(10, 20) é reciclado como (10, 20, 10, 20, 10, 20), e a soma elemento a elemento resulta em \((11, 22, 13, 24, 15, 26)\). Quando \(n\) não é múltiplo de \(m\), o R ainda faz a operação, mas emite um aviso, pois isso costuma indicar um erro de lógica:

c(1, 2, 3) + c(10, 20)
#> [1] 11 22 13
#> Warning message: longer object length is not a multiple of shorter object length
Cuidado com a reciclagem silenciosa

A reciclagem é poderosa, mas pode mascarar erros. Uma comparação como x == c(1, 2) não testa se cada elemento de x é 1 ou 2; ela compara posições ímpares com 1 e pares com 2. Para pertinência a um conjunto, use x %in% c(1, 2). As funções do tidyverse reciclam apenas vetores de comprimento 1, justamente para evitar esse tipo de surpresa.

3.4.3 Geração de Sequências

Algumas funções auxiliares para criar vetores são usadas o tempo todo:

1:5                         #> [1] 1 2 3 4 5
seq(0, 1, by = 0.25)        #> [1] 0.00 0.25 0.50 0.75 1.00
seq(0, 10, length.out = 3)  #> [1]  0  5 10
rep(c("A", "B"), times = 3) #> [1] "A" "B" "A" "B" "A" "B"
rep(c("A", "B"), each = 2)  #> [1] "A" "A" "B" "B"
seq_along(c("x", "y", "z")) #> [1] 1 2 3
rev(1:4)                    #> [1] 4 3 2 1

Prefira seq_along(x) e seq_len(n) a 1:length(x) em laços: se x tiver comprimento zero, 1:length(x) produz c(1, 0), o que levaria o laço a executar duas vezes indevidamente.


3.5 Estruturas de Dados Fundamentais

As estruturas de dados no R são divididas de acordo com sua dimensionalidade (unidimensional, bidimensional ou multidimensional) e homogeneidade dos dados (homogênea ou heterogênea). A tabela a seguir, adaptada de Wickham (2019), organiza essa classificação:

Dimensão Homogênea Heterogênea
1d Vetor atômico Lista
2d Matriz Data frame
\(n\)d Array —

3.5.1 1. Vetores (Vectors)

O vetor é a estrutura homogênea unidimensional mais básica do R. Todos os elementos de um vetor precisam obrigatoriamente ser do mesmo tipo de dado (todos numéricos, todos caractere ou todos lógicos). Para criar um vetor, usamos a função de concatenação c():

# Vetor numérico
idades <- c(25, 30, 22, 45)

# Vetor de caracteres (texto)
nomes <- c("Alice", "Bob", "Charlie")

# Vetor lógico
status <- c(TRUE, FALSE, TRUE, TRUE)
Importante

A indexação de elementos no R começa em 1 (um), diferentemente de Python e C que começam em 0 (zero). Assim, idades[1] retornará o primeiro valor do vetor (25).

3.5.1.1 Indexação de Vetores

O operador [ permite extrair subconjuntos de um vetor de quatro maneiras distintas, que convém dominar com precisão:

  1. Inteiros positivos — retornam os elementos nas posições indicadas;
  2. Inteiros negativos — excluem os elementos nas posições indicadas;
  3. Vetores lógicos — retornam os elementos em que o índice é TRUE (indexação por máscara);
  4. Nomes (character) — retornam os elementos com os nomes indicados, se o vetor tiver o atributo names.
idades <- c(ana = 25, bia = 30, caio = 22, davi = 45)

idades[2]                 #> bia 30
idades[c(1, 3)]           #> ana 25  caio 22
idades[-1]                #> bia 30  caio 22  davi 45
idades[idades > 24]       #> ana 25  bia 30  davi 45
idades[c("caio", "ana")]  #> caio 22  ana 25
idades[10]                #> <NA> NA   (posição inexistente)
which(idades > 24)        #> ana 1  bia 2  davi 4  (posições TRUE)

A indexação lógica merece destaque. A expressão idades > 24 produz o vetor (TRUE, TRUE, FALSE, TRUE); ao usá-lo como índice, o R mantém apenas as posições 1, 2 e 4. Esse mecanismo é exatamente o que está por trás do verbo filter() do dplyr, que veremos adiante. A indexação também pode ser usada para modificar valores:

idades[idades > 40] <- 40     # trunca (winsoriza) valores acima de 40
idades
#>  ana  bia caio davi
#>   25   30   22   40

3.5.2 2. Fatores (Factors)

Variáveis categóricas (qualitativas) são representadas em R por fatores. Internamente, um fator é um vetor de inteiros com dois atributos: levels (os níveis possíveis, isto é, as categorias) e class = "factor". Essa representação é natural para o estatístico: ela corresponde à codificação das categorias \(\{1, \dots, K\}\) de uma variável nominal ou ordinal.

escolaridade <- c("medio", "superior", "fundamental", "medio", "superior")
f <- factor(escolaridade,
            levels = c("fundamental", "medio", "superior"))
f
#> [1] medio       superior    fundamental medio       superior
#> Levels: fundamental medio superior
as.integer(f)   #> [1] 2 3 1 2 3   (códigos internos)
levels(f)       #> [1] "fundamental" "medio" "superior"
table(f)
#> f
#> fundamental       medio    superior
#>           1           2           2

Para variáveis ordinais, usamos ordered = TRUE, o que habilita comparações de ordem:

f_ord <- factor(escolaridade,
                levels = c("fundamental", "medio", "superior"),
                ordered = TRUE)
f_ord > "medio"   #> [1] FALSE  TRUE FALSE FALSE  TRUE

Os fatores são importantes na modelagem: ao incluir um fator com \(K\) níveis em lm() ou glm(), o R cria automaticamente \(K - 1\) variáveis indicadoras (dummies), usando o primeiro nível como categoria de referência — tópico retomado no Capítulo de Modelos de Regressão. Uma armadilha comum é converter um fator de rótulos numéricos diretamente para número: as.numeric(factor(c("10", "20"))) retorna c(1, 2) (os códigos!), e não c(10, 20). O correto é as.numeric(as.character(f)).

3.5.3 3. Matrizes (Matrices)

As matrizes são estruturas bidimensionais homogêneas (linhas e colunas onde todos os dados possuem o mesmo tipo):

# Criando uma matriz 2x3 com valores de 1 a 6
minha_matriz <- matrix(1:6, nrow = 2, ncol = 3)
print(minha_matriz)
#>      [,1] [,2] [,3]
#> [1,]    1    3    5
#> [2,]    2    4    6

Observe que, por padrão, a matriz é preenchida por colunas (column-major order), herança do Fortran. Para preencher por linhas, use byrow = TRUE. Internamente, uma matriz é apenas um vetor com o atributo dim; o elemento \(a_{ij}\) de uma matriz \(n \times p\) ocupa a posição \(i + (j - 1)\,n\) do vetor subjacente.

A indexação de matrizes usa dois índices separados por vírgula, A[i, j]; deixar um deles vazio seleciona a linha ou coluna inteira:

A <- matrix(1:6, nrow = 2)
A[2, 3]     #> [1] 6
A[1, ]      #> [1] 1 3 5    (primeira linha)
A[, 2]      #> [1] 3 4      (segunda coluna)
dim(A)      #> [1] 2 3

3.5.3.1 Operações Matriciais

Como a álgebra linear é a linguagem da estatística multivariada e do aprendizado de máquina, é fundamental distinguir as operações elemento a elemento das operações matriciais:

Operação Notação matemática Código R
Produto elemento a elemento (Hadamard) \(\mathbf{A} \odot \mathbf{B}\) A * B
Produto matricial \(\mathbf{A}\mathbf{B}\) A %*% B
Transposta \(\mathbf{A}^\top\) t(A)
Inversa \(\mathbf{A}^{-1}\) solve(A)
Solução de \(\mathbf{A}\mathbf{x} = \mathbf{b}\) \(\mathbf{x} = \mathbf{A}^{-1}\mathbf{b}\) solve(A, b)
Determinante \(\det(\mathbf{A})\) det(A)
Produto cruzado \(\mathbf{X}^\top\mathbf{X}\) crossprod(X)
Matriz identidade \(\mathbf{I}_n\) diag(n)
Autovalores/autovetores \(\mathbf{A}\mathbf{v} = \lambda \mathbf{v}\) eigen(A)

Exemplo resolvido. Sejam

\[ \mathbf{A} = \begin{pmatrix} 2 & 1 \\ 1 & 3 \end{pmatrix}, \qquad \mathbf{b} = \begin{pmatrix} 5 \\ 10 \end{pmatrix}. \]

O determinante é \(\det(\mathbf{A}) = 2 \cdot 3 - 1 \cdot 1 = 5\), e a inversa de uma matriz \(2 \times 2\) é

\[ \mathbf{A}^{-1} = \frac{1}{\det(\mathbf{A})}\begin{pmatrix} 3 & -1 \\ -1 & 2 \end{pmatrix} = \begin{pmatrix} 0{,}6 & -0{,}2 \\ -0{,}2 & 0{,}4 \end{pmatrix}. \]

Assim, a solução de \(\mathbf{A}\mathbf{x} = \mathbf{b}\) é

\[ \mathbf{x} = \mathbf{A}^{-1}\mathbf{b} = \begin{pmatrix} 0{,}6 \cdot 5 - 0{,}2 \cdot 10 \\ -0{,}2 \cdot 5 + 0{,}4 \cdot 10 \end{pmatrix} = \begin{pmatrix} 1 \\ 3 \end{pmatrix}. \]

Verificação: \(2 \cdot 1 + 1 \cdot 3 = 5\) e \(1 \cdot 1 + 3 \cdot 3 = 10\). Em R:

A <- matrix(c(2, 1, 1, 3), nrow = 2)
b <- c(5, 10)
det(A)          #> [1] 5
solve(A)
#>      [,1] [,2]
#> [1,]  0.6 -0.2
#> [2,] -0.2  0.4
solve(A, b)     #> [1] 1 3
A %*% solve(A)  # deve ser a identidade (a menos de erro numérico)
A * A           # Hadamard: eleva cada elemento ao quadrado
A %*% A         # produto matricial: [5 5; 5 10]
solve(A, b) em vez de solve(A) %*% b

Numericamente, resolver o sistema diretamente (via decomposição LU) é mais rápido e mais estável do que calcular a inversa explicitamente e depois multiplicar. A mesma lição vale para o estimador de mínimos quadrados \(\hat{\boldsymbol\beta} = (\mathbf{X}^\top\mathbf{X})^{-1}\mathbf{X}^\top\mathbf{y}\): em código, prefira solve(crossprod(X), crossprod(X, y)).

Como aplicação, podemos calcular o estimador de mínimos quadrados de uma regressão linear simples “à mão” e conferir com lm():

x <- c(1, 2, 3, 4, 5)
y <- c(2.1, 3.9, 6.2, 7.8, 10.1)
X <- cbind(1, x)                      # matriz de delineamento com intercepto
beta <- solve(crossprod(X), crossprod(X, y))
beta
#>   [,1]
#>   0.05
#> x 1.99
coef(lm(y ~ x))
#> (Intercept)           x
#>        0.05        1.99

Matrizes de mais de duas dimensões são chamadas de arrays e criadas com array(dados, dim = c(d1, d2, d3)); elas aparecem, por exemplo, na representação de imagens coloridas (altura × largura × canal).

3.5.4 4. Listas (Lists)

As listas são estruturas heterogêneas unidimensionais que podem conter objetos de qualquer tamanho e tipo, incluindo vetores, matrizes, data frames ou até mesmo outras listas:

# Criando uma lista heterogênea
meu_cadastro_completo <- list(
  identificacao = cadastro,
  notas = c(9.5, 8.0, 7.5),
  curso = "Ciência de Dados"
)

(O objeto cadastro usado acima é o data frame definido na seção seguinte.) A indexação de listas possui uma sutileza essencial, frequentemente ilustrada pela metáfora de um trem com vagões:

  • lista[i] (colchete simples) retorna uma sublista — o trem com apenas o vagão \(i\);
  • lista[[i]] (colchete duplo) retorna o conteúdo do elemento \(i\) — o que está dentro do vagão;
  • lista$nome é um atalho para lista[["nome"]].
l <- list(a = 1:3, b = "texto", c = list(d = TRUE))
class(l["a"])     #> [1] "list"
class(l[["a"]])   #> [1] "integer"
l$a               #> [1] 1 2 3
l$c$d             #> [1] TRUE      (acesso aninhado)
length(l)         #> [1] 3
str(l)            # exibe a estrutura de forma compacta

As listas são onipresentes: o resultado de lm(), por exemplo, é uma lista (com classe "lm") contendo coeficientes, resíduos, valores ajustados etc. Você pode explorar isso com names(modelo) e str(modelo).

3.5.5 5. Data Frames

O Data Frame é a estrutura de dados mais importante para o cientista de dados. Trata-se de uma tabela bidimensional heterogênea, o que significa que cada coluna pode conter um tipo de dado diferente (uma coluna de idades como numérica, uma de nomes como caractere e uma de status como lógica). É a representação direta de uma tabela de banco de dados ou planilha:

# Criando um data frame a partir de vetores
cadastro <- data.frame(
  Nome = c("Alice", "Bob", "Charlie"),
  Idade = c(25, 30, 22),
  Ativo = c(TRUE, FALSE, TRUE)
)
print(cadastro)

Tecnicamente, um data frame é uma lista de vetores de mesmo comprimento, com os atributos names (nomes das colunas), row.names e class = "data.frame". Por isso, ele combina a indexação de listas (colunas) com a de matrizes (linhas e colunas):

cadastro$Idade                  #> [1] 25 30 22
cadastro[["Nome"]]              #> [1] "Alice"   "Bob"     "Charlie"
cadastro[2, ]                   # segunda linha (observação)
cadastro[cadastro$Ativo, c("Nome", "Idade")]   # filtragem lógica em base R
nrow(cadastro); ncol(cadastro)  #> [1] 3   [1] 3
str(cadastro)
summary(cadastro)               # resumo estatístico por coluna
cadastro$Idade_meses <- cadastro$Idade * 12    # cria uma coluna nova

3.5.5.1 Tibbles

O tidyverse introduz uma versão moderna do data frame, o tibble (pacote tibble). Um tibble é um data frame (herda sua classe), mas com comportamentos mais previsíveis:

  • imprime apenas as primeiras linhas e as colunas que cabem na tela, exibindo o tipo de cada coluna;
  • nunca converte texto em fator automaticamente nem altera nomes de colunas;
  • tb[, "x"] sempre retorna um tibble (nunca “simplifica” para vetor);
  • recicla apenas vetores de comprimento 1;
  • não faz correspondência parcial de nomes com $ (o data frame clássico aceita cadastro$Ida como cadastro$Idade, o que é fonte de erros).
library(tibble)
tb <- tibble(
  nome  = c("Alice", "Bob", "Charlie"),
  idade = c(25, 30, 22),
  idade_meses = idade * 12        # colunas podem referenciar colunas anteriores
)
tb
#> # A tibble: 3 × 3
#>   nome    idade idade_meses
#>   <chr>   <dbl>       <dbl>
#> 1 Alice      25         300
#> 2 Bob        30         360
#> 3 Charlie    22         264

3.5.6 Valores Especiais: NA, NULL, NaN e Inf

O R possui quatro valores especiais que frequentemente confundem iniciantes. Distingui-los é parte importante do trabalho com dados reais.

Valor Significado Comprimento Teste
NA Dado ausente (Not Available) — o valor existe, mas é desconhecido 1 is.na()
NULL Ausência de objeto — “nada” 0 is.null()
NaN Resultado indefinido (Not a Number), como \(0/0\) 1 is.nan()
Inf, -Inf Infinito, como \(1/0\) 1 is.infinite()
0 / 0           #> [1] NaN
1 / 0           #> [1] Inf
log(0)          #> [1] -Inf
sqrt(-1)        #> [1] NaN  (com aviso)
length(NULL)    #> [1] 0
length(NA)      #> [1] 1
c(1, NULL, 3)   #> [1] 1 3   (NULL desaparece)
c(1, NA, 3)     #> [1]  1 NA  3
is.na(NaN)      #> [1] TRUE  (todo NaN é NA, mas não o contrário)

O NA é contagioso: qualquer operação envolvendo um valor desconhecido produz, em geral, um resultado desconhecido. Isso é coerente com a lógica de três valores: se não sabemos \(x\), também não sabemos \(x + 1\) nem se \(x > 0\). Por isso, NA == NA retorna NA (e não TRUE), e o teste correto de ausência é is.na(x). As funções de resumo possuem o argumento na.rm:

renda <- c(2500, NA, 3200, 4100)
mean(renda)                 #> [1] NA
mean(renda, na.rm = TRUE)   #> [1] 3266.667
sum(is.na(renda))           #> [1] 1   (número de faltantes)
NA & FALSE                  #> [1] FALSE (o resultado independe do NA)
NA | TRUE                   #> [1] TRUE

Observe que remover os NA com na.rm = TRUE equivale a uma análise de casos completos, que só é não viesada sob a hipótese de que os dados faltam completamente ao acaso (Missing Completely at Random, MCAR). Estratégias de tratamento de faltantes são discutidas no Capítulo de Análise Exploratória de Dados com Python.

3.5.7 Memória e a Semântica de Cópia na Modificação

Para escrever código correto e eficiente, é útil compreender o que acontece na memória quando escrevemos b <- a. Em R, nomes são vinculados (bound) a objetos: a atribuição não copia o vetor, apenas faz com que o nome b aponte para o mesmo objeto que a. A cópia só ocorre quando um dos dois é modificado — daí o nome cópia na modificação (copy-on-modify) (Wickham 2019). A função tracemem() permite observar o momento exato da cópia:

a <- c(1, 2, 3)
b <- a                  # nenhuma cópia: dois nomes, um único objeto
tracemem(a) == tracemem(b)
#> [1] TRUE              (mesmo endereço de memória)
b[1] <- 10              # agora sim, o R copia o vetor antes de alterá-lo
#> tracemem[0x63d147512dd8 -> 0x63d1475129c8]:
a                       #> [1] 1 2 3   (o original fica intacto)
b                       #> [1] 10  2  3

O mesmo vale para argumentos de funções: uma função recebe uma referência ao objeto original e, se o modificar, trabalha sobre uma cópia local. Por isso, funções em R não têm efeitos colaterais sobre seus argumentos:

f <- function(v) { v[1] <- 100; v }
x <- 1:3
y <- f(x)
x     #> [1] 1 2 3      (inalterado)
y     #> [1] 100   2   3

Essa semântica é uma escolha de desenho que favorece a segurança (o programador raramente é surpreendido por alterações à distância, ao contrário do que ocorre com listas em Python, como veremos no próximo capítulo) em detrimento, às vezes, da eficiência. Para mitigar o custo, o R mantém uma contagem de quantos nomes apontam para cada objeto: se um objeto tem um único nome, a modificação pode ser feita no lugar (in place), sem cópia. As exceções importantes à cópia na modificação são os ambientes (estudados na seção sobre funções) e os objetos R6, que têm semântica de referência.

O tamanho de um objeto na memória pode ser consultado com object.size(). Um vetor double ocupa 8 bytes por elemento e um integer, 4 bytes, mais um pequeno cabeçalho:

print(object.size(numeric(1e6)), units = "Kb")    #> 7812.5 Kb  (10^6 x 8 bytes)
print(object.size(integer(1e6)), units = "Kb")    #> 3906.3 Kb  (10^6 x 4 bytes)
print(object.size(rep("abc", 1e6)), units = "Kb") #> 7812.6 Kb

O último resultado revela outro detalhe interno: um vetor de caracteres armazena apenas ponteiros (8 bytes cada) para cadeias guardadas em um repositório global de strings (o global string pool); a cadeia "abc" é armazenada uma única vez, embora apareça um milhão de vezes. Essa é uma das razões pelas quais os fatores perderam parte de sua vantagem de memória em relação a vetores de texto.


3.6 Controle de Fluxo

3.6.1 Condicionais

A estrutura if/else avalia uma condição lógica de comprimento 1:

nota <- 8.5
if (nota >= 7) {
  print("Aprovado")
} else if (nota >= 5) {
  print("Recuperação")
} else {
  print("Reprovado")
}
#> [1] "Aprovado"

Para aplicar uma condição a um vetor inteiro, usa-se a versão vetorizada ifelse(teste, sim, nao) (ou dplyr::if_else(), mais estrita quanto a tipos, e dplyr::case_when() para múltiplas condições):

notas <- c(8.5, 4.0, 6.0, 9.2)
ifelse(notas >= 7, "Aprovado", ifelse(notas >= 5, "Recuperação", "Reprovado"))
#> [1] "Aprovado"    "Reprovado"   "Recuperação" "Aprovado"

3.6.2 Laços de Repetição

O R oferece três tipos de laço: for, while e repeat. Os comandos next (pula para a próxima iteração) e break (interrompe o laço) controlam o fluxo interno.

# for: percorre os elementos de um vetor
for (i in seq_len(3)) {
  cat("Iteração", i, "\n")
}

# while: repete enquanto a condição for verdadeira
# Exemplo: método de Newton para calcular sqrt(2)
x <- 1
while (abs(x^2 - 2) > 1e-10) {
  x <- x - (x^2 - 2) / (2 * x)
}
x   #> [1] 1.414214

O método de Newton usado acima ilustra bem um laço while. Para encontrar a raiz de \(g(x) = x^2 - 2\), iteramos

\[ x_{k+1} = x_k - \frac{g(x_k)}{g'(x_k)} = x_k - \frac{x_k^2 - 2}{2x_k}. \]

Partindo de \(x_0 = 1\): \(x_1 = 1 - (1 - 2)/2 = 1{,}5\); \(x_2 = 1{,}5 - (2{,}25 - 2)/3 \approx 1{,}41667\); \(x_3 \approx 1{,}414216\). Em apenas quatro iterações atinge-se a precisão de \(10^{-10}\), uma manifestação da convergência quadrática do método.

Pré-aloque a memória em laços

Se precisar mesmo de um laço que constrói um vetor, crie-o com o tamanho final antes (res <- numeric(n)) e preencha res[i]. Fazer res <- c(res, valor) dentro do laço obriga o R a copiar o vetor inteiro a cada iteração, o que leva a um custo total da ordem de \(O(n^2)\).

A afirmação sobre o custo quadrático pode ser justificada com um argumento simples de contagem. Na iteração \(i\), a expressão c(res, valor) aloca um novo vetor de comprimento \(i\) e copia para ele os \(i - 1\) elementos anteriores. O número total de elementos copiados ao longo de \(n\) iterações é \[ \sum_{i=1}^{n} (i - 1) = \frac{n(n-1)}{2} = O(n^2), \] ao passo que, com pré-alocação, cada elemento é escrito uma única vez, com custo total \(O(n)\). Para \(n = 10^5\), isso significa cerca de \(5 \times 10^9\) cópias contra \(10^5\) escritas — uma diferença que se reflete diretamente no tempo de execução, como mostraremos na seção sobre desempenho.


3.7 Funções

3.7.1 Definindo Funções

Em R, funções são objetos de primeira classe: podem ser atribuídas a variáveis, passadas como argumento e retornadas por outras funções. Uma função tem três componentes: os argumentos (formals()), o corpo (body()) e o ambiente em que foi criada (environment()).

# Coeficiente de variação: CV = s / média
coef_variacao <- function(x, na.rm = FALSE, percentual = TRUE) {
  if (!is.numeric(x)) {
    stop("O argumento 'x' deve ser numérico.")
  }
  cv <- sd(x, na.rm = na.rm) / mean(x, na.rm = na.rm)
  if (percentual) cv <- 100 * cv
  cv          # a última expressão avaliada é o valor de retorno
}

coef_variacao(c(2, 4, 4, 5, 10))         #> [1] 60
coef_variacao(c(2, NA, 4), na.rm = TRUE)
coef_variacao(c(10, 20), percentual = FALSE)

Conferindo o primeiro resultado: no exemplo de variância vimos que, para \(\mathbf{x} = (2, 4, 4, 5, 10)\), \(s = 3\) e \(\bar{x} = 5\); logo \(\mathrm{CV} = 3/5 = 0{,}6 = 60\%\).

Alguns pontos importantes:

  • Argumentos padrão (na.rm = FALSE) tornam o argumento opcional.
  • Correspondência de argumentos: os argumentos podem ser passados por posição, por nome completo ou por nome parcial; em código de produção, nomeie os argumentos opcionais.
  • Avaliação preguiçosa (lazy evaluation): um argumento só é avaliado quando é usado no corpo da função. Por isso f <- function(x, y) x; f(1, stop("erro")) retorna 1 sem erro.
  • O argumento especial ... (reticências) captura um número variável de argumentos e os repassa a outras funções, como em function(x, ...) mean(x, ...).
  • return() pode ser usado para sair antecipadamente; stop(), warning() e message() sinalizam condições.

3.7.2 Escopo Léxico

O R utiliza escopo léxico (lexical scoping), herdado do Scheme: quando uma função usa um nome que não é argumento nem variável local, o R o procura no ambiente em que a função foi definida (e não no ambiente de onde foi chamada), subindo pela cadeia de ambientes até o ambiente global e os pacotes carregados.

a <- 10
f <- function(x) {
  x + a        # 'a' não é local: buscado no ambiente de definição
}
f(1)           #> [1] 11

g <- function() {
  a <- 1000    # este 'a' é local de g e NÃO afeta f
  f(1)
}
g()            #> [1] 11

O escopo léxico permite criar fechamentos (closures): funções que “lembram” o ambiente em que foram criadas. São úteis, por exemplo, para fabricar funções:

fabrica_potencia <- function(expoente) {
  function(x) x^expoente
}
quadrado <- fabrica_potencia(2)
cubo     <- fabrica_potencia(3)
quadrado(4)   #> [1] 16
cubo(2)       #> [1] 8

3.7.3 Ambientes: a Estrutura por Trás do Escopo

O mecanismo que implementa o escopo léxico é o ambiente (environment). Um ambiente é um objeto que associa nomes a valores (como uma lista nomeada) e que possui, adicionalmente, um ponteiro para um ambiente pai (enclosing environment). Formalmente, a busca de um nome \(v\) a partir de um ambiente \(E_0\) percorre a cadeia \[ E_0 \to E_1 = \mathrm{pai}(E_0) \to E_2 = \mathrm{pai}(E_1) \to \cdots \to \texttt{emptyenv()}, \] retornando o valor associado a \(v\) no primeiro ambiente \(E_k\) em que ele estiver definido; se a cadeia chegar ao ambiente vazio sem encontrar o nome, ocorre o erro object 'v' not found. Quatro ambientes merecem destaque:

  • o ambiente global (globalenv()), onde vivem os objetos criados no console;
  • o ambiente de execução, criado a cada chamada de função para guardar as variáveis locais e descartado ao final (por isso uma função “esquece” seus valores locais entre chamadas);
  • o ambiente de definição (enclosing) de uma função, consultado por environment(f), que é o pai de cada ambiente de execução — eis o escopo léxico;
  • os ambientes dos pacotes, encadeados no caminho de busca (search path), exibido por search().
search()
#> [1] ".GlobalEnv"        "package:stats"     "package:graphics"
#> [4] "package:grDevices" "package:utils"     "package:datasets"
#> [7] "package:methods"   "Autoloads"         "package:base"

e <- new.env()                  # ambientes também podem ser criados explicitamente
assign("k", 5, envir = e)
get("k", envir = e)             #> [1] 5
exists("k")                     #> [1] FALSE  (k não está no ambiente global)
environmentName(environment(mean))   #> [1] "base"

O caminho de busca explica o fenômeno do mascaramento mencionado na seção de pacotes: ao executar library(dplyr), o ambiente do pacote é inserido logo após .GlobalEnv, e sua função filter() passa a ser encontrada antes da função homônima de package:stats.

Diferentemente dos demais objetos do R, ambientes não seguem a semântica de cópia na modificação: eles são modificados no lugar e compartilhados por referência. Combinados com o operador de superatribuição <<- — que atribui o valor no primeiro ambiente ancestral em que o nome já existe —, isso permite que um fechamento mantenha um estado mutável entre chamadas:

contador <- function() {
  i <- 0
  function() {
    i <<- i + 1     # altera o 'i' do ambiente de definição, não um 'i' local
    i
  }
}
c1 <- contador()
c1(); c1()
c1()                    #> [1] 3
c2 <- contador()        # novo ambiente, contagem independente
c2()                    #> [1] 1
environment(c1)$i       #> [1] 3

Cada chamada a contador() cria um ambiente de execução novo, que sobrevive após o término da chamada porque a função retornada mantém uma referência a ele. Esse padrão é usado, por exemplo, para implementar caches e geradores de números. O uso de <<- em código comum, porém, deve ser evitado: modificar variáveis fora da própria função torna o comportamento difícil de prever e de testar.

3.7.4 Programação Funcional: a Família apply e o purrr

Como funções são objetos, é natural aplicar uma função a cada elemento de uma coleção, substituindo laços explícitos. Na terminologia de programação funcional, essa operação é chamada de map: dada \(f\) e uma coleção \((x_1, \dots, x_n)\), retorna \((f(x_1), \dots, f(x_n))\). O R base oferece a família apply:

Função Entrada Saída Uso típico
lapply(X, f) vetor/lista lista aplicar f a cada elemento
sapply(X, f) vetor/lista vetor/matriz (simplifica) uso interativo
vapply(X, f, FUN.VALUE) vetor/lista vetor do tipo declarado código robusto
apply(M, MARGIN, f) matriz vetor/matriz MARGIN = 1 linhas, 2 colunas
tapply(x, g, f) vetor + fator vetor aplicar f por grupo
mapply(f, x, y) vários vetores vetor/lista iterar em paralelo
M <- matrix(1:6, nrow = 2)
apply(M, 1, sum)        #> [1]  9 12    (soma das linhas)
apply(M, 2, mean)       #> [1] 1.5 3.5 5.5 (médias das colunas)

lista_amostras <- list(a = c(1, 2, 3), b = c(10, 20), c = c(5, 5, 5, 5))
sapply(lista_amostras, mean)   #> a b c: 2 15 5
vapply(lista_amostras, length, FUN.VALUE = integer(1))

alturas <- c(1.60, 1.75, 1.82, 1.58, 1.70)
sexo    <- c("F", "M", "M", "F", "F")
tapply(alturas, sexo, mean)
#>     F     M
#> 1.627 1.785

O pacote purrr, do tidyverse, oferece uma interface mais consistente: map() sempre retorna lista, e as variantes map_dbl(), map_chr(), map_lgl() e map_int() retornam vetores do tipo indicado (falhando se isso não for possível). Funções anônimas podem ser escritas como \(x) x^2 (sintaxe disponível a partir do R 4.1):

library(purrr)
map_dbl(lista_amostras, mean)
map_dbl(lista_amostras, \(x) sd(x) / mean(x))   # CV de cada amostra
map2_dbl(c(1, 2, 3), c(10, 20, 30), \(x, y) x * y)  #> [1] 10 40 90

3.7.4.1 Os Funcionais Clássicos: Map, Filter e Reduce

A programação funcional se apoia em três operações fundamentais sobre coleções, presentes em praticamente todas as linguagens modernas (inclusive no Python, como veremos no próximo capítulo):

  • map — aplica \(f\) a cada elemento: \((x_1, \dots, x_n) \mapsto (f(x_1), \dots, f(x_n))\);
  • filter — mantém os elementos que satisfazem um predicado \(p\): \((x_1, \dots, x_n) \mapsto (x_i : p(x_i) = \texttt{TRUE})\);
  • reduce (ou fold) — combina os elementos dois a dois com uma operação binária \(\oplus\), da esquerda para a direita: \((x_1, \dots, x_n) \mapsto (\cdots((x_1 \oplus x_2) \oplus x_3) \oplus \cdots) \oplus x_n\).

No R base, essas operações correspondem a Map(), Filter() e Reduce() (no purrr, a map(), keep() e reduce()):

Filter(function(x) x %% 2 == 0, 1:10)    #> [1]  2  4  6  8 10
Map(function(x, y) x + y, 1:3, 4:6)      # lista: 5, 7, 9
Reduce(`+`, 1:5)                         #> [1] 15
Reduce(`+`, 1:5, accumulate = TRUE)      #> [1]  1  3  6 10 15   (somas acumuladas)
Reduce(function(a, b) a * 10 + b, c(1, 2, 3))   #> [1] 123

Verificando o último exemplo passo a passo: \((1 \oplus 2) = 1 \cdot 10 + 2 = 12\) e \((12 \oplus 3) = 12 \cdot 10 + 3 = 123\) — o algoritmo de Horner para converter dígitos em número. Um uso prático de Reduce() é combinar uma lista de tabelas com junções sucessivas: Reduce(function(x, y) merge(x, y, by = "id"), lista_de_tabelas).

Como funções são valores, também é possível escrever funções que recebem e retornam funções, como a composição \((g \circ f)(x) = g(f(x))\):

compor <- function(f, g) function(x) g(f(x))
h <- compor(log, sqrt)    # h(x) = sqrt(log(x))
h(exp(4))                 #> [1] 2

3.7.5 Um Vislumbre do Sistema de Objetos S3

O sistema S3 é a forma mais simples de orientação a objetos em R e explica por que print(), summary() e plot() se comportam de maneira diferente para cada tipo de objeto. Uma função genérica despacha a chamada para um método de acordo com o atributo class do primeiro argumento (o método summary.lm é chamado quando o objeto tem classe "lm"). Podemos criar nossas próprias classes:

nova_amostra <- function(x, rotulo) {
  structure(list(dados = x, rotulo = rotulo), class = "amostra")
}
print.amostra <- function(x, ...) {
  cat("Amostra:", x$rotulo, "| n =", length(x$dados),
      "| média =", round(mean(x$dados), 2), "\n")
  invisible(x)
}
am <- nova_amostra(c(3, 5, 7), "Grupo A")
am     #> Amostra: Grupo A | n = 3 | média = 5

A herança em S3 é expressa simplesmente por um vetor de classes: um objeto de classe c("glm", "lm") é tratado como "glm" e, se não houver método específico para essa classe, como "lm". Por exemplo, inherits(glm(am ~ wt, binomial, mtcars), "lm") retorna TRUE. Dentro de um método, NextMethod() delega a chamada ao método da próxima classe do vetor.

3.7.6 Os Sistemas S4 e R6

O S3 é informal: nada impede que alguém crie um objeto de classe "lm" sem os componentes esperados. Para projetos maiores, o R oferece dois outros sistemas de objetos (Chambers 2008; Wickham 2019):

  • S4 — sistema formal, com definição explícita de classes (setClass()), atributos tipados chamados slots (acessados com @), validação automática, funções genéricas (setGeneric()) e métodos registrados (setMethod()), inclusive com despacho múltiplo (sobre os tipos de vários argumentos). É amplamente usado no Bioconductor e no pacote Matrix.
  • R6 — sistema com semântica de referência e métodos que “pertencem” ao objeto, no estilo de Python e Java. Por se basear em ambientes, os objetos R6 são modificados no lugar.
setClass("Amostra",
  representation(dados = "numeric", rotulo = "character"),
  validity = function(object) {
    if (length(object@dados) < 2) "São necessárias ao menos 2 observações." else TRUE
  })
setGeneric("cv", function(obj, ...) standardGeneric("cv"))
setMethod("cv", "Amostra", function(obj, ...) sd(obj@dados) / mean(obj@dados))

a <- new("Amostra", dados = c(2, 4, 4, 5, 10), rotulo = "A")
cv(a)                                        #> [1] 0.6
new("Amostra", dados = 1, rotulo = "B")      # a validação impede objetos inválidos
#> Error in validObject(.Object): invalid class "Amostra" object:
#>   São necessárias ao menos 2 observações.
library(R6)
Conta <- R6Class("Conta", public = list(
  saldo = 0,
  depositar = function(v) { self$saldo <- self$saldo + v; invisible(self) },
  sacar = function(v) {
    if (v > self$saldo) stop("Saldo insuficiente.")
    self$saldo <- self$saldo - v
    invisible(self)
  }
))
c1 <- Conta$new()
c2 <- c1                      # c2 é OUTRO NOME para o MESMO objeto
c1$depositar(100)$sacar(30)   # métodos encadeados
c2$saldo                      #> [1] 70  (semântica de referência!)

O último exemplo contrasta diretamente com a cópia na modificação: com objetos R6, alterar c1 altera também c2. A tabela resume as diferenças:

Característica S3 S4 R6
Definição de classe Informal (atributo class) Formal (setClass) Formal (R6Class)
Acesso a campos $ ou [[ @ $
Validação Manual Automática (validity) Manual
Despacho Pelo 1.º argumento Múltiplo Método do objeto
Semântica Cópia na modificação Cópia na modificação Referência
Uso típico print, summary, modelos Bioconductor, Matrix Objetos com estado (Shiny, conexões)

Os conceitos gerais de orientação a objetos (classe, objeto, atributo, método, herança e polimorfismo) são apresentados em detalhe no Capítulo de Introdução à Linguagem Python.


3.8 Distribuições de Probabilidade e Simulação

Por ter nascido como uma linguagem para estatísticos, o R traz, já no pacote stats, as principais distribuições de probabilidade, organizadas sob uma convenção de nomes uniforme. Para cada distribuição de nome dist (por exemplo, norm, binom, pois, exp, t, chisq, unif), há quatro funções:

Prefixo Função matemática Exemplo Interpretação
d densidade \(f(x)\) ou função de probabilidade \(P(X = x)\) dbinom(2, 5, 0.3) \(P(X = 2)\)
p função de distribuição acumulada \(F(x) = P(X \le x)\) pnorm(1.96) \(P(Z \le 1{,}96)\)
q função quantil \(F^{-1}(p) = \inf\{x : F(x) \ge p\}\) qnorm(0.975) quantil de 97,5%
r geração de números pseudoaleatórios rnorm(100) amostra de tamanho 100

Exemplo resolvido. Seja \(X \sim \text{Binomial}(n = 5, p = 0{,}3)\), o número de sucessos em 5 ensaios independentes. Pela fórmula \[ P(X = k) = \binom{n}{k} p^k (1 - p)^{n - k}, \] temos \(P(X = 2) = \binom{5}{2}(0{,}3)^2(0{,}7)^3 = 10 \times 0{,}09 \times 0{,}343 = 0{,}3087\). A probabilidade acumulada \(P(X \le 2) = P(X=0) + P(X=1) + P(X=2) = 0{,}16807 + 0{,}36015 + 0{,}3087 = 0{,}83692\). Em R:

dbinom(2, size = 5, prob = 0.3)    #> [1] 0.3087
pbinom(2, size = 5, prob = 0.3)    #> [1] 0.83692
sum(dbinom(0:2, 5, 0.3))           #> [1] 0.83692  (mesmo resultado)
qbinom(0.5, 5, 0.3)                #> [1] 1        (mediana)

pnorm(1) - pnorm(-1)               #> [1] 0.6826895  (regra 68-95-99,7)
qnorm(0.975)                       #> [1] 1.959964   (o famoso 1,96)
pnorm(180, mean = 170, sd = 10, lower.tail = FALSE)   #> [1] 0.1586553
qt(0.975, df = 10)                 #> [1] 2.228139   (quantil da t de Student)

O penúltimo comando responde: se a altura de uma população segue \(N(170, 10^2)\) (em cm), qual a proporção de pessoas com mais de 180 cm? Padronizando, \(P(X > 180) = P(Z > 1) = 1 - \Phi(1) \approx 0{,}1587\). O argumento lower.tail = FALSE calcula diretamente a cauda superior \(1 - F(x)\), o que é numericamente mais preciso do que 1 - pnorm(...) quando a probabilidade é muito pequena (lembre-se do cancelamento catastrófico).

3.8.1 Números Pseudoaleatórios e a Semente

Um computador é uma máquina determinística e, portanto, não produz números verdadeiramente aleatórios. O que as funções r* geram são números pseudoaleatórios: uma sequência determinística \(u_1, u_2, \dots\) que se comporta, segundo uma bateria de testes estatísticos, como uma amostra i.i.d. da distribuição \(U(0, 1)\). Um gerador é definido por um estado interno \(s_k\), uma função de transição \(s_{k+1} = T(s_k)\) e uma função de saída \(u_k = G(s_k)\). O exemplo histórico mais simples é o gerador congruencial linear (GCL) (Knuth 1997): \[ s_{k+1} = (a\, s_k + c) \bmod m, \qquad u_k = s_k / m. \]

Exemplo resolvido. Com \(m = 16\), \(a = 5\), \(c = 3\) e semente \(s_0 = 7\): \(s_1 = (5 \cdot 7 + 3) \bmod 16 = 38 \bmod 16 = 6\); \(s_2 = (5 \cdot 6 + 3) \bmod 16 = 33 \bmod 16 = 1\); \(s_3 = (5 + 3) \bmod 16 = 8\), e assim por diante:

gcl <- function(n, semente, a = 5, c = 3, m = 16) {
  s <- integer(n)
  estado <- semente
  for (i in seq_len(n)) {
    estado <- (a * estado + c) %% m
    s[i] <- estado
  }
  s
}
gcl(18, semente = 7)
#>  [1]  6  1  8 11 10  5 12 15 14  9  0  3  2 13  4  7  6  1

Observe que a sequência percorre todos os 16 valores possíveis e então se repete: seu período é 16, o máximo possível. Pelo teorema de Hull–Dobell, um GCL com \(c \neq 0\) atinge o período máximo \(m\) se, e somente se, (i) \(c\) e \(m\) são primos entre si; (ii) \(a - 1\) é divisível por todos os fatores primos de \(m\); e (iii) \(a - 1\) é divisível por 4 se \(m\) o for — condições satisfeitas aqui (\(c = 3\), \(a - 1 = 4\)). Geradores reais usam \(m\) gigantesco; o padrão do R é o Mersenne Twister (Matsumoto e Nishimura 1998), cujo período é \(2^{19937} - 1\), um número com mais de 6.000 dígitos.

RNGkind()
#> [1] "Mersenne-Twister" "Inversion"        "Rejection"
set.seed(2026); runif(3)
#> [1] 0.6986735 0.5565305 0.1401400
set.seed(2026); runif(3)          # mesma semente => mesma sequência
#> [1] 0.6986735 0.5565305 0.1401400

O segundo elemento de RNGkind() indica que valores normais são obtidos por inversão, e o terceiro, que sample() usa um método de rejeição. Fixar a semente com set.seed() torna toda a simulação reprodutível, requisito indispensável em trabalhos científicos.

3.8.2 O Método da Transformação Inversa

Como obter amostras de uma distribuição qualquer a partir de uniformes? O resultado central é o seguinte: se \(U \sim U(0, 1)\) e \(F\) é uma função de distribuição acumulada contínua e estritamente crescente, então \(X = F^{-1}(U)\) tem distribuição \(F\). A demonstração é imediata: \[ P(X \le x) = P(F^{-1}(U) \le x) = P(U \le F(x)) = F(x). \]

Exemplo resolvido: a distribuição exponencial. Se \(X \sim \text{Exp}(\lambda)\), então \(F(x) = 1 - e^{-\lambda x}\) para \(x \ge 0\). Resolvendo \(u = 1 - e^{-\lambda x}\) em \(x\), obtemos \(F^{-1}(u) = -\log(1 - u)/\lambda\). Com \(\lambda = 2\), esperamos média \(1/\lambda = 0{,}5\) e variância \(1/\lambda^2 = 0{,}25\):

set.seed(10)
u <- runif(1e5)
x <- -log(1 - u) / 2       # transformação inversa
mean(x)                    #> [1] 0.49703
var(x)                     #> [1] 0.2484762

3.8.3 O Método de Monte Carlo

O método de Monte Carlo usa a simulação para aproximar quantidades que seriam difíceis de calcular analiticamente (Robert e Casella 2004). A ideia se apoia na Lei dos Grandes Números: se \(X_1, \dots, X_n\) são i.i.d. com \(\mathbb{E}[g(X)] = \theta\), então \[ \hat{\theta}_n = \frac{1}{n}\sum_{i=1}^n g(X_i) \xrightarrow{\text{q.c.}} \theta, \] e, pelo Teorema Central do Limite, o erro padrão da estimativa é \(\sigma_g / \sqrt{n}\), estimado por \(s_g/\sqrt{n}\). Note que o erro decresce à taxa \(1/\sqrt{n}\): para ganhar um dígito de precisão é preciso multiplicar o número de simulações por cem.

Exemplo resolvido: estimando \(\pi\). Sorteie pontos \((U, V)\) uniformemente no quadrado \([0, 1]^2\). A probabilidade de o ponto cair dentro do quarto de círculo \(u^2 + v^2 \le 1\) é a razão entre as áreas, \((\pi/4)/1 = \pi/4\). Logo, \(\pi = 4\,P(U^2 + V^2 \le 1)\), e basta estimar essa probabilidade pela proporção de pontos que caem dentro do círculo:

set.seed(42)
n <- 1e6
u <- runif(n); v <- runif(n)
dentro <- (u^2 + v^2) <= 1                # vetor lógico
pi_hat <- 4 * mean(dentro)                # mean de lógico = proporção
ep <- 4 * sd(dentro) / sqrt(n)            # erro padrão de Monte Carlo
pi_hat                                    #> [1] 3.139992
ep                                        #> [1] 0.001643296
pi_hat + c(-1, 1) * 1.96 * ep             #> [1] 3.136771 3.143213

O intervalo de 95% contém o valor verdadeiro \(\pi \approx 3{,}14159\). O erro padrão também pode ser obtido analiticamente: com \(p = \pi/4 \approx 0{,}785\), \(\mathrm{EP} = 4\sqrt{p(1-p)/n} \approx 4\sqrt{0{,}785 \times 0{,}215/10^6} \approx 0{,}00164\), em concordância com a simulação.

Exemplo resolvido: o problema do aniversário. Qual a probabilidade de que, em um grupo de 23 pessoas, ao menos duas façam aniversário no mesmo dia (ignorando anos bissextos)? O cálculo exato usa o complementar: \[ P(\text{ao menos uma coincidência}) = 1 - \frac{365 \times 364 \times \cdots \times 343}{365^{23}} \approx 0{,}5073. \] A simulação confirma o resultado, que costuma surpreender pela magnitude:

set.seed(2024)
coincide <- replicate(1e4, any(duplicated(sample(365, 23, replace = TRUE))))
mean(coincide)                      #> [1] 0.5175
1 - prod((365:343) / 365)           #> [1] 0.5072973

A diferença entre \(0{,}5175\) e \(0{,}5073\) é compatível com o erro de Monte Carlo, \(\sqrt{0{,}5 \times 0{,}5/10^4} = 0{,}005\) (cerca de dois erros padrão). A função replicate(n, expr) avalia a expressão n vezes — cada vez com novos números aleatórios — e é a ferramenta natural para simulações desse tipo.

Simulando o Teorema Central do Limite

A simulação é uma excelente ferramenta didática para “ver” resultados teóricos. Se \(X_i \sim \text{Exp}(1)\), a média de 30 observações tem esperança 1 e desvio padrão \(1/\sqrt{30} \approx 0{,}1826\). O código set.seed(7); medias <- replicate(10000, mean(rexp(30))) produz mean(medias) igual a \(1{,}0009\) e sd(medias) igual a \(0{,}1825\); um histograma de medias exibe a forma de sino prevista pelo TCL, apesar de a exponencial ser fortemente assimétrica.


3.9 Desempenho: Medindo Antes de Otimizar

A frase de Donald Knuth segundo a qual “a otimização prematura é a raiz de todo o mal” resume uma lição importante: antes de tentar acelerar um código, é preciso medir onde o tempo é gasto. O R oferece ferramentas para isso em vários níveis de detalhe:

  • system.time(expr) mede o tempo de uma única execução, separando o tempo de CPU do usuário (user), do sistema (system) e o tempo de relógio (elapsed);
  • microbenchmark::microbenchmark() (ou bench::mark()) executa as expressões muitas vezes e reporta a distribuição dos tempos, o que é essencial para operações rápidas, cujos tempos individuais são dominados por ruído;
  • Rprof() e o pacote profvis fazem o perfilamento (profiling) do código, indicando quais funções consomem mais tempo.

Exemplo resolvido: três maneiras de construir um vetor. Comparemos as três estratégias discutidas na seção sobre laços, para \(n = 10^5\):

n <- 1e5
cresce   <- function(n) { r <- c(); for (i in seq_len(n)) r <- c(r, i^2); r }
prealoca <- function(n) { r <- numeric(n); for (i in seq_len(n)) r[i] <- i^2; r }
vetoriz  <- function(n) seq_len(n)^2

system.time(cresce(n))     #>  elapsed 4.710
system.time(prealoca(n))   #>  elapsed 0.004
system.time(vetoriz(n))    #>  elapsed 0.001
identical(cresce(10), vetoriz(10))    #> [1] TRUE

(Os tempos exatos dependem da máquina; o que importa são as ordens de grandeza.) A versão que faz o vetor crescer é mais de mil vezes mais lenta que a pré-alocada, em concordância com a análise \(O(n^2)\) versus \(O(n)\). A versão vetorizada é ainda mais rápida, pois o laço ocorre em C, sem o custo de interpretar cada iteração. Para operações rápidas, o microbenchmark dá uma visão mais confiável:

library(microbenchmark)
x <- runif(1e4)
soma_laco <- function(x) { s <- 0; for (v in x) s <- s + v; s }
microbenchmark(laco = soma_laco(x), sum = sum(x), times = 100)
#> Unit: microseconds
#>  expr    min     lq    mean  median     uq      max neval
#>  laco 75.020 80.394 92.497  81.659 84.485 1121.445   100
#>   sum  6.484  6.502  6.814   6.525  6.947   16.582   100

A mediana é a medida de resumo mais adequada nesse caso, pois a distribuição dos tempos é assimétrica (observe o máximo de mais de 1 ms causado por interrupções do sistema operacional ou pela coleta de lixo).

Estratégias de otimização em ordem de preferência
  1. Use uma função vetorizada nativa (sum, cumsum, rowSums, colMeans, pmax, tabulate).
  2. Evite cópias desnecessárias: pré-aloque e não faça objetos crescerem dentro de laços.
  3. Escolha o algoritmo certo: reduzir a complexidade (por exemplo, de \(O(n^2)\) para \(O(n \log n)\)) supera qualquer microajuste.
  4. Use pacotes de alto desempenho para dados grandes (data.table, arrow, duckdb).
  5. Como último recurso, reescreva o trecho crítico em C++ com o pacote Rcpp.

3.10 Condições, Depuração e Testes

3.10.1 O Sistema de Condições

Erros, avisos e mensagens são, no R, objetos chamados condições, sinalizados respectivamente por stop(), warning() e message(). Assim como as exceções do Python, as condições podem ser capturadas e tratadas com tryCatch():

res <- tryCatch(
  log(-1),
  warning = function(w) {
    message("Aviso capturado: ", conditionMessage(w))
    NA                                      # valor retornado em caso de aviso
  }
)
#> Aviso capturado: NaNs produced
res                                         #> [1] NA

tryCatch(stop("falhou"),
         error   = function(e) conditionMessage(e),
         finally = cat("fim\n"))            # 'finally' executa sempre
#> fim
#> [1] "falhou"

tryCatch() interrompe a expressão ao capturar a condição e retorna o valor do tratador. Já withCallingHandlers() executa o tratador e permite que a expressão continue — útil, por exemplo, para registrar avisos em um log sem abortar o cálculo. A função try(expr, silent = TRUE) é uma versão simplificada, que retorna um objeto de classe "try-error" em caso de erro, permitindo que um laço sobre muitos arquivos continue mesmo que um deles esteja corrompido.

3.10.2 Depuração

Quando um erro ocorre, o primeiro passo é descobrir onde. As principais ferramentas são:

Ferramenta Função
traceback() Exibe a pilha de chamadas que levou ao último erro
browser() Inserido no código, pausa a execução e abre um console interativo naquele ambiente
debug(f) / debugonce(f) Executa f passo a passo na próxima chamada
options(error = recover) Ao ocorrer um erro, permite escolher e inspecionar qualquer quadro da pilha
Pontos de parada no RStudio Clique à esquerda do número da linha; equivalente a browser()

Dentro do modo de depuração, os comandos n (próxima linha), s (entrar na função chamada), c (continuar), Q (sair) e ls() (listar variáveis locais) permitem inspecionar o estado do programa linha a linha.

3.10.3 Testes Automatizados com testthat

Um teste automatizado é um trecho de código que verifica se uma função produz o resultado esperado em casos conhecidos. Testes funcionam como uma rede de segurança: ao modificar uma função, basta reexecutá-los para saber se algo deixou de funcionar (as chamadas regressões). O pacote testthat (Wickham 2011) é o padrão da comunidade R:

library(testthat)

test_that("coef_variacao reproduz exemplos calculados à mão", {
  expect_equal(coef_variacao(c(2, 4, 4, 5, 10)), 60)
  expect_equal(coef_variacao(c(2, 4, 4, 5, 10), percentual = FALSE), 0.6)
  expect_error(coef_variacao("a"), "numérico")
  expect_true(is.na(coef_variacao(c(1, NA))))
})
#> Test passed with 4 successes.

A função expect_equal() compara números com tolerância (usando a mesma ideia de all.equal()), o que é indispensável diante dos erros de arredondamento discutidos neste capítulo; expect_identical() exige igualdade exata. Bons testes cobrem: (i) casos típicos, com resultado verificado à mão; (ii) casos de borda, como vetores vazios, de comprimento 1 ou com NA; e (iii) propriedades matemáticas, como a invariância da variância por translação, \(\operatorname{Var}(X + c) = \operatorname{Var}(X)\). Em pacotes, os testes ficam na pasta tests/testthat/ e são executados por devtools::test() e pelo R CMD check.


3.11 Pacotes e o CRAN

Um pacote é a unidade de distribuição de código reutilizável em R: reúne funções, dados, documentação e testes. O repositório oficial é o CRAN, que submete todo pacote a verificações automáticas (R CMD check) em diversos sistemas operacionais antes de aceitá-lo. Outros repositórios importantes são o Bioconductor (bioinformática) e o GitHub (versões em desenvolvimento).

install.packages("dplyr")            # instala (uma única vez por máquina)
library(dplyr)                       # carrega (a cada sessão)
dplyr::filter(cadastro, Idade > 23)  # usa uma função sem carregar o pacote
installed.packages()[, "Version"]    # pacotes instalados e versões
update.packages()                    # atualiza pacotes
sessionInfo()                        # versão do R e pacotes carregados

A distinção entre instalar (baixar e copiar o pacote para a biblioteca local) e carregar (anexá-lo à sessão) é fundamental. O operador :: também resolve conflitos de nomes: tanto o pacote base stats quanto o dplyr possuem uma função filter(), e a última biblioteca carregada “mascara” a anterior — mensagem que o R exibe ao carregar o dplyr.

Para garantir a reprodutibilidade de um projeto ao longo do tempo, o pacote renv registra as versões exatas de todos os pacotes usados em um arquivo renv.lock, permitindo restaurar o mesmo ambiente em outra máquina com renv::restore().


3.12 Leitura e Escrita de Dados

O R base oferece read.csv() e write.csv(), mas o pacote readr (parte do tidyverse) é mais rápido, mais consistente e retorna tibbles. As principais funções são:

Formato Leitura Escrita Pacote
CSV com vírgula read_csv() write_csv() readr
CSV com ponto e vírgula (padrão brasileiro) read_csv2() write_csv2() readr
Texto delimitado read_delim() write_delim() readr
Excel read_excel() — readxl
SPSS, Stata, SAS read_sav(), read_dta(), read_sas() write_sav() etc. haven
Objeto R nativo readRDS() saveRDS() base

Arquivos gerados por planilhas em português costumam usar ; como separador de campos e , como separador decimal. Por isso existe read_csv2(), e é possível controlar a codificação e a localidade:

library(readr)
vendas <- read_csv2(
  "dados/vendas.csv",
  locale = locale(decimal_mark = ",", grouping_mark = ".", encoding = "UTF-8"),
  col_types = cols(
    data    = col_date(format = "%d/%m/%Y"),
    regiao  = col_factor(),
    valor   = col_double(),
    .default = col_guess()
  ),
  na = c("", "NA", "-")
)
problems(vendas)     # lista linhas que não puderam ser interpretadas
spec(vendas)         # mostra a especificação de colunas utilizada

write_csv(vendas, "dados/processados/vendas_limpas.csv")
saveRDS(vendas, "dados/processados/vendas.rds")   # preserva tipos e fatores

Especificar col_types explicitamente torna a leitura determinística: o readr deixa de “adivinhar” os tipos a partir das primeiras linhas, o que evita surpresas quando, por exemplo, uma coluna numérica contém um texto na linha 5.000.


3.13 Cadeias de Caracteres e Expressões Regulares

Grande parte dos dados reais chega como texto: nomes de municípios digitados de formas diferentes, endereços, respostas abertas de questionários, códigos de produtos. Limpar e extrair informação desses campos é uma tarefa diária do cientista de dados.

3.13.1 Funções Básicas

O R base oferece um conjunto de funções para manipular strings; o pacote stringr (parte do tidyverse) reorganiza essas funcionalidades com nomes consistentes (todas começam com str_), sempre recebendo o texto como primeiro argumento — o que combina bem com o pipe:

Tarefa R base stringr
Número de caracteres nchar(x) str_length(x)
Concatenar paste(x, y, sep = ""), paste0() str_c(x, y)
Subcadeia substr(x, 1, 4) str_sub(x, 1, 4)
Maiúsculas/minúsculas toupper(), tolower() str_to_upper(), str_to_lower()
Remover espaços trimws(x) str_trim(x), str_squish(x)
Detectar padrão grepl(p, x) str_detect(x, p)
Extrair padrão regmatches(x, regexpr(p, x)) str_extract(x, p)
Substituir sub(), gsub() str_replace(), str_replace_all()
Dividir strsplit(x, p) str_split(x, p)
Formatar números sprintf("%.2f", x), format() —
library(stringr)
cidades <- c("  Recife-PE ", "olinda-pe", "Caruaru - PE", NA)
str_squish(cidades)        # remove espaços nas pontas e espaços repetidos
#> [1] "Recife-PE"    "olinda-pe"    "Caruaru - PE" NA
str_length(cidades)        #> [1] 12  9 12 NA
nchar("Ciência")           #> [1] 7   (conta caracteres, não bytes)
paste0("x", 1:3)           #> [1] "x1" "x2" "x3"
paste(c("a", "b"), collapse = "+")         #> [1] "a+b"
sprintf("Média: %.2f (n = %d)", 7.856, 30L) #> [1] "Média: 7.86 (n = 30)"

Note que nchar("Ciência") retorna 7, embora o caractere “ê” ocupe 2 bytes na codificação UTF-8. A codificação de caracteres — o mapeamento entre símbolos e sequências de bytes — é uma fonte frequente de problemas com textos em português: um arquivo gravado em Latin-1 (ISO-8859-1) e lido como UTF-8 exibe “Ciência” no lugar de “Ciência”. Por isso, recomenda-se sempre declarar a codificação na leitura, como feito com locale(encoding = "UTF-8") na seção anterior.

3.13.2 Expressões Regulares

Uma expressão regular (regex) é uma linguagem compacta para descrever padrões de texto (Friedl 2006). Do ponto de vista teórico, as expressões regulares descrevem exatamente a classe das linguagens regulares, aquelas reconhecidas por autômatos finitos — um resultado clássico da teoria da computação devido a Kleene. Na prática, as bibliotecas modernas acrescentam extensões (como retrovisores e lookarounds) que vão além dessa classe. Os principais metacaracteres são:

Símbolo Significado Exemplo Casa com
. qualquer caractere a.c “abc”, “a-c”
\\d, \\w, \\s dígito, caractere de palavra, espaço \\d\\d “42”
[abc], [^abc] classe de caracteres (ou sua negação) [aeiou] qualquer vogal
^, $ início e fim do texto ^R textos que começam com “R”
*, +, ? 0 ou mais, 1 ou mais, 0 ou 1 repetições \\d+ “7”, “2024”
{m,n} entre \(m\) e \(n\) repetições \\d{3} exatamente três dígitos
(...) grupo de captura (\\d{2})/(\\d{2}) “05/03” (com dois grupos)
\\. ponto literal (escape) \\.br$ textos terminados em “.br”

Além desses, a barra vertical indica alternativa: o padrão PE|PB casa com “PE” ou com “PB”. Em R, a barra invertida precisa ser duplicada dentro de uma string comum: o padrão regex \d é escrito "\\d". A partir do R 4.0, é possível usar strings brutas, r"(\d{3})", que dispensam a duplicação.

Exemplo resolvido: extraindo CPFs de um texto. Um CPF formatado tem o formato ddd.ddd.ddd-dd. O padrão correspondente é: três dígitos (\\d{3}), ponto literal (\\.), três dígitos, ponto, três dígitos, hífen e dois dígitos:

cad <- c("CPF 123.456.789-09 cadastrado",
         "sem documento",
         "CPFs: 987.654.321-00 e 111.222.333-44")
pad <- "\\d{3}\\.\\d{3}\\.\\d{3}-\\d{2}"

str_detect(cad, pad)       #> [1]  TRUE FALSE  TRUE
str_count(cad, pad)        #> [1] 1 0 2
str_extract(cad, pad)      #> [1] "123.456.789-09" NA  "987.654.321-00"
str_extract_all(cad, pad)[[3]]
#> [1] "987.654.321-00" "111.222.333-44"

# Anonimização: mascara os nove primeiros dígitos
str_replace_all(cad, "\\d{3}\\.\\d{3}\\.\\d{3}", "***.***.***")
#> [1] "CPF ***.***.***-09 cadastrado" "sem documento"
#> [3] "CPFs: ***.***.***-00 e ***.***.***-44"

Observe que str_extract() retorna apenas a primeira ocorrência, enquanto str_extract_all() retorna todas (em uma lista, pois cada texto pode ter um número diferente de ocorrências). Os grupos de captura permitem extrair partes do padrão separadamente, e também reaproveitá-las na substituição por meio de retrovisores (\\1, \\2):

datas <- c("Coleta em 05/03/2024", "Coleta em 17/11/2023")
str_match(datas, "(\\d{2})/(\\d{2})/(\\d{4})")
#>      [,1]         [,2] [,3] [,4]
#> [1,] "05/03/2024" "05" "03" "2024"
#> [2,] "17/11/2023" "17" "11" "2023"

sub("(\\w+) (\\w+)", "\\2, \\1", "Jodavid Ferreira")   #> [1] "Ferreira, Jodavid"
Expressões regulares são fáceis de escrever e difíceis de ler

Uma regex que “parece funcionar” pode aceitar casos indevidos ou rejeitar casos válidos. O padrão de CPF acima, por exemplo, verifica apenas o formato, e não os dígitos verificadores. Teste sempre os padrões com exemplos positivos e negativos, documente o que cada padrão pretende capturar e, para formatos padronizados (datas, e-mails, URLs), prefira funções especializadas a regexes caseiras.


3.14 Datas e Horas

Datas parecem simples, mas escondem complicações consideráveis: meses com números diferentes de dias, anos bissextos, fusos horários e horário de verão. A estratégia do R é representar datas e horas como números contados a partir de uma origem fixa, a chamada época Unix (1.º de janeiro de 1970):

  • a classe Date armazena o número de dias desde 1970-01-01;
  • a classe POSIXct armazena o número de segundos desde 1970-01-01 00:00:00 UTC, acompanhado de um fuso horário.

Como são números, datas podem ser subtraídas, comparadas, ordenadas e usadas em sequências:

d <- as.Date("2024-03-05")
as.numeric(d)                         #> [1] 19787   (dias desde 1970-01-01)
typeof(d); class(d)                   #> [1] "double"   [1] "Date"
as.Date("05/03/2024", format = "%d/%m/%Y")      # formato brasileiro
as.Date("2024-12-25") - d             #> Time difference of 295 days
seq(d, by = "month", length.out = 3)  #> [1] "2024-03-05" "2024-04-05" "2024-05-05"
format(d, "%d de %B de %Y")           #> [1] "05 de março de 2024"  (em locale pt_BR)

Conferindo o intervalo: de 5 de março a 25 de dezembro de 2024 há \(26\) dias restantes em março, mais \(30 + 31 + 30 + 31 + 31 + 30 + 31 + 30\) dias de abril a novembro, mais \(25\) dias de dezembro, totalizando \(26 + 244 + 25 = 295\) dias. Os códigos de formato mais usados são %d (dia), %m (mês numérico), %B (nome do mês), %Y (ano com quatro dígitos), %H:%M:%S (hora, minuto e segundo).

O pacote lubridate (Grolemund e Wickham 2011) simplifica enormemente o trabalho com datas. Suas funções de leitura têm o nome da ordem dos componentes (dmy(), ymd(), mdy(), ymd_hms()), e as funções de acesso extraem componentes (year(), month(), wday(), quarter()):

library(lubridate)
dmy("05/03/2024")                      #> [1] "2024-03-05"
year(d); quarter(d)                    #> [1] 2024   [1] 1
wday(d, label = TRUE)                  #> [1] ter
floor_date(ymd("2024-03-17"), "month") #> [1] "2024-03-01"
leap_year(2024)                        #> [1] TRUE

t <- ymd_hms("2024-03-05 14:30:00", tz = "America/Recife")
with_tz(t, "UTC")                      #> [1] "2024-03-05 17:30:00 UTC"

Um ponto conceitual importante do lubridate é a distinção entre durações (intervalos físicos exatos, em segundos) e períodos (intervalos de “calendário”, como “um mês”). Somar um mês a 31 de janeiro, por exemplo, não tem resposta óbvia:

ymd("2024-01-31") + months(1)          #> [1] NA            (31/02 não existe)
ymd("2024-01-31") %m+% months(1)       #> [1] "2024-02-29"  (último dia válido)

Exemplo resolvido: idade em anos completos. Para uma pessoa nascida em 15/08/2001, qual a idade em 05/03/2024? Como o aniversário de 2024 ainda não ocorreu, a idade é \(2024 - 2001 - 1 = 22\) anos. Dividir o número de dias por 365,25 é uma aproximação que pode errar perto do aniversário; o lubridate faz a conta exata com intervalos de calendário:

nasc <- dmy("15/08/2001"); ref <- dmy("05/03/2024")
as.numeric(ref - nasc)                      #> [1] 8238      (dias)
interval(nasc, ref) / years(1)              #> [1] 22.55464
floor(interval(nasc, ref) / years(1))       #> [1] 22

3.15 O Ecossistema Tidyverse e o Pipe

Para realizar manipulação de dados moderna e eficiente, a comunidade utiliza amplamente o Tidyverse (Wickham e Grolemund 2017), uma coleção de pacotes estruturada sobre uma filosofia de código limpo, intuitivo e focado na ciência de dados. O pacote dplyr é o coração da manipulação de dados no ecossistema e fornece “verbos” fáceis de ler para manipulações tabulares:

  • filter(): Filtra linhas com base em critérios ou condições lógicas.
  • select(): Seleciona colunas específicas, permitindo remover ou reordenar atributos.
  • mutate(): Cria novas colunas ou modifica colunas existentes aplicando transformações matemáticas.
  • arrange(): Ordena as linhas do Data Frame com base nos valores de uma ou mais colunas.
  • summarise() (ou summarize()): Agrega dados, reduzindo as colunas a valores únicos (como calcular médias, somas, contagens).
  • group_by(): Agrupa os dados para realizar análises segmentadas por categoria (muito útil em conjunto com o summarise()).

Para encadear essas operações em sequência de forma elegante e evitar a criação desnecessária de variáveis temporárias ou códigos aninhados complexos, utilizamos o operador Pipe (%>% do pacote magrittr, ou o operador pipe nativo do R |> inserido em versões mais recentes). Ele envia o resultado da operação anterior diretamente como o primeiro argumento da função seguinte:

# Exemplo de fluxo de manipulação com dplyr e pipe
cadastro_filtrado <- cadastro |>
  filter(Idade > 20) |>
  select(Nome, Idade) |>
  arrange(desc(Idade))

3.15.1 A Semântica do Pipe

Matematicamente, o pipe corresponde à composição de funções escrita na ordem de leitura. A expressão x |> f() |> g() |> h() é equivalente a \(h(g(f(x)))\), ou seja, \((h \circ g \circ f)(x)\). A versão aninhada exige ler “de dentro para fora”, enquanto a versão com pipe é lida “da esquerda para a direita”, na mesma ordem em que as operações acontecem:

# Aninhado: difícil de ler
arrange(select(filter(cadastro, Idade > 20), Nome, Idade), desc(Idade))

# Com pipe: lê-se "pegue cadastro, ENTÃO filtre, ENTÃO selecione, ENTÃO ordene"
cadastro |> filter(Idade > 20) |> select(Nome, Idade) |> arrange(desc(Idade))

Há algumas diferenças entre os dois pipes:

Característica Pipe do magrittr Pipe nativo (R 4.1 ou superior)
Requer pacote Sim (magrittr, reexportado pelo dplyr) Não
Marcador de posição . (em qualquer argumento) _ (apenas argumento nomeado, R 4.2 ou superior)
Chamada sem parênteses (x %>% f) Permitida Não permitida (exige f())
Desempenho Pequeno custo de chamada de função Transformação sintática, sem custo
# Marcador de posição: passar o data frame para um argumento que não é o primeiro
cadastro %>% lm(Idade ~ Ativo, data = .)
cadastro |> lm(Idade ~ Ativo, data = _)

3.15.2 Os Verbos do dplyr em Detalhe

Para os exemplos a seguir, utilizaremos um pequeno conjunto de dados de vendas:

library(dplyr)
vendas <- tibble(
  vendedor = c("Ana", "Ana", "Bruno", "Bruno", "Carla", "Carla"),
  regiao   = c("Norte", "Norte", "Sul", "Sul", "Norte", "Sul"),
  mes      = c(1, 2, 1, 2, 1, 2),
  valor    = c(100, 150, 200, 120, 80, 160)
)
# select: escolher, renomear e reordenar colunas (com auxiliares)
vendas |> select(vendedor, valor)
vendas |> select(-mes)                       # remove a coluna mes
vendas |> select(starts_with("v"))           # vendedor, valor
vendas |> rename(montante = valor)

# filter: manter linhas que satisfazem condições (vírgula = E lógico)
vendas |> filter(regiao == "Norte", valor >= 100)
vendas |> filter(vendedor %in% c("Ana", "Carla") | valor > 150)

# mutate: criar ou transformar colunas (vetorizado)
vendas |> mutate(
  valor_mil = valor / 1000,
  categoria = case_when(
    valor >= 150 ~ "alta",
    valor >= 100 ~ "média",
    .default = "baixa"
  )
)

# arrange: ordenar linhas
vendas |> arrange(regiao, desc(valor))

3.15.3 Agrupamento e Sumarização: o Paradigma Split–Apply–Combine

A combinação group_by() + summarise() implementa a estratégia dividir–aplicar–combinar (split–apply–combine): (i) os dados são divididos em grupos segundo os valores de uma ou mais variáveis; (ii) uma função de resumo é aplicada a cada grupo; (iii) os resultados são combinados em uma nova tabela. Formalmente, se \(G_g\) é o conjunto de índices das observações do grupo \(g\), o total e a média por grupo são

\[ T_g = \sum_{i \in G_g} y_i, \qquad \bar{y}_g = \frac{1}{|G_g|}\sum_{i \in G_g} y_i . \]

Exemplo resolvido. Na tabela vendas, a região Norte tem os valores \(\{100, 150, 80\}\) e a região Sul tem \(\{200, 120, 160\}\). Assim, \(T_{\text{Norte}} = 330\), \(\bar{y}_{\text{Norte}} = 110\), \(T_{\text{Sul}} = 480\) e \(\bar{y}_{\text{Sul}} = 160\). O código correspondente é:

vendas |>
  group_by(regiao) |>
  summarise(
    n      = n(),
    total  = sum(valor),
    media  = mean(valor),
    maximo = max(valor),
    .groups = "drop"
  )
#> # A tibble: 2 × 5
#>   regiao     n total media maximo
#>   <chr>  <int> <dbl> <dbl>  <dbl>
#> 1 Norte      3   330   110    150
#> 2 Sul        3   480   160    200

O group_by() também altera o comportamento do mutate(): as funções de resumo passam a ser calculadas dentro de cada grupo. Por exemplo, a participação percentual de cada venda no total de sua região:

vendas |>
  group_by(regiao) |>
  mutate(participacao = 100 * valor / sum(valor)) |>
  ungroup()
# Ana, mês 1 (Norte): 100 / 330 = 30,3%

Outros auxiliares úteis: count() (contagens de frequência), distinct() (linhas únicas), slice_max()/slice_min() (as \(k\) maiores/menores linhas), across() (aplicar a mesma função a várias colunas) e o argumento .by, que realiza um agrupamento temporário sem group_by():

vendas |> count(regiao, sort = TRUE)
vendas |> summarise(across(c(mes, valor), mean), .by = vendedor)
vendas |> slice_max(valor, n = 2)

3.15.4 Junções (Joins)

Na prática, a informação está espalhada por várias tabelas que precisam ser combinadas por meio de uma ou mais chaves. As junções do dplyr correspondem às operações da álgebra relacional (e ao JOIN da linguagem SQL). Sejam \(X\) e \(Y\) duas tabelas e \(k\) a chave:

Função Resultado
inner_join(X, Y) Apenas linhas cuja chave aparece em \(X\) e em \(Y\)
left_join(X, Y) Todas as linhas de \(X\); colunas de \(Y\) preenchidas com NA quando não há correspondência
right_join(X, Y) Todas as linhas de \(Y\)
full_join(X, Y) Todas as linhas de \(X\) ou de \(Y\)
semi_join(X, Y) Linhas de \(X\) que têm correspondência em \(Y\) (sem adicionar colunas)
anti_join(X, Y) Linhas de \(X\) sem correspondência em \(Y\) (útil para diagnosticar chaves)
vendedores <- tibble(
  vendedor = c("Ana", "Bruno", "Diego"),
  admissao = c(2019, 2021, 2023)
)

vendas |> left_join(vendedores, by = "vendedor")
# Carla não está em 'vendedores': sua coluna 'admissao' fica NA

vendas |> inner_join(vendedores, by = join_by(vendedor))
# Linhas de Carla são descartadas; Diego não aparece (não tem vendas)

vendedores |> anti_join(vendas, by = "vendedor")
# Retorna Diego: vendedor cadastrado sem nenhuma venda
Chaves duplicadas multiplicam linhas

Se a chave não for única em nenhuma das tabelas, a junção produz o produto cartesiano das linhas correspondentes: uma chave que aparece 3 vezes em \(X\) e 4 vezes em \(Y\) gera \(3 \times 4 = 12\) linhas. Sempre verifique a unicidade da chave (por exemplo, vendedores |> count(vendedor) |> filter(n > 1)) e o número de linhas antes e depois da junção.


3.16 Dados Tidy e o Pacote tidyr

O conceito de dados arrumados (tidy data), formalizado por Wickham (2014), estabelece três regras para a organização de uma tabela:

  1. Cada variável forma uma coluna;
  2. Cada observação forma uma linha;
  3. Cada tipo de unidade observacional forma uma tabela.

Essa definição corresponde à noção estatística de matriz de dados \(n \times p\), em que as linhas são as unidades amostrais e as colunas as variáveis medidas. Todos os pacotes do tidyverse — inclusive o ggplot2, apresentado no Capítulo de Visualização de Dados com ggplot2 — assumem dados nesse formato. Uma discussão mais aprofundada do conceito aparece no Capítulo de Análise Exploratória de Dados com Python.

O pacote tidyr fornece as funções para converter tabelas entre os formatos largo (wide) e longo (long):

  • pivot_longer(): transforma várias colunas em pares nome–valor (do largo para o longo);
  • pivot_wider(): faz a operação inversa (do longo para o largo).

Exemplo. Suponha as notas de três alunos em duas provas, no formato largo:

aluno prova1 prova2
Ana 7,0 8,5
Bia 6,0 9,0
Caio 8,0 7,5

Aqui, “prova” é uma variável que está codificada nos nomes das colunas, o que viola a regra 1. A versão tidy tem \(3 \times 2 = 6\) linhas e as colunas aluno, prova e nota:

library(tidyr)
notas_largo <- tibble(
  aluno  = c("Ana", "Bia", "Caio"),
  prova1 = c(7.0, 6.0, 8.0),
  prova2 = c(8.5, 9.0, 7.5)
)

notas_longo <- notas_largo |>
  pivot_longer(
    cols = starts_with("prova"),
    names_to = "prova",
    names_prefix = "prova",
    values_to = "nota"
  )
notas_longo
#> # A tibble: 6 × 3
#>   aluno prova  nota
#>   <chr> <chr> <dbl>
#> 1 Ana   1       7
#> 2 Ana   2       8.5
#> 3 Bia   1       6
#> 4 Bia   2       9
#> 5 Caio  1       8
#> 6 Caio  2       7.5

# No formato longo, resumir por prova é imediato
notas_longo |> summarise(media = mean(nota), .by = prova)
#> prova 1: (7 + 6 + 8) / 3 = 7,0 ; prova 2: (8,5 + 9 + 7,5) / 3 = 8,33

# E voltamos ao formato largo
notas_longo |>
  pivot_wider(names_from = prova, values_from = nota, names_prefix = "prova")

Outras funções úteis do tidyr são separate_wider_delim() (dividir uma coluna em várias), unite() (juntar colunas), drop_na() (remover linhas com NA), replace_na() e complete() (explicitar combinações ausentes).


3.17 Um Fluxo de Trabalho Completo

Para consolidar os conceitos, considere o conjunto de dados mtcars, nativo do R, com 32 automóveis e 11 variáveis. Queremos saber, para cada número de cilindros, o consumo médio (em km/l) e a potência mediana, apenas para carros com câmbio manual.

library(dplyr)

# 1 milha por galão (mpg) = 1,609344 km / 3,785411784 l ≈ 0,4251 km/l
resumo <- mtcars |>
  tibble::rownames_to_column("modelo") |>
  as_tibble() |>
  filter(am == 1) |>                              # câmbio manual
  mutate(
    km_l = mpg * 1.609344 / 3.785411784,
    cyl  = factor(cyl)
  ) |>
  group_by(cyl) |>
  summarise(
    n            = n(),
    km_l_media   = mean(km_l),
    km_l_dp      = sd(km_l),
    hp_mediana   = median(hp),
    .groups = "drop"
  ) |>
  arrange(desc(km_l_media))

resumo

Esse pequeno pipeline mostra a sequência típica de uma análise: preparar a tabela (rownames_to_column, as_tibble), selecionar o subconjunto de interesse (filter), derivar variáveis (mutate), agrupar e resumir (group_by + summarise) e ordenar o resultado (arrange). O passo natural seguinte é a visualização, assunto do Capítulo de Visualização de Dados com ggplot2.


3.18 Boas Práticas e Estilo de Código

Código é lido muito mais vezes do que é escrito. Seguir um guia de estilo consistente — como o tidyverse style guide — facilita a colaboração e reduz erros. As recomendações principais são:

  • Nomes: use snake_case, substantivos para objetos (dados_limpos) e verbos para funções (calcular_cv()); evite sobrescrever nomes de funções nativas (c, T, mean, data).
  • Espaçamento: espaços ao redor de operadores (x <- y + 1) e após vírgulas; nunca antes de vírgulas.
  • Linhas: limite-as a cerca de 80 caracteres; em pipelines, coloque cada etapa em uma linha.
  • TRUE/FALSE por extenso: T e F são variáveis comuns e podem ser redefinidas (T <- 0).
  • Comentários: explique o porquê, não o quê; o código já diz o que faz.
  • Não use setwd() nem rm(list = ls()) em scripts: use projetos e reinicie a sessão (Ctrl + Shift + F10) para garantir que o script roda do zero.
  • Funções pequenas e testáveis: se um trecho de código foi copiado e colado mais de duas vezes, transforme-o em função.
  • Reprodutibilidade: fixe a semente de números aleatórios com set.seed(), registre sessionInfo() e use renv em projetos de longa duração.

Os pacotes styler (formata o código automaticamente) e lintr (aponta problemas de estilo e possíveis erros) automatizam boa parte dessas verificações.


3.19 Resumo do Capítulo

  • O R descende da linguagem S, criada nos Bell Labs, e foi desenvolvido por Ihaka e Gentleman com semântica inspirada no Scheme; é software livre, interpretado e mantido pelo R Core Team.
  • O RStudio e os projetos .Rproj organizam o trabalho e eliminam caminhos absolutos; a documentação é acessada com ? e help().
  • Todo dado em R é um vetor. Os tipos atômicos (logical, integer, double, character, complex, raw) obedecem a uma hierarquia de coerção; números reais têm precisão finita (IEEE 754), o que exige cuidado em comparações.
  • A vetorização e a regra da reciclagem permitem traduzir fórmulas matemáticas quase literalmente em código, sem laços.
  • As estruturas de dados se classificam por dimensão e homogeneidade: vetor, matriz e array (homogêneos); lista e data frame (heterogêneos). Fatores representam variáveis categóricas.
  • A indexação pode ser feita por posição, por exclusão, por máscara lógica ou por nome; em listas, [ retorna sublista e [[ retorna o conteúdo.
  • Operações matriciais (%*%, t(), solve(), crossprod()) permitem implementar diretamente resultados de álgebra linear, como o estimador de mínimos quadrados.
  • NA, NULL, NaN e Inf têm significados distintos; o NA é propagado pelas operações e exige is.na() e na.rm.
  • Funções são objetos de primeira classe, com argumentos padrão, avaliação preguiçosa e escopo léxico; a família apply e o purrr substituem laços por maps.
  • Os números de ponto flutuante têm espaçamento relativo constante, com unidade de arredondamento \(u = 2^{-53}\); há limites de overflow e underflow (contornados com a escala logarítmica), e fórmulas algebricamente equivalentes podem diferir drasticamente em estabilidade numérica, como mostra o cancelamento catastrófico na fórmula de uma passada da variância.
  • O R adota a semântica de cópia na modificação: atribuições não copiam objetos e funções não alteram seus argumentos; ambientes e objetos R6 são as exceções, com semântica de referência.
  • O escopo léxico é implementado por ambientes encadeados; fechamentos guardam o ambiente em que foram criados, e <<- permite manter estado entre chamadas. Map, Filter e Reduce são os funcionais clássicos.
  • Além do S3, o R possui os sistemas de objetos S4 (formal, com validação e despacho múltiplo) e R6 (com semântica de referência).
  • As distribuições de probabilidade seguem a convenção d/p/q/r; os números pseudoaleatórios vêm de geradores determinísticos (Mersenne Twister), tornados reprodutíveis por set.seed(). A transformação inversa e o método de Monte Carlo, com erro padrão de ordem \(1/\sqrt{n}\), permitem simular e aproximar quantidades de interesse.
  • O desempenho deve ser medido (system.time, microbenchmark, profvis) antes de ser otimizado; vetorização e pré-alocação evitam custos quadráticos.
  • Erros, avisos e mensagens são condições tratadas com tryCatch(); traceback(), browser() e debug() auxiliam a depuração, e o testthat automatiza a verificação de funções.
  • O stringr e as expressões regulares permitem detectar, extrair e substituir padrões em textos; datas (Date) e datas-horas (POSIXct) são números contados a partir de 1970-01-01, e o lubridate facilita sua leitura e aritmética.
  • O tidyverse fornece leitura de dados (readr), verbos de manipulação (dplyr), junções relacionais e reestruturação de dados tidy (tidyr), encadeados com o pipe |> ou %>%.

3.20 Exercícios Propostos

  1. (Teórico) Determine, sem usar o computador, o resultado e o tipo (typeof) de cada expressão: (a) c(1L, TRUE, 2.5); (b) c(FALSE, "0"); (c) sum(c(TRUE, TRUE, FALSE, NA), na.rm = TRUE); (d) as.integer(-2.7). Justifique com base na hierarquia de coerção.

  2. (Teórico) Explique por que 0.1 * 3 == 0.3 retorna FALSE. Escreva uma função quase_igual(a, b, tol = 1e-8) que compare dois números reais com tolerância relativa \(|a - b| \le \text{tol} \cdot \max(|a|, |b|)\).

  3. (Teórico) Usando a fórmula da regra da reciclagem, calcule à mão o resultado de 1:7 * c(1, -1, 0) e indique se o R emitirá um aviso. Confira no computador.

  4. (Prático) Sem utilizar laços nem as funções var() e sd(), escreva uma expressão vetorizada que calcule o desvio padrão amostral de x <- c(12, 15, 9, 20, 14). Compare com sd(x).

  5. (Teórico/Prático) Considere o sistema linear \(3x_1 + 2x_2 = 12\) e \(x_1 - x_2 = -1\). (a) Escreva-o na forma matricial \(\mathbf{A}\mathbf{x} = \mathbf{b}\); (b) calcule \(\det(\mathbf{A})\) e \(\mathbf{A}^{-1}\) à mão; (c) resolva-o em R com solve() e verifique o resultado com %*%.

  6. (Prático) Gere uma matriz de delineamento \(\mathbf{X}\) com intercepto e uma covariável x <- 1:20, simule y <- 3 + 0.5 * x + rnorm(20) (com set.seed(123)) e calcule \(\hat{\boldsymbol\beta} = (\mathbf{X}^\top\mathbf{X})^{-1}\mathbf{X}^\top\mathbf{y}\) usando crossprod() e solve(). Compare com coef(lm(y ~ x)).

  7. (Teórico) Dada a lista l <- list(a = 1:3, b = list(c = "x", d = 4)), diga o que retornam e de que classe são: l[1], l[[1]], l$b$d, l[["b"]]["c"].

  8. (Prático) Crie um fator com as respostas c("concordo", "neutro", "discordo", "concordo", "concordo") em uma escala ordinal adequada. Obtenha a tabela de frequências absolutas e relativas (table() e prop.table()) e a moda.

  9. (Teórico) Explique a diferença entre NA, NULL e NaN, dando um exemplo de situação real de dados em que cada um apareceria. Por que NA == NA não retorna TRUE?

  10. (Prático) Escreva uma função padronizar(x, na.rm = TRUE) que retorne os escores \(z_i = (x_i - \bar{x})/s\). Verifique que o resultado tem média 0 e desvio padrão 1. Em seguida, aplique-a a todas as colunas numéricas de mtcars com sapply() e com purrr::map_dfc().

  11. (Teórico) Considere k <- 2; f <- function(x) x * k; g <- function() { k <- 100; f(5) }. Qual o valor de g()? Explique usando o conceito de escopo léxico.

  12. (Prático) Usando o conjunto mtcars e o dplyr, obtenha, para cada combinação de cyl e gear, o número de carros, a média de mpg e o desvio padrão de hp, ordenando pela média de mpg decrescente.

  13. (Prático) Crie duas tabelas, alunos (matrícula, nome) e notas (matrícula, disciplina, nota), em que há um aluno sem notas e uma nota de matrícula inexistente. Aplique inner_join, left_join, full_join e anti_join e explique o número de linhas obtido em cada caso.

  14. (Prático) A tabela relig_income (pacote tidyr) está em formato largo. Converta-a para o formato longo com pivot_longer(), calcule a proporção de cada faixa de renda dentro de cada religião e retorne ao formato largo com pivot_wider().

  15. (Teórico) Mostre que x |> f(y) |> g() equivale a g(f(x, y)) e discuta as vantagens de legibilidade do pipe. Em que situação o marcador de posição _ é necessário?

  16. (Teórico) Converta \(0{,}625\) e \(0{,}2\) para a base 2, usando o método das multiplicações sucessivas. Qual deles é representado exatamente em ponto flutuante? Confira com sprintf("%.20f", x).

  17. (Teórico/Prático) Sabendo que o espaçamento entre doubles no intervalo \([2^k, 2^{k+1})\) é \(2^{k-52}\), determine o menor inteiro positivo \(N\) tal que N + 1 == N retorna TRUE. Verifique no R.

  18. (Prático) Gere x <- 1e8 + rnorm(1000) e calcule a variância pela fórmula de uma passada e pela fórmula de duas passadas. Compare com var(x) e explique a discrepância usando o conceito de número de condição. Repita com deslocamentos \(10^4\), \(10^6\), \(10^8\) e \(10^{10}\) e organize os resultados em uma tabela.

  19. (Prático) Implemente a função logsumexp(a) e use-a para calcular a log-verossimilhança de uma mistura de duas normais, \(\sum_i \log\{0{,}5\,\phi(x_i; 0, 1) + 0{,}5\,\phi(x_i; 3, 1)\}\), trabalhando apenas com dnorm(..., log = TRUE).

  20. (Teórico) Explique, usando tracemem(), quantas cópias de um vetor de \(10^6\) elementos ocorrem em a <- runif(1e6); b <- a; b[1] <- 0; a[1] <- 0. Por que o resultado seria diferente se a e b fossem objetos R6?

  21. (Prático) Escreva uma função cria_acumulador() que retorne uma função que, a cada chamada com um número \(x\), devolva a média de todos os valores recebidos até então (sem armazenar os valores, apenas a soma e a contagem). Explique o papel do ambiente de definição e do operador <<-.

  22. (Prático) Usando Reduce() com accumulate = TRUE, calcule a sequência de saldos de uma aplicação que rende 1% ao mês, com depósitos mensais de R$ 100,00 durante 12 meses. Confira o saldo final com a fórmula da soma de uma progressão geométrica.

  23. (Teórico/Prático) Verifique à mão que \(P(X \le 1)\) para \(X \sim \text{Poisson}(2)\) vale \(3e^{-2} \approx 0{,}406\) e confira com ppois(). Em seguida, use qnorm() para obter os quantis que delimitam 90%, 95% e 99% da área central da normal padrão.

  24. (Prático) Use o método da transformação inversa para simular da distribuição de Weibull, cuja função de distribuição é \(F(x) = 1 - \exp\{-(x/\lambda)^k\}\), \(x \ge 0\). Compare a média amostral de \(10^5\) simulações com \(\lambda = 2\) e \(k = 1{,}5\) com o valor teórico \(\lambda\,\Gamma(1 + 1/k)\) e com rweibull().

  25. (Prático) Estime por Monte Carlo a integral \(\int_0^1 e^{-x^2}\,dx\) como a média de \(g(U) = e^{-U^2}\), com \(U \sim U(0, 1)\). Reporte a estimativa, o erro padrão e um intervalo de 95%, e compare com integrate(). Quantas simulações seriam necessárias para obter um erro padrão menor que \(10^{-4}\)?

  26. (Prático) Com o microbenchmark, compare três formas de calcular as médias das linhas de uma matriz \(1000 \times 100\): um laço for, apply(M, 1, mean) e rowMeans(M). Discuta os resultados.

  27. (Prático) Escreva testes com testthat para a função padronizar() do Exercício 10, cobrindo: média zero e desvio padrão um do resultado, comportamento com NA, erro para entrada não numérica e vetor de comprimento 1.

  28. (Prático) Dado o vetor tel <- c("(81) 99876-5432", "81 3456-7890", "telefone: (11)91234-5678", "sem contato"), escreva uma expressão regular que extraia o DDD e o número de cada telefone, usando str_match(). Em seguida, padronize todos no formato (DD) NNNNN-NNNN ou (DD) NNNN-NNNN.

  29. (Prático) Para as datas de nascimento c("15/08/2001", "29/02/2000", "01/01/1999"), calcule a idade em anos completos em 05/03/2024, o dia da semana em que cada pessoa nasceu e o número de dias até o próximo aniversário. Discuta o caso de quem nasceu em 29 de fevereiro.


3.21 Leituras Recomendadas

  • Wickham e Grolemund (2017) — R for Data Science: referência principal para o tidyverse, cobrindo importação, arrumação, transformação, visualização e comunicação de dados. A segunda edição está disponível gratuitamente on-line.
  • Wickham (2019) — Advanced R: para quem deseja compreender a linguagem em profundidade (vetores, subconjuntos, funções, ambientes, programação funcional e os sistemas de objetos S3, S4 e R6).
  • Wickham (2014) — Tidy Data: artigo que formaliza o conceito de dados arrumados.
  • Ihaka e Gentleman (1996) — artigo original de apresentação do R, com a motivação do desenho da linguagem.
  • Becker, Chambers, e Wilks (1988) e Chambers (2008) — obras de referência sobre a linguagem S e sobre a filosofia de programação para análise de dados que originou o R.
  • R Core Team (2024) — documentação oficial da linguagem, incluindo os manuais An Introduction to R e R Language Definition, disponíveis em https://cran.r-project.org/manuals.html.
  • Morettin e Singer (2021) — para revisar os conceitos estatísticos (média, variância, regressão) implementados nos exemplos deste capítulo.
  • Goldberg (1991) — What Every Computer Scientist Should Know About Floating-Point Arithmetic: o texto clássico sobre o padrão IEEE 754, erros de arredondamento e cancelamento catastrófico.
  • Higham (2002) — tratamento rigoroso da precisão e da estabilidade de algoritmos numéricos, incluindo o cálculo de somas e variâncias.
  • Chan, Golub, e LeVeque (1983) e Welford (1962) — análise comparativa dos algoritmos para o cálculo da variância amostral e o algoritmo de uma passada numericamente estável.
  • Robert e Casella (2004) — referência abrangente sobre métodos de Monte Carlo em Estatística, da geração de variáveis aleatórias aos métodos MCMC.
  • Matsumoto e Nishimura (1998) e Knuth (1997) — o gerador Mersenne Twister e a teoria clássica dos geradores de números pseudoaleatórios, incluindo os geradores congruenciais lineares.
  • Friedl (2006) — Mastering Regular Expressions: referência definitiva sobre expressões regulares e seus mecanismos internos.
  • Grolemund e Wickham (2011) e Wickham (2011) — artigos de apresentação dos pacotes lubridate (datas e horas) e testthat (testes automatizados).