Funções e Dependências (Zen do R parte 5)

Nesta série de posts, estamos apresentamos a todos a nossa primeira tentativa de escrever um livro: O Zen do R! Durante as próximas semanas, todas as quartas, traremos para o nosso blog os capítulos que já escrevemos do livro e responderemos qualquer pergunta sobre o conteúdo.

Hoje o assunto é ideal para quem precisa escrever código reprodutível: funções e dependências.

Funções e dependências

Até este momento, foi abordada apenas uma forma de organizar os arquivos de uma análise: projetos. Entretanto existe ainda outra maneira, ainda mais interessante, de guardar análises. Se você programou em R, com certeza já se deparou com essa ferramenta, os bons e velhos pacotes ou bibliotecas. É surpreendentemente fácil criar um diretório que pode ser completamente acessado através da função library().

Quando uma tarefa de análise de dados aumenta em complexidade, o número de funções e arquivos necessários para manter tudo em ordem cresce exponencialmente. Um arquivo para ler os dados, outro para limpar os nomes das colunas, mais um para fazer joins… Cada um deles com incontáveis blocos de código que rapidamente se transformam em uma macarronada.

O primeiro passo para sair dessa situação é transformar tudo em funções. Essa tarefa está longe de simples, mas os benefícios são imensos; ao encontrar um erro no resultado, fica bem mais fácil depurar a função culpada do que uma coleção desordenada de código. Funções têm argumentos e saídas, enquanto código solto pode modificar globais e criar resultados tardios que são impossíveis de acompanhar sem conhecer profundamente a tarefa sendo realizada.

library(dplyr)
library(tibble)

# Limpar dados
mtcars_clean <- mtcars %>%
  rownames_to_column(var = "model") %>%
  as_tibble() %>%
  filter(cyl < 8)

# Selecionar carros com 4 cyl e tirar média de mpg e wt
mtcars_clean %>%
  filter(cyl == 4) %>%
  group_by(cyl) %>%
  summarise(
    mpg = mean(mpg),
    wt = mean(wt)
  )
#> # A tibble: 1 x 3
#>     cyl   mpg    wt
#>   <dbl> <dbl> <dbl>
#> 1     4  26.7  2.29

# Selecionar carros com 6 cyl e tirar média de drat e disp
mtcars_clean %>%
  filter(cyl == 6) %>%
  group_by(cyl) %>%
  summarise(
    drat = mean(drat),
    disp = mean(disp)
  )
#> # A tibble: 1 x 3
#>     cyl  drat  disp
#>   <dbl> <dbl> <dbl>
#> 1     6  3.59  183.

O código acima é somente um exemplo de análise. Como descrito pelos comentários, mtcars é limpa e depois são extraídas as médias de diferentes variáveis para duas seleções da tabela (número de cilindros igual a 4 e 6). Abaixo está descrita uma forma de transformar a maioria deste código em funções. É verdade que pela natureza simples do exemplo, fica difícil ver os benefícios do encapsulamento das tarefas de limpeza e resumo, mas perceba, por exemplo, que, se fosse necessário trocar mean() por median(), antes seria necessário alterar quatro linhas e agora apenas uma. Esse tipo de ganho a longo prazo pode salvar análises inteiras do caos.

library(dplyr)
library(tibble)

# Limpa tabela, filtrando cyl < cyl_max
clean <- function(data, cyl_max = 8) {
  data %>%
  rownames_to_column(var = "model") %>%
  as_tibble() %>%
  filter(cyl < cyl_max)
}

# Resume tabela onde cyl == cyl_max, tirando média das colunas em ...
summarise_cyl <- function(data, cyl_num, ...) {
  data %>%
  filter(cyl == cyl_num) %>%
  group_by(cyl) %>%
  summarise_at(vars(...), mean)
}

# 4 cyl, média de mpg e wt
mtcars %>%
  clean(cyl_max = 8) %>%
  summarise_cyl(cyl_num = 4, mpg, wt)
#> # A tibble: 1 x 3
#>     cyl   mpg    wt
#>   <dbl> <dbl> <dbl>
#> 1     4  26.7  2.29

# 6 cyl, média de drat e disp
mtcars %>%
  clean(cyl_max = 8) %>%
  summarise_cyl(cyl_num = 6, drat, disp)
#> # A tibble: 1 x 3
#>     cyl  drat  disp
#>   <dbl> <dbl> <dbl>
#> 1     6  3.59  183.

