Loops

Loops

Voltando ao nosso cálculo da mediana, vamos ao invés de ir em cada coluna, construir um loop:

saida <- vector("double", ncol(df))  # 1. saída
for (i in seq_along(df)) {            # 2. sequencia
  saida[[i]] <- median(df[[i]])      # 3. corpo
}
saida
## [1] -0.34986024 -0.26790199 -0.01824207 -0.75018611

Loops

Todo for loop tem três componentes:

  1. A saída (ou output): saida <- vector("double", ncol(df)). Antes de iniciar o loop, você sempre deve alocar espaço suficiente para a saída. Isso é muito importante para a eficiência: se você aumentar o loop for a cada iteração usando c() (por exemplo), o loop for será muito lento.

Uma maneira geral de criar um vetor vazio de determinado comprimento é a função vector(). Ele tem dois argumentos: o tipo do vetor (“logical”, “integer”, “double”, “character”, etc) e o comprimento do vetor.

Loops

  1. A sequência: i em seq_along(df). Isso determina sobre que fazer o loop: cada execução do loop for atribuirá i a um valor diferente de seq_along(df). É útil pensar em i como um pronome, como “it”.

Você pode não ter visto seq_along() antes. É uma versão segura do familiar 1:length(l), com uma diferença importante: se você tem um vetor de comprimento zero, seq_along() faz a coisa certa:

Loops

y <- vector("double", 0)
seq_along(y)
## integer(0)
1:length(y)
## [1] 1 0

Loops

  1. O corpo: saida[[i]] <- median(df[[i]]). Este é o código que faz o trabalho. É executado repetidamente, sempre com um valor diferente para i. A primeira iteração executará saída[[1]] <- median(df[[1]]), a segunda executará saída[[2]] <- median(df[[2]]) e assim por diante.

Variações do loop for

Variações do loop for

Existem três variações importantes do loop for básico:

  1. Modificando um objeto existente, em vez de criar um novo objeto;
  2. Loop sobre nomes ou valores, em vez de índices;
  3. Manipulação de saídas de comprimento desconhecido;

Variações do loop for

Suponha que queiramos aplicar uma função sobre as colunas de uma função repetidas vezes como abaixo.

rescale01 <- function(x) {
  rng <- range(x, na.rm = TRUE)
  (x - rng[1]) / (rng[2] - rng[1])
}

df$a <- rescale01(df$a)
df$b <- rescale01(df$b)
df$c <- rescale01(df$c)
df$d <- rescale01(df$d)

Podemos resolver esse problema com um loop:

for (i in seq_along(df)) {
  df[[i]] <- rescale01(df[[i]])
}

Variações do loop for

Existem três maneiras básicas de fazer um loop sobre um vetor. Até agora, mostramos o mais geral: repetir os índices numéricos com for (i em seq_along(xs)) e extrair o valor com x[[i]]. Existem duas outras formas:

  • Loop sobre os elementos: for (x in xs). Isso é mais útil se você se preocupa apenas com efeitos colaterais, como plotar ou salvar um arquivo, porque é difícil salvar a saída com eficiência.

  • Loop sobre os nomes: for (nm in names(xs)). Isso fornece um nome, que você pode usar para acessar o valor com x[[nm]]. Isso é útil se você deseja usar o nome em um título de plot ou em um nome de arquivo. Se você estiver criando uma saída nomeada, nomeie o vetor de resultados da seguinte forma:

Variações do loop for

results <- vector("list", length(x))
names(results) <- names(x)

A iteração sobre os índices numéricos é a forma mais geral, pois, dada a posição, é possível extrair o nome e o valor:

for (i in seq_along(x)) {
  name <- names(x)[[i]]
  value <- x[[i]]
}

Variações do loop for

Às vezes você pode não saber o tamanho que a saída terá. Por exemplo, imagine que você deseja simular alguns vetores aleatórios de comprimentos aleatórios. Uma solução é salvar os resultados em uma lista e depois combiná-los em um vetor único depois que o loop terminar.

means <- c(0, 1, 2)
out <- vector("list", length(means))
for (i in seq_along(means)) {
  n <- sample(100, 1)
  out[[i]] <- rnorm(n, means[[i]])
}
str(out)
## List of 3
##  $ : num [1:8] -0.2971 0.4064 0.0545 1.299 0.1446 ...
##  $ : num [1:14] 0.7 2.163 0.196 0.614 0.988 ...
##  $ : num [1:52] 1.656 2.675 4.011 3.071 -0.769 ...
str(unlist(out))
##  num [1:74] -0.2971 0.4064 0.0545 1.299 0.1446 ...

As funções map

As funções map

O padrão de criar um loop sobre um vetor, fazer algo em cada elemento e salvar os resultados é tão comum que o pacote purrr fornece uma família de funções para fazer isso. Há uma função para cada tipo de saída:

  • map() faz uma lista.
  • map_lgl() cria um vetor lógico.
  • map_int() cria um vetor inteiro.
  • map_dbl() cria um vetor duplo.
  • map_chr() cria um vetor de caractere.

As funções map

Cada função pega um vetor como entrada, aplica uma função a cada peça e retorna um novo vetor que tem o mesmo comprimento (e tem os mesmos nomes) que a entrada. O tipo do vetor é determinado pelo sufixo da função de mapa.

Depois de dominar essas funções, você descobrirá que leva muito menos tempo para resolver problemas de iteração. Mas você nunca deve se sentir mal ao usar um loop for em vez de uma função de mapa. As funções do mapa são um passo à frente em uma torre de abstração e podem levar muito tempo para você entender como elas funcionam. O importante é que você resolva o problema em que está trabalhando, e não escreva o código mais conciso e elegante (embora isso seja definitivamente algo que você deseja buscar!).

As funções map

df %>% map_dbl(mean)
##         a         b         c         d 
## 0.3269043 0.3619871 0.4902709 0.5355998
df %>% map_dbl(median)
##         a         b         c         d 
## 0.1233160 0.3575584 0.5199698 0.5195861
df %>% map_dbl(sd)
##         a         b         c         d 
## 0.3807706 0.3218500 0.3229475 0.2825503

As funções map

Existem alguns atalhos que você pode usar com .f para economizar um pouco de digitação. Imagine que você deseja ajustar um modelo linear para cada grupo em um conjunto de dados. O exemplo a seguir divide o conjunto de dados mtcars em três partes (uma para cada valor do cilindro) e ajusta o mesmo modelo linear a cada peça:

models <- mtcars %>% 
  split(.$cyl) %>% 
  map(function(df) lm(mpg ~ wt, data = df))

As funções map

A sintaxe para criar uma função anônima no R é bastante detalhada, assim, o pacote purrr fornece um atalho conveniente: uma fórmula unilateral.

models <- mtcars %>% 
  split(.$cyl) %>% 
  map(~lm(mpg ~ wt, data = .))

Aqui, usamos . como pronome: refere-se ao elemento da lista atual (da mesma maneira que i se referia ao índice atual no loop for).

As funções map

Podemos usar a função map também para extrair os elementos de models, como abaixo.

models %>% 
  map(summary) %>% 
  map_dbl(~.$r.squared)
##         4         6         8 
## 0.5086326 0.4645102 0.4229655
models %>% 
  map(summary) %>% 
  map_dbl("r.squared")
##         4         6         8 
## 0.5086326 0.4645102 0.4229655