4 Subsetting

este capítulo está em construção. O que segue abaixo é apenas um rascunho.

4.1 Introdução

Referência básica: Advanced R (2e) cap. 4 and cap. 5.

Na aula de hoje, aprenderemos operações mais concretas, mas muito essenciais.

Haverão três aprendizados principais:

(i) como funciona o sistema de subsetting, e sua relação com os tipos de valores;

(ii) as ferramentas de control flow no R;

(iii) o sistema de condições e suas ferramentas.

Para os exemplos, considere:

x <- c(1.3, 7.4, 6.0, 5.9)
m <- matrix(1:9, nrow = 3, ncol = 3)
colnames(m) <- c("A", "B", "C")
df <- as.data.frame(m)
l <- list("a" = 1:3, "b" = "oi", "c" = 4:6)

4.2 Subset - Operador [

4.2.1 Operador [

O operador [ é utilizado após um vetor x[...], para selecionar um subconjunto de seus elementos.

O conteúdo em ... pode tomar seis formas.

4.2.2 Operador [ Com Vetores Atômicos

Seis ‘insumos’ para selecionar com [:

A1. Inteiros positivos: retornam elementos nas posições:

  • x[1] #> 1.3.
  • x[1:3] #> 1.3 7.4 6.0.
  • x[c(1,3)] #> 1.3 6.0.

A2. Inteiros negativos: retornam todos os elementos menos os das posições.

  • x[-c(1,3)] #> 7.4 5.9

Obs: decimais são truncados x[1.7] == x[1], e factors ‘viram’ inteiros.

A3. Vetores lógicos: retornam os elementos nas posições onde tem-se TRUE’s.

  • x[c(TRUE, TRUE, FALSE, FALSE)] #> 1.3 7.4.

A4. Vetores de texto: retornam os elementos com os nomes escolhidos.

  • c(a = 1, b = 2, c = 3)[c("a", "c")] #> 1 3.

Reciclagem: para relacionar vetores de tamanhos diferentes, o R multiplica o tamanho do menor, se for múltiplo do maior: x[c(TRUE, FALSE)].

A5. Nada: x[] retorna o vetor original.

  • Será relevante para matrizes, e para substituir o conteúdo, deixando a estrutura intacta.

A6. Zero: retorna um vetor de tamanho 0.

Note que 1. e 4. podem reordear um vetor x[c(1, 3, 2)], bem como retornar um único elemento.

4.2.3 Operador [ Com Matrizes e Arrays

Com matrizes (vetores com dimensão), existem três ‘métodos’:

M1. Múltiplos vetores: um vetor para cada dimensão:

  • m[c(TRUE, FALSE, TRUE), c("B", "A")].
  • Um vetor omisso retorna todos os ‘elementos da dimensão’: a[, c(1, 3)].
  • Para arrays, use um argumento para cada dimensão a[1:2, 2, , 4].

M2. Vetor único: como matrizes são vetores, podemos tratá-las como tal:

  • m[5:7] do elemento 5 até o 7 (a ordem é “column-major”).
  • Podemos usar um vetor lógico aqui. E, como dimensão é um mero meta-dado, uma matriz lógica.

M3. Matrizes seletoras: onde cada coluna é uma dimensão, e cada linha um elemento quisto:

  • m[rbind(c(1,1), c(3,1), c(2,4))].
  • Para arrays, precisamos de três ou mais colunas.

4.2.4 Operador [ Com Listas e Dataframes

Com listas, não temos nenhuma diferença. [ sempre retorna uma lista, mesmo ao selecionar um único elemento.

E para data frames? O que df[1:2] deve retornar?

E df[1:2, ]? É “natural” que esses métodos coexistam?

Com dataframes, que são uma ‘lista com cara de matriz’, ambos os métodos existem.

4.2.5 Operador [ e Simplificação

O operador [ simplifica a dimensão do resultado.

  • Em matrizes/arrays, dimensões de length 1 são desfeitas:
    • m[1,], e m[,1]. Ou também m[1:9].
  • Ou ao selecionar uma única coluna de um dataframe no ‘modo matriz’ df[..., 2].
    • Mas selecionar linhas, mais de uma coluna, ou pelo ‘modo elemento’, mantêm a dimensão.

Isso é controlado pelo argumento drop = TRUE do operador. Podemos alterá-lo: df[1, , drop = FALSE].

Tibbles tem drop = FALSE por padrão, sempre retornando outra tibble/nunca simplificando (alá a preguiça).

4.3 Subset - Operador [[

4.3.1 Operador [[

Lembre que, em listas, [ sempre retorna uma lista – retorna o(s) elemento(s) e a ‘estrutura em volta’.

Para retornar apenas um (único) elemento, usamos os operadores [[...]] ou $....

Qual deve ser o resultado de x[1] == x[[1]]? Porque?

Pode ser útil usar [[ mesmo assim?

4.3.2 Operador [[ Com Listas

Dois ‘insumos’ para selecionar com [[:

L1. Um escalar inteiro: selecionando o elemento pelo índide.

L2. Um escalar string: selecionando pelo nome.

No segundo caso, podemos usar a abreviação l$a, no lugar de l[["a"]].

Obs:

  • Selecionar com um vetor seleciona recursivamente na lista, x[[c(1, 2)]] é similar à x[[1]][[2]].
  • $ faz matching parcial nos nomes, use options(warnPartialMatchDollar = TRUE).

4.4 Subset - Seleções Erradas e Assignment

4.4.1 Seleções Erradas

E se selecionarmos um elemento que não existe? A resposta é suco de R:

x fun -Int OOB +Int OOB Chr OOB NA 0-length
Atomic [ x NA NA N-length NA 0-length
List [ x NULL NULL N-length NULL 0-length
Atomic [[ Error Error Error Error Error
List [[ Error Error NULL NULL Error
NULL [[/[ NULL NULL NULL NULL NULL

Obs: o comportamento é o mesmo com ’x’s de tamanho zero.

Não é importante decorar essa matriz, mas note:

  • [ é mais permissivo, retorna “nada” em vez de um erro.
    • Com -Int, retorna x, faz sentido.
    • O que pode representar “nada” em listas? E em atômicos?
  • [[ é menos permissivo, retorna erro em vez de nada.
    • Mas com listas, temos duas exceções.
  • O que significa selecionar com NA? Em qual dos tipos de conteúdo ele se encaixa?

Para lidar com essas inconsistências, existem as seguintes funções:

4.4.2 Assingment

Todos os operadores de subset podem ser combinados com <-: x[i] <- ....

x[[i]] <- NULL remove o item de uma lista, x[i] <- list(NULL) adiciona um elemento NULL.

Subsetar com nada seleciona o ‘conteúdo’ de um objeto, e pode ser combinado com assignment para manter a estrutura inalterada:

df[] <- lapply(df, as.integer) # df se mantem um dataframe
df <- lapply(df, as.integer) # df vira uma lista

4.5 Control Flow - Condicionais

4.5.1 Control Flow

As ferramentas de control flow são aquelas que alteram a ordem de execução de um dado código.

No R, temos dois grupos principais:

4.5.2 If e Else

Podemos criar uma expressão que apenas é avaliada se uma certa condição for verdade. Se não for, podemos complementar com expressões auxiliares:

x <- 60
if (x > 90) {
  "A"
} else if (x > 80) {
  "B"
} else if (x > 50) {
  "C"
} else {
  "F"
}

4.5.3 If e Else

Características gerais:

  • A condição deve gerar TRUE ou FALSE (os escalares). Outras coisas geram erros.

  • Podemos escrever essas expressões em uma linha, sem “{”.

  • Podemos utilizá-las para assinalar valores: x <- if (TRUE) 1 else 2.

    • Se não houver nenhum else, if retorna um NULL.
  • Mais pra fente, veremos que if é uma função! `if(cond, expr1, expr2)`.

4.5.4 If e Else Vetorizado

A função ifelse() funciona para um vetor de condições, diferentemente de if, que exigia um único valor booleano.

Para cada elemento, ela checa a condição, e avalia uma ou outra expressão, salvando os resultados em um vetor.

O que o código abaixo vai retornar?

x <- 1:10
ifelse(x > 5, NA, x)

Outra opção é dplyr::case_when(), que permite vários pares condição-resultado:

k <- 1:10
dplyr::case_when(
  k %% 35 == 0 ~ "fizz buzz",
  k %% 5 == 0 ~ "fizz",
  k %% 7 == 0 ~ "buzz",
  is.na(k) ~ "???",
  TRUE ~ as.character(k)
)
#>  [1] "1"    "2"    "3"    "4"    "fizz" "6"    "buzz" "8"    "9"    "fizz"

4.5.5 And And e Or Or

Existem os operadores “and and” && e “or or” || que garantem sempre um único valor booleano (dão erros com vetores longos), além de serem “short circuit”.

c(FALSE, TRUE) && TRUE
#> erro por usar vetor longo

c(FALSE) && stop()
#> FALSE pois para no `FALSE`

c(FALSE) & stop() #gera erro,
#> erro, pois não para no `FALSE`

Por isso, use && e || em if, & e | em ifelse().

4.5.6 Switch

Existe uma maneira mais simples de escrever uma expressão if de “escolha de opções”, com a função switch():

if (x == "a") {
    "option 1"
  } else if (x == "b") {
    "option 2" 
  } else if (x == "c") {
    "option 3"
  } else {
    stop("`x` inválido")
}
switch(x,
  a = "option 1",
  b = "option 2",
  c = "option 3",
  stop("`x` inválido")
)

4.5.7 Infix Operator

Muitas vezes, queremos substituir o valor de uma variável caso ela seja NULL. Muitas vezes, funções usam isso para definir argumentos padrão.

Para isso, temos o infix operator:

k <- NULL
k <- k %||% 10

4.6 Control Flow - Loops

4.6.1 Loops For

Loops for são usados para iterar em elementos de um vetor:

for (item in vector) expr

item é literalmente um objeto, cujo valor vai sendo atualizado a cada iteração, e fica armazenado no ambiente global.

Dicas gerais:

  • O R é uma lingua vetorizada, use operações vetorizadas no lugar de loops!
ifelse(x < 0, -x, x)
for (i in seq_along(x)) {
  if (x[1] < 0) x[1] <- -x[1] else x[1] <- x[1]
}
  • Ao usar um loop para criar uma variável, crie o “contêiner” de antemão, pois é bem mais rápido:
x <- NULL; for(i in 1:5) x <- c(x, i)
x <- NULL; for(i in 1:5) x[[i]] <- i
  • Prefira usar seq_along(x) no lugar de 1:length(x).

  • Cuidado que loops costuma remover atributos. Use seq_along() e acesse os itens você mesmo:

x <- as.Date(c("2020-01-01", "2010-01-01"))
for (i in x) print(i)

4.6.2 Map

Em muitas linguagens, um map é uma higher-order-function que aplica uma função aos elementos de uma coleção.

Este é um padrão funcional, e será discutido na aula 5, mas tem sua semelhança com loops.

No R, existem vários maps:

lapply(x, \(a) a + 1)
purrr::map(x, \(a) a + 1)

E existem muitas variações:

sapply(x, \(a) a + 1)
replicate(1:10, rnorm(1000))
apply(df, 1, \(a) a + 1)
Map(\(a) a + 1, x)

4.6.3 Outros Loops

Se você não tem um vetor de opções de antemão, não sabe sobre quais valores quer iterar, você pode:

  • Usar um loop while (condition) {...}, que repete uma ação enquanto uma condição valer.

  • Ou, ainda mais flexível, repeat(...), que repete uma ação para sempre até encontrar um break.

É uma boa prática usar a opção menos flexível possível, para dar menos brechas para erros. Prefira usar o for, ou lapply/purrr::map.

4.7 Control Flow - Keywords

4.7.1 Keywords

A keyword next pula para a próxima iteração, e break termina o loop/condicional.

Como será a ordem do loop abaixo?

for (i in 1:10) {
  if (i < 3) next
  print(i)
  if (i >= 5) break
}

4.8 Condition System

4.8.1 Conditions

O sistema de condições serve para funções indicarem ao usuário alguma informação, como a existência de erros.

Existem três tipos de informação:

  • Erros: muito severos, indicam que nem foi possível terminar a execução da função
  • Avisos: médiamente severos, indicam alga provávelmente errado, mas que não impediu a execução.
  • Mensagens: pouco severos, normalmente indicam que a função realizou alguma correção para o usuário.

4.8.2 Condition Functions

Dentro da função, chamadas à stop(), warning(), ou message() criam a condição. stop(), inclusive, para a execução da função.

Também existe rlang::abort(), rlang::warn(), rlang::inform().

O usuário pode encapsular uma call em funções como try() e tryCatch() para ignorar ou lidar com condições.

4.8.3 Signaling

Autores de funções devem aprender quando e como indicar cada tipo de condição. Se tiver curiosidade, veja a seção 8.2 do AdvR.

Considere as função de teste do pacote varr.

test$category <- function(arg, options, env) {
  arg_name <- rlang::ensym(arg)

  if (!all(arg %in% options)) {
    cli_abort(
      "{.var {arg_name}} must contain only {.or {options}}",
      call = env
    )
  }
}

test$unused <- function(arg, x, class, env) {
  arg_name <- ensym(arg)

  if (class(x) == class) {
    cli_inform(
      "{.val {arg_name}} is unused if `x` is of class {class}.",
      call = env
    )
  }
}

4.8.4 Ignoring

No R, podemos ignorar condições:

Considere o padrão abaixo.

x <- NULL
try(x <- call_que_pode_dar_errado())

4.8.5 Handling

Podemos lidar com condições usando tryCatch() e withCallingHandlers(), que especificam funções para se executar no caso de cada tipo de condição:

tryCatch(
  error = function(cnd) {
    # código a ser executado quando ocorrer um erro
  },
  warning = function(cnd) {
    # código a ser executado quando um aviso for sinalizado
  },
  message = function(cnd) {
    # código a ser executado quando uma mensagem for sinalizada
  },
  call_rodada_com_handlers
)

Qual a diferença das duas?

  • tryCatch() define exiting handlers, após a condição aparecer, a função handler relevante é ativada, e seu valor é retornado para onde tryCatch() foi chamado.

  • withCallingHandlers() define calling handlers, após a condição, a função handler relevante é ativada, e seu valor é retornado para onde a call que gerou a condição estava.

Isto é, uma sai da call, outra não. A primeira é mais intuitiva para erros, a segunda para avisos ou mensagens.


Complemento

Recapitulando

4.8.5.1 Subsetting

Seis ‘insumos’ para selecionar com [:

A1. Inteiros positivos: retornam elementos nas posições.

A2. Inteiros negativos: retornam todos os elementos menos os das posições.

A3. Vetores lógicos: retornam os elementos nas posições onde tem-se TRUE’s.

A4. Vetores de texto: retornam os elementos com os nomes escolhidos.

A5. Nada: x[] retorna o vetor original.

A6. Zero: retorna um vetor de tamanho 0.

Reciclagem: para relacionar vetores de tamanhos diferentes, o R multiplica o tamanho do menor, se for múltiplo do maior: x[c(TRUE, FALSE)].

Três ‘métodos’ para selecionar com dimensões:

M1. Múltiplos vetores: um vetor para cada dimensão. M2. Vetor único: como matrizes são vetores, podemos tratá-las como tal. M3. Matrizes seletoras: onde cada coluna é uma dimensão, e cada linha um elemento quisto.

Com listas e dataframes:

  • Com listas, não temos nenhuma diferença. [ sempre retorna uma lista, mesmo ao selecionar um único elemento.
  • Com dataframes, que são uma ‘lista com cara de matriz’, temos o método normal e o de matriz.

O operador [ simplifica a dimensão do resultado. Veja o argumento drop = TRUE.

Para retornar apenas um (único) elemento, usamos os operadores [[...]] ou $....

Dois ‘insumos’ para selecionar com [[:

L1. Um escalar inteiro: selecionando o elemento pelo índide. L2. Um escalar string: selecionando pelo nome.

Selecionar elementos que não existem está relacionado a comportamentos inconsistentes, as vezes retornando erros, NULL, ou NA.

Todos os operadores de subset podem ser combinados com <-: x[i] <- ....

4.8.5.2 Control Flow

As ferramentas de control flow são aquelas que alteram a ordem de execução de um dado código.

Também vimos:

  • Operadores and and e or or.
  • Ooperador infix.
  • Keywords next e break.

4.8.5.3 Condições

O sistema de condições serve para funções indicarem ao usuário alguma informação, como a existência de erros.

Existem três tipos de informação: erros (stop()), avisos (warning()), e mensagens (message()).

Podemos ignorar condições:

E podemos lidar com condições usando tryCatch() e withCallingHandlers(), que especificam funções para se executar no caso de cada tipo de condição.

  • tryCatch() define exiting handlers, após a condição aparecer, a função handler relevante é ativada, e seu valor é retornado para onde tryCatch() foi chamado.
  • withCallingHandlers() define calling handlers, após a condição, a função handler relevante é ativada, e seu valor é retornado para onde a call que gerou a condição estava.

Dicionário de Funções

Categoria Função Descrição
subset [, [[, $ extração de elementos
control flow if, switch(), ifelse(), %||% if else, ifelse vetorizado, e infix operator
control flow for, while, repeat loops
control flow break, next keywords
operadores &&, || lógica
outros seq(), seq.int(), seq_along(), seq_len() geração de sequências
maps lapply(), apply(), purrr::map() maps
maps sapply(), replicate(), Map() maps (variações)
conditions stop(), warning(), message() signaling conditions
conditions rlang::abort(), rlang::warn(), rlang::inform() signaling conditions (rlang)
conditions try(), supressWarnings(), supressErrors() ignoring conditions
conditions tryCatch(), withCallingHandlers handling conditions

Referências

Referência básica: Advanced R (2e) cap. 4 and cap. 5.

@@bs4-math@@