Um código bem encapsulado reduz a necessidade de objetos intermediários ( base_tratada, base_filtrada, etc.) pois para gerar um deles basta a aplicação de uma função. Além disso, programas com funções normalmente são muito mais enxutos e limpos do que scripts soltos, pois estes estimulam repetição de código. Às vezes é mais rápido copiar e colar um pedaço de código e adaptá-lo ao novo contexto do que criar uma função que generalize a operação desejada para as duas situações, mas os benefícios das funções são de longo prazo: ao encontrar um bug, haverá apenas um lugar para concertar; se surgir a necessidade de modificar uma propriedade, haverá apenas um lugar para editar; se aquele código se tornar obsoleto, haverá apenas um lugar para deletar.

Pense na programação funcional1 como ir à academia. No início o processo é difícil e exige uma quantidade considerável de esforço, mas depois de um tempo se torna um hábito e traz benefícios consideráveis para a saúde (neste caso, do código). As recomendações para quando criar uma nova função ou separar uma função em duas variam muito, mas normalmente é uma boa ideia não deixar uma única função ser encarregada de mais uma tarefa ou ficar longa/complexa demais.

No mundo ideal, na pasta R/ do seu projeto haverá uma coleção de arquivos, cada um com uma coleção de funções relacionadas e bem documentadas, e apenas alguns arquivos que utilizam essas funções para realizar a análise em si. Como dito anteriormente, isso fica muito mais fácil se você já tiver esse objetivo em mente desde o momento de criação do novo projeto.

::

No exemplo da seção anterior, é possível notar as chamadas para as bibliotecas dplyr e tibble. Elas têm inúmeras funções úteis, mas aqui somente algumas poucas foram utilizadas. Além disso, se o código fosse muito maior, ficaria impossível saber de uma biblioteca ainda está sendo utilizada; se não fosse mais necessário utilizar rownames_to_column(), qual seria a melhor forma de saber que pode ser removida a chamada library(tibble)?

A resposta para essa pergunta pode assustar: no código ideal, a função library() nunca seria chamada, todas as funções teriam seus pacotes de origem explicitamente referenciados pelo operador ::.

Esta subseção está separada porque ela de fato é um pouco radical demais. É excessivamente preciosista pedir para que qualquer análise em R seja feita sem a invocação de nenhuma biblioteca, apenas com chamadas do tipo biblioteca::funcao(). Muitas pessoas inclusive nem sabem que é possível invocar uma função diretamente através dessa sintaxe!

Se algum leitor estiver tendendo a seguir o caminho do TOC da programação, existem dois grandes benefícios em chamar todas as funções diretamente: - O código, no total, executa um pouco mais rápido porque são carregadas menos funções no ambiente global (isso é especialmente importante em aplicações interativas feitas em Shiny). - As dependências do código estão sempre atualizadas porque elas estão diretamente atreladas às próprias funções sendo utilizadas.

Existe um terceiro e importante benefício, mas este será abordado apenas no próximo capítulo. A título de curiosidade, o código anterior ficaria assim caso fosse escrito sem as chamadas para library():

# Referência ao pipe
`%>%` <- magrittr::`%>%`

# Limpa tabela, filtrando cyl < cyl_max
clean <- function(data, cyl_max = 8) {
  data %>%
    tibble::rownames_to_column(var = "model") %>%
    dplyr::as_tibble() %>%
    dplyr::filter(cyl < cyl_max)
}

# Resume tabela onde cyl == cyl_max, tirando média das colunas em ...
summarise_cyl <- function(data, cyl_num, ...) {
  data %>%
    dplyr::filter(cyl == cyl_num) %>%
    dplyr::group_by(cyl) %>%
    dplyr::summarise_at(dplyr::vars(...), mean)
}

# 4 cyl, média de mpg e wt
mtcars %>%
  clean(cyl_max = 8) %>%
  summarise_cyl(cyl_num = 4, mpg, wt)
#> # A tibble: 1 x 3
#>     cyl   mpg    wt
#>   <dbl> <dbl> <dbl>
#> 1     4  26.7  2.29

# 6 cyl, média de drat e disp
mtcars %>%
  clean(cyl_max = 8) %>%
  summarise_cyl(cyl_num = 6, drat, disp)
#> # A tibble: 1 x 3
#>     cyl  drat  disp
#>   <dbl> <dbl> <dbl>
#> 1     6  3.59  183.

Se serve de consolo, o RStudio facilita muito esse tipo de programação por causa da sua capacidade de sugerir continuações para código interativamente. Para escrever dplyr::, por exemplo, basta digitar d, p, l e apertar TAB uma vez. Com os ::, as sugestões passarão a ser somente de funções daquele pacote.


  1. Aqui o termo “programação funcional” é usado de forma figurativa. Na computação linguagens denominadas “funcionais” tem um modus operandi bastante específico não abordado neste capítulo.↩︎

comments powered by Disqus