Programação funcional para o cotidiano no trabalho
Como escrever funções no R que facilitam a sua vida
Este tutorial tem os seguintes pré-requisitos:
- Nível intermediário em R;
- Familiaridade com o pacote tidyverse, em especial dplyr, assim como com o operador pipe (%>%);
- Conhecimento básico de funções.
Introdução
Você está trabalhando com uma base de dados e iniciou a exploração dos mesmos. Sem perceber, está fazendo mais ctrl + c e ctrl +v, com algumas alterações, do que resolvendo de fato sua demanda. Que tal tiramos alguns minutos para otimizarmos o processo?
Minhas atribuições em meu emprego — sou estatístico na UnB — me fazem lidar com dados todos os dias (amém). Dentre as bases de dados que lidamos, a de alunos é a mais utilizada, devido não só a sua importância para a universidade como um todo, mas também por sua diversidade e riqueza de informações.
Infelizmente não irei disponibilizar os dados, pois estou mexendo com informações pessoais. Sinto muito. No entanto, farei meu melhor para explicitar cada decisão tomada ao longo do processo de análise, decisão e programação.
O problema
Recebemos diariamente diversas demandas por dados em formatos específicos. Na maioria dos casos encaminhamos os demandantes para as publicações já existentes, como nosso Anuário Estatístico. Em outros, como o que vou apresentar aqui, atendemos à demanda específica.
O demandante, docente de um dos cursos de engenharia da universidade, solicitou ajuda para conseguir a série histórica dos dados de ingressantes e formados, de todos os cursos, desde 2010 até 2019, por semestre. Essa tabela específica até existe no Anuário, porém apenas para o ano-base do relatório. Como não seria possível conseguir todas as informações solicitadas apenas consultando os Anuários anteriores (por motivos que extrapolam esta publicação), nos restou atender ao pedido do docente.
Conceitos apresentados
Ingressante: Aluno que iniciou seus estudos em algum curso da universidade no período considerado. Seu ingresso pode ter ocorrido de diversas formas — por vaga nova (vestibular, PAS ou SISU), por vaga remanescente (vagas não ocupadas de anos anteriores, oferecidas para quem já tem um curso superior, para dupla-diplomação, transferência optativa, dentre outras) ou por outros casos (convênios, transferências obrigatórias, etc.).
Formado: Aluno que concluiu algum dos cursos da universidade no período considerado, obtendo um título (bacharelo, licenciatura, especialização, mestrado ou doutorado).
Analisando a tabela, especialmente a versão em Excel, vemos que há duas informações distintas (ingressantes e formados) dispostas em três níveis de agregação diferentes: os totais por curso, por unidade e o total geral. Ou seja, a replicação desses dados precisará considerar esse formato específico. Bom destacar que cada curso está ligado a uma unidade distinta — Estatística, por exemplo, pertence ao Instituto de Exatas (IE), que é uma das 26 Unidades Acadêmicas da UnB.
A solução
Na minha abordagem, este problema tem quatro etapas a serem vencidas:
- Criação de função que crie a tabela de ingressantes;
- Criação de função que crie a tabela de formados;
- Criação de função que junte em uma só tabela as duas acima;
- Iteração (for loop) que crie todas as tabelas dos anos que quero.
Como a base é a mesma e as condições são apenas duas (uma para os ingressantes e outra para os formados), julguei ser necessário utilizar apenas a variável de ano para a iteração, o que facilita a resolução do problema. Indo ponto a ponto então.
Tabela de ingressantes
Nossa tabela precisa, inicialmente, filtrar apenas os estudantes que ingressaram no ano de interesse. Depois é feita a agregação (contagem) por curso, separando os semestres (1º e 2º) em colunas distintas e aplicando a soma deles, para termos os ingressantes anuais por curso. Devemos primeiro testar o código isoladamente — caso dê certo, transformamos o bloco em função.
Como pode ser visto acima, estou usando os pacotes tidyverse, que é uma coleção de pacotes que inclui, dentre eles, dplyr e stringr, que serão ambos utilizados neste projeto; e janitor, que tem funções que facilitam bastante nossa vida de manipulador de dados, como a adorn_totals, que cria uma linha e/ou coluna de totais. O código que cria nossa primeira tabela está todo comentado. Parece muita coisa para criar só uma tabela, mas cada comando tem sua função e, o mais importante, funcionou, conforme figura abaixo.
Curiosidades
A função count, do dplyr, já faz automagicamente o agrupamento dos dados em categorias, ou seja, por isso que não utilizei a função group_by antes da count. Uma alternativa seria usar group_by seguido por tally;
A função pivot_wider (e pivot_longer) foi adicionada recentemente, e tem a mesma funcionalidade da spread (e gather), então elas podem ser utilizadas intercambiavelmente. No entanto spread e gather foram descontinuadas, isto é, não terão mais seu código atualizado.
Ok, fizemos a primeira agregação, por cursos. Falta a agregação por unidade e o total geral. A tabela de ingressantes por unidade é a mesma da de ingressantes por curso, mudando apenas o nível de agregação. Já adiantei um pouco o processo mudando o nome da variável que tem a descrição da unidade (UnidadeDesc) para ser igual ao nome completo do curso (`Nome Anuário`), pois iremos juntar as duas tabelas.
Conseguimos, sem problemas, os ingressantes por unidade, conforme abaixo.
Agora “só” precisamos juntar essas duas tabelas numa só tabela e adicionar uma linha com o total geral. Simples, certo?
Uma primeira tentativa
tab_ing <- ingressantes %>%
bind_rows(totais) %>%
arrange(Unidade, `Nome Anuário`)
Mostra que a ordenação ainda não está do jeito que queremos: linha de total da unidade primeiro, seguida pelas linhas dos cursos da unidade, conforme o excel publicado no site. Como resolver então?
Após alguns testes, descobri que que botar um hífen (-) no nome da unidade da primeira tabela (com os cursos), sem modificar a segunda tabela (de totais), resolveria meu problema. Pois é. Voltando ao código dos ingressantes por curso então:
Agora sim, do jeito que queremos:
Pronto, só falta inserir a linha de total. Simples, né? Hmmm. Nossa tabela tem uma particularidade — uma linha de total iria duplicar os valores, pois estamos somando duas vezes as mesmas métricas (o número de ingressantes em todos os cursos e número de ingressantes em todas as unidades, que são iguais). Tem algumas formas de resolver isso e eu escolhi utilizar a função bind_rows, tanto para continuar no tidyverse quanto para aprender a generalizar essa situação para utilizações futuras. Note que estou fazendo todo um processo dentro de bind_rows, por isso tive que explicitar o ponto “.” no primeiro select.
Explicando melhor
O ponto “.” serve para indicar à função que deve utilizar o resultado do que está ao lado esquerdo (ou acima) do pipe (%>%) que, aqui, é a tabela ordenada já com todas as observações dos cursos e das unidades.
Como resultado, temos uma singela porém correta linha de total geral:
Terminamos de fazer a tabela de UM ano. Agora vamos generalizar, incluindo-a numa função. Essa parte é realmente simples, basta darmos um nome, definir a função utilizando a variável que já escolhemos (ano) e abraçar tudo com um par de chaves. Aproveitei para arrumar os nomes das colunas. E não pode esquecer de, ao fim, “chamar” o que queremos como resultado: a tabela finalizada.
Pronto! Primeira tabela completa. Agora se eu quiser, por exemplo, saber dos ingressantes de 2010, faço:
ingressantes_2010 <- geraIng(2010)
Bem mais elegante que copiar, colar e buscar o ano para substituir, certo? Com isso, nosso primeiro objetivo está completo (C̶r̶i̶a̶ç̶ã̶o̶ ̶d̶e̶ ̶f̶u̶n̶ç̶ã̶o̶ ̶q̶u̶e̶ ̶c̶r̶i̶e̶ ̶a̶ ̶t̶a̶b̶e̶l̶a̶ ̶d̶e̶ ̶i̶n̶g̶r̶e̶s̶s̶a̶n̶t̶e̶s̶). Passando ao segundo objetivo.
Tabela de formados
Para fazer a tabela de formados, precisamos mudar apenas o nome da função, o nome de algumas variáveis e algumas condições — enquanto que os ingressantes são todos que ingressaram num determinado ano, os formados são todos que conseguiram se formar no ano de referência. Dessa forma, já vou pular para o código pronto:
Da mesma forma que os ingressantes, podemos criar a tabela de formados para o ano que desejarmos utilizando o comando:
formados2011 <- geraFor(2011)
Segundo objetivo completo (C̶r̶i̶a̶ç̶ã̶o̶ ̶d̶e̶ ̶f̶u̶n̶ç̶ã̶o̶ ̶q̶u̶e̶ ̶c̶r̶i̶e̶ ̶a̶ ̶t̶a̶b̶e̶l̶a̶ ̶d̶e̶ ̶f̶o̶r̶m̶a̶d̶o̶s̶). Agora precisamos juntar as duas tabelas, que é nosso terceiro objetivo.
Tabela de ingressantes e formados
Para juntar as duas tabelas anteriores, precisamos de alguma função de join. Vamos explorar as principais opções:
- left_join(ingressantes, formados) usaria apenas os cursos que tiveram ingressantes, o que poderia deixar de foras alguns cursos que tiveram formados;
- right_join(ingressantes, formados) usaria apenas os cursos que tiveram formados, o que poderia excluir cursos novos, que tiveram apenas ingressantes;
- semi_join e anti_join não cabem aqui, pois iriam limitar nossa junção, semelhantemente ao left e right joins;
- full_join é a solução então, pois irá utilizar todos os registros, de ambas as tabelas.
Como o full_join provavelmente irá criar NAs, vou aproveitar para substituí-los por zero (0), pois é o que realmente são. Por fim, vou retirar a coluna que utilizei como “truque” para conseguir que o total da unidade viesse antes dos totais por curso.
Agora sim, temos a tabela igual à publicada no site do Anuário Estatístico, concluindo nosso terceiro objetivo (C̶r̶i̶a̶ç̶ã̶o̶ ̶d̶e̶ ̶f̶u̶n̶ç̶ã̶o̶ ̶q̶u̶e̶ ̶j̶u̶n̶t̶e̶ ̶e̶m̶ ̶u̶m̶a̶ ̶s̶ó̶ ̶t̶a̶b̶e̶l̶a̶ ̶a̶s̶ ̶d̶u̶a̶s̶ ̶a̶c̶i̶m̶a̶). Poderíamos parar por aqui e gerar cada tabela com um comando separado, e depois salvá-las individualmente em um excel. Seria justo. Mas de acordo com o que botei lá no começo deste artigo, vamos ter que fazer essa automatização. Sorry.
Criar e salvar todas as tabelas, dado um intervalo de anos
O problema, descrito lá no começo, era de criar as tabelas de ingressantes e formados para cada ano solicitado. Já temos a tabela final e já conseguimos criar para cada um dos anos solicitados. Agora só falta conseguir criar para todos os anos solicitados. Como fazer isso? A solução mais comum é utilizando um for loop.
Antes disso, vamos pensar no formato dos dados. Pretendo salvar como Excel, então .xlsx. Para tal utilizo o pacote rio com o comando export (rio::export), que emprega a função write.xlsx para salvar as tabelas no formato decidido. Um detalhe interessante é que o primeiro argumento dessa função pode ser um único objeto ou uma lista. Se lista, a função salva cada um dos objetos da lista em uma aba separada, dentro de um só arquivo. Parece o ideal para o que buscamos.
Observação: caso você não queira instalar o pacote rio, é do pacote openxlsx que se origina a função write.xlsx.
Com isso mente, podemos voltar ao raciocínio da iteração: temos que criar uma lista e povoá-la com as tabelas dos anos escolhidos, nomeando os elementos corretamente, para que não haja confusão ao abrirmos o arquivo com todas as tabelas. O código para isso está abaixo.
Agora sim, conseguimos criar e salvar, de uma só vez, todas as tabelas dos anos escolhidos, finalizando o quarto e último objetivo (I̶t̶e̶r̶a̶ç̶ã̶o̶ ̶(̶f̶o̶r̶ ̶l̶o̶o̶p̶)̶ ̶q̶u̶e̶ ̶c̶r̶i̶e̶ ̶t̶o̶d̶a̶s̶ ̶a̶s̶ ̶t̶a̶b̶e̶l̶a̶s̶ ̶d̶o̶s̶ ̶a̶n̶o̶s̶ ̶q̶u̶e̶ ̶q̶u̶e̶r̶o̶).
Finalizo por aqui. Apesar de ter sido um caso específico aplicado à minha realidade de trabalho, tenho certeza que a abordagem e as técnicas utilizadas podem ser apropriadas para qualquer outra situação em que seja necessária a criação de tabelas específicas e a automatização do processo de criação das mesmas.
Obrigado por ficar até o fim, e até a próxima!