Calculando Estatísticas Resumidas

Agregando os seus Dados

Clique aqui para assistir o vídeo da aula online do dia 23 de abril de 2021. E o chat.

Estatísticas Resumidas (summarize)

Nos últimos tutoriais, não mexemos com a unidade de análise de nosso banco de dados - sempre avaliamos os voos individuais, por exemplo. Porém, mesmo depois de vários filtros e mutates, os seus dados provavelmente tem dezenas, centenas ou milhares de linhas - é difícil incluir toda esta informação no seu relatório, e é impossível para o leitor entender tudo. É por isso que usamos estatísticas resumidas: médias, medianas etc.

Para gerar um número único que resume uma variável em nossa tabela, usamos o verbo summarize() que funciona perfeitamente dentro do fluxo da nossa análise com o pipe (%>%). O summarize() gera um novo tibble (tabela/data.frame) pequeno para conter as estatísticas resumidas, abandonando o nosso tibble original.

A função exige três elementos (i) o nome da nova variável no novo tibble; (ii) a função que vai agregar/resumir a variável, e (iii) a variável que será resumida. Veja o exemplo abaixo, que calcula a distância média de todos os voos.

flights %>% summarize(media_distance=mean(distance))
# A tibble: 1 x 1
  media_distance
           <dbl>
1          1040.

Fácil, não? Usando a variedade das funções estatísticas em R (ou qualquer pacote adicional), você pode calcular qualquer estatística que te interesse. Experimente com os exemplos na tabela.

Estatística Função em R
Média mean(variável)
Mediana median(variável)
Desvio padrão sd(variável)
Quantil (10%) quantile(variável, probs=0.1)
Máximo max(variável)
Mínimo min(variável)

A nossa nova tabela agregada pode conter mais de uma estatística resumida, cada uma em uma coluna nova:

flights %>% summarize(media_distance=mean(distance),
                      mediana_distance=median(distance),
                      sd_distance=sd(distance))

É comum incorporar estatísticas resumidas no texto do nosso documento de R Markdown. Lembra-se de códigos ‘in-line’, no qual usamos `r ` fora do chunk? Como podemos inserir a nossa estatística no texto do relatório? O resultado de summarize() ainda é um tibble, e uma tabela não cabe em um parágrafo. Temos que transformar o valor no tibble em um valor único.

Uma função bastante útil aqui é pull() (tirar). Ele transforma uma variável de um tibble para um vetor, e quando o vetor tem apenas um elemento (ou seja, o tibble tem apenas uma linha), o resultado é um valor único, perfeito para inserir em in-line código:

estatisticas <- flights %>% summarize(media_distance=mean(distance),
                                      mediana_distance=median(distance),
                                      sd_distance=sd(distance))

media_distance <- estatisticas %>% pull(media_distance)

Agora, posso gera a frase no relatório que se refere ao valor de media_distance:

"A distância média dos voos é `r media_distance`.

“A distância média dos voos é 1039.9126036”.

Habilidade Básica de Programação: Excluindo NAs

Vamos tentar mais uma estatística:

flights %>% summarize(dep_delay=mean(dep_delay))

Qual foi o resultado? NA? O que significa dados faltantes aqui? O padrão em R é reclamar quando tem um erro em potencial, forçando você a investigar. Isso pode ser chato às vezes, mas no longo prazo é uma medida necessária para garantir que você entenda o que você está calculando. Neste caso, na presença de pelo menos um valor NA na variável que estamos resumido, o R passa este NA para o resultado final, mesmo que existem milhares de outros valores prontos para serem resumidos. Isso é o nosso sinal que os nossos dados contém lacunas, e temos que deixar explícito para R como a tratar estes casos. Por enquanto vamos ignorar eles usando o argumento na.rm=TRUE, e calcular a estatística resumida apenas com os dados restantes:

flights %>% summarize(dep_delay=mean(dep_delay,na.rm=TRUE))


Habilidade Básica de Programação: Funções Novas

O R é muito flexível - se você quiser uma agregação não disponível em uma função atual, pode gerar a sua própria função. Escrever uma função depende de um formato padrão - um nome pela função, os insumos que a função recebe como argumentos, e o objeto que quer devolver como resultado da função.

nome_funcao <- function(insumo1, insumo2){

  resultado <- ...
  
  return(resultado)
    
}

Imagine que queremos calcular a razão entre o percentil 90 e o percentil 10. Não existe uma função pronta para calcular isso, então vamos escrever nós mesmos.

percentile_90_10 <- function(variavel) {
  
  calculo <- quantile(variavel, probs=0.9,na.rm=TRUE)/
    quantile(variavel, probs=0.1,na.rm=TRUE)
    
    return(calculo)
}

Usamos variavel aqui para fazer referência a qualquer vetor (coluna) que o usuário vai especificar como insumo no futuro, e cada vez que precisamos pegar este vetor dentro da função, usamos o mesmo nome, variavel.

Note que esta função aceita um vetor (apenas uma coluna do nosso tibble), e devolve um valor único, indicado por return(cálculo). Vamos aplicar a nossa nova função:

flights %>% summarize(percentile_90_10_distance=percentile_90_10(distance),
                      percentile_90_10_air_time=percentile_90_10(air_time))

Os resultados significam que há mais variação na variável distance (o percentil 90 é 11 vezes maior que o percentil 10) do que na variável air_time (apenas 6.8 vezes maior).

Isto é programação. Agora sabemos como trabalhar com os dois elementos fundamentais: objetos (data.frames/tibbles etc.) e funções. Tudo em R é uma combinação de objetos (substantivos) com funções (verbos) para criar a nossa receita de análise.

Grupos (group_by)

Quase sempre, os nossos dados estão organizados em grupos e subgrupos, pode ser anos, meses e dias, aeroportos, ou países. Frequentemente, nós queremos as estatísticas resumidas por ano, ou por país. O poder de summarize() é ampliado exponencialmente quando os resumos/agregações são feitos no nível de grupos e não para o banco de dados inteiro. O que define os grupos? Uma outra variável em nossa tabela.

Dado que podemos criar vários níveis/tipos de agrupamentos de nossos dados, temos que especificar quais nos queremos. Para definir os grupos relevantes, podemos criar um ‘grouped tibble’ usando o verbo group_by():

flights_por_aeroporto <- flights %>% group_by(origin)

Qual o resultado, flights_por_aeroporto, e como difere do banco de dados original de flights? Parece igual! O número de linhas e colunas é igual, nada mudou…Se digitamos o nome do novo objeto flights_por_aeroporto no ‘Console’ no canto baixo do RStudio, podemos ver uma pequena diferença: existe uma linha Groups: origin [3] que não existe no banco de dados original de flights. Este ‘3’ significa os três aeroportos de origem nos dados que usamos para agrupamento.

Na prática, group_by() sozinho não é útil para nada. Temos que combinar com mais uma função subsequente para gerar resultados interessantes. Por exemplo, vamos calcular a média da distância por aeroporto:

flights %>% group_by(origin) %>% 
  summarize(mean_distance=mean(distance))

Agora, a nova tabela de resumo tem três linhas, uma para cada aeroporto. Os três grupos correspondem às três estatísticas resumidas. Note que não mudamos nada no summarize() do último comando - só temos mais um verbo em nosso pipe, o group_by().

Os argumentos de group_by() são sempre as variáveis de agrupamento, e podem ser vários:

flights %>% group_by(origin, month) %>% 
  summarize(mean_distance=mean(distance))

Quantas linhas têm o resultado? Porque 36? Porque pedimos agrupamento por origem (3 possibilidades) e mês (12 possibilidades): \(3*12=36\). A unidade de análise na tabela final é o aeroporto-mês.

Note que o resultado de summarize() é sempre um tibble, então ele não precisa terminar o nosso fluxo de análise - podemos continuar processando o resultado de summarize() com todas as funções que já estamos acostumados a usar. Por exemplo, podemos combinar as funções filter e mutate para criar uma tabela apropriado para incluir em nosso relatório:

flights %>% group_by(origin, month) %>% 
  summarize(mean_distance=mean(distance)) %>%
  filter(origin!="LGA") %>%
  mutate(mean_distance_km=mean_distance*1.60934)

Outras funções de resumo também funcionam com group_by(). Quer o voo mais atrasado por aeroporto de origem? Use group_by conjunto com top_n (do tutorial anterior):

flights %>% group_by(origin) %>%
  top_n(1,dep_delay)

Recebemos três voos, o mais atrasado em EWR, o mais atrasado em JFK, e o mais atrasado em LGA.

Exercício 1: Análises por Grupo

Usando o banco de dados de flights no pacote nycflights13, responda às seguintes perguntas:

  1. Calcule a duração (air_time) média por companhia aérea (carrier).
Mostrar Código
flights %>%
    group_by(carrier) %>%
    summarize(media_air_time = mean(air_time, na.rm = T))
  1. Calcule a duração (air_time) média por companhia aérea (carrier) e mês (month).
Mostrar Código
flights %>%
    group_by(carrier, month) %>%
    summarize(media_air_time = mean(air_time, na.rm = T))
  1. Calcule o atraso médio (dep_delay) por aeroporto de origem (origin). Qual aeroporto tem o pior atraso?
Mostrar Código
flights %>%
    group_by(origin) %>%
    summarize(media_dep_delay = mean(dep_delay, na.rm = T)) %>%
    arrange(-media_dep_delay)
  1. Qual companhia aérea (carrier) tem o pior registro de atrasos (dep_delay) na média no aeroporto JFK?
Mostrar Código
flights %>%
    filter(origin == "JFK") %>%
    group_by(carrier) %>%
    summarize(media_dep_delay = mean(dep_delay, na.rm = T)) %>%
    arrange(-media_dep_delay)
  1. Você odeia atrasos. Qual é o pior mês para viajar do aeroporto JFK?
Mostrar Código
flights %>%
    filter(origin == "JFK") %>%
    group_by(month) %>%
    summarize(media_dep_delay = mean(dep_delay, na.rm = T)) %>%
    arrange(-media_dep_delay)

Número de observações por Grupo (tally)

Uma aplicação enormemente útil de group_by() é calcular o número de observações (linhas) em cada grupo do banco de dados.

flights %>% group_by(origin) %>% 
  tally()

Assim, é fácil comparar o número de voos em cada aeroporto. A função tally não precisa de argumentos.

Quantos voos decolaram de cada aeroporto de origem para cada destino?

flights %>% group_by(origin, dest) %>% 
  tally()

Exercício 2: Observações por Grupo

  1. Quantos voos decolaram de Nova Iorque em cada mês de 2013?
Mostrar Código
flights %>%
    group_by(month) %>%
    tally()
  1. Qual companhia aérea teve o maior número de voos em cada mês de 2013?
Mostrar Código
flights %>%
    group_by(month, carrier) %>%
    tally() %>%
    group_by(month) %>%
    top_n(1, n)
  1. Qual é a média do número de voos que decolaram dos três aeroportos, em cada mês?
Mostrar Código
flights %>%
    group_by(origin, month) %>%
    tally() %>%
    group_by(month) %>%
    summarize(media_n = mean(n, na.rm = T))
  1. Qual é a média mensal do número de voos que decolaram em cada aeroporto?
Mostrar Código
flights %>%
    group_by(origin, month) %>%
    tally() %>%
    group_by(origin) %>%
    summarize(media_n = mean(n, na.rm = T))
  1. Qual horário de partida (dep_time) é o segundo mais congestionado (medida pelo número de decolagens) em cada aeroporto? (O mais congestionado é o horário NA, então é mais interessante pegar o segundo mais congestionado).
Mostrar Código
flights %>%
    group_by(origin, dep_time) %>%
    tally() %>%
    group_by(origin) %>%
    top_n(2, n)

Mutate por Grupo

Não é apenas resumos que conseguimos executar por grupo. É comum também aplicar um mutate() por grupo. Esta combinação fornece muita flexibilidade e poder. Por exemplo, se quiser manter o tamanho e a unidade de análise do seu banco de dados original, e inserir a média do grupo como coluna, pode executar assim:

flights %>% group_by(origin) %>%
  mutate(media_distance=mean(distance,na.rm=TRUE))

Confirme no tibble resultante que o número de linhas não mudou, e que a média distância é igual para todos os voos do mesmo aeroporto, e varia entre aeroportos.

Qual a diferença conceitual entre summarize() e mutate()?

  1. summarize() sempre reduz o número de linhas no banco de dados - é uma agregação total ou por grupo.

  2. mutate() nunca reduz (ou aumenta) o número de linhas no banco de dados - apenas adiciona uma nova coluna.

Saindo de Agrupamentos (ungroup)

Agrupamentos são poderosos - eles permitem organizar os nossos dados na forma que faz sentido para a nossa análise sem calcular os denominadores separadamente. Mas tome cuidado: O R lembra de tudo. Quando você usa group_by() ele é mantido para o resto da operação (os pipes seguintes), e também dentro de objetos salvos. Isso é útil se queremos continuar com o mesmo agrupamento, mas pode gerar resultados inesperados se esquecemos que já agrupamos os nossos dados. Para tirar o agrupamento, temos que usar a função ungroup(), sem argumento.

Para demonstrar isso, veja a descrição do objeto salvo abaixo com a função de utilidade groups que imprime os grupos do tibble:

flights_media <- flights %>% group_by(origin) %>%
  mutate(media_distance=mean(distance,na.rm=TRUE))

groups(flights_media)
[[1]]
origin

O que acontece se usarmos o tibble agrupado em uma nova operação? Imagine que em mais três páginas de código queremos calcular a média de atrasos (dep_delay) com summarize:

flights_media %>% summarize(media_atraso=mean(dep_delay,na.rm=T))

Recebemos três linhas, mesmo pedindo só uma média! O agrupamento ainda restringe o escopo do resumo de summarize, e o R gerou uma média por aeroporto (origin). Mais geralmente, se esquecemos do agrupamento, é fácil gerar o resultado inesperado.

Para evitar esta situação chata, temos que sempre lembrar qual é a unidade de agrupamento. Para voltar analisar o banco de dados inteiro, temos que tirar o agrupamento com ungroup():

flights_media %>% ungroup() %>% 
  summarize(media_atraso=mean(dep_delay,na.rm=T))

Fácil de fazer, difícil de lembrar! Preste atenção!

Porcentagens

Um dos pedidos mais comuns é calcular porcentagens em R. Tome cuidado: já vi muitos cálculos errados, mesmo sendo um cálculo simples de porcentagem. A chave para evitar erros está na definição do denominador da fórmula da porcentagem:

\[\text{%} = \frac{\text{Valor}}{\text{Total do grupo relevante}}*100 \]

Queremos uma porcentagem para cada observação no banco de dados, então isso exige um mutate(). Por exemplo, se quisermos calcular a porcentagem da distância de cada voo na distância total de todos os voos, podemos calcular o total, e depois dividir cada valor pelo total:

flights %>% 
  mutate(Total_distance=sum(distance,na.rm=TRUE)) %>% 
  mutate(Pct_distance=100*(distance/Total_distance)))

Salvando digitação, podemos combinar as duas etapas com o mesmo resultado:

flights %>% mutate(Pct_distance=100*(distance/sum(distance,na.rm=TRUE)))

Note que o sum() aqui está somando a distância de todas as observações. Claro que cada voo é uma porcentagem pequena do total na última coluna.

Se quisermos a porcentagem da distância de cada voo no total de cada mês, temos que limitar o escopo de sum() apenas para as observações de um mesmo mês. O group_by() facilita isso:

flights %>% group_by(month) %>% 
  mutate(Pct_distance_por_mes=100*(distance/sum(distance,na.rm=TRUE)))

Como interpretamos este código? Pegue o banco de dados flights, divida ele em grupos, um para cada mês (group_by(month)), calcule a distância total voada em cada mês (sum(distance,na.rm=TRUE)), divida cada distância individual pelo total do mês apropriado (em que ele voou), multiplique ele por 100, e salve o resultado na coluna Pct_distance_por_mes.

Agora a última coluna reflete a porcentagem de distância de cada voo no total de milhas de voos no mesmo mês. Podemos ser mais específico ainda, limitando o denominador e aumentado a porcentagem resultante:

flights %>% group_by(month, day, hour, origin) %>% 
  mutate(Pct_distance_por_mes_hora_origem=100*(distance/sum(distance,na.rm=TRUE)))

A variável nova agora mede: entre todos os voos que decolaram no mesmo mês, mesmo dia e mesma hora, no mesmo aeroporto, qual porcentagem da distância voada contribuiu este voo específico?

Finalmente, é comum calcular a porcentagem do número de observações (linhas) em um grupo comparado com o total. Neste caso, calculamos a porcentagem não baseada em uma variável, mas baseada no número de linhas. O fluxo de trabalho recomendado é:

flights %>% group_by(origin) %>% 
  tally() %>%
  mutate(Pct_por_aeroporto=100*(n/sum(n)))

Se quiser calcular a porcentagem de voos por mês em cada aeroporto separado, podemos usar dois processos de agrupamento, primeiro para calcular o número de observações por aeroporto-mês, e segundo para definir o denominador como o aeroporto para o cálculo de porcentagem:

flights %>% group_by(origin, month) %>% 
  tally() %>%
  group_by(origin) %>% 
  mutate(Pct_por_mes_no_aeroporto=100*(n/sum(n)))

Você consegue descrever o que o código acima fez, passo-a-passo?

É importante entender que as porcentagens acima são diferentes do que calculamos se trocamos origin por month no segundo agrupamento:

flights %>% group_by(origin, month) %>% 
  tally() %>%
  group_by(month) %>% 
  mutate(Pct_por_mes_no_aeroporto=100*(n/sum(n)))

Agora, a porcentagem representa quanto cada aeroporto contribuiu para o número de voos em cada mês, então os valores são mais próximos a um terço cada um.

Habilidade Básica de Programação: Filtros Avançados (%in%)

Filtros são úteis para limitar as nossas operações, mas às vezes é demorado construir condições complexas. Por exemplo, se quisermos calcular a porcentagem da distância de cada voo na distância total de todos os voos com destino de “ILM”, “ACK”, “GRR” ou “PSP”

flights %>% filter(dest=="ILM"|dest=="ACK"|dest=="GRR"|dest=="PSP") %>%
  mutate(Pct_distance=100*(distance/sum(distance,na.rm=TRUE)))

Que chato repetir dest== cada vez… É necessário porque podemos combinar condições de várias variáveis e precisamos ser explícito com R qual variável queremos comparar cada vez. Mas existe uma alternativa: criamos um vetor de todas as opções com c() e pedimos o R filtrar a variável para qualquer elemento do vetor. É equivalente a == para cada elemento, e uma relação de ‘OR’ entre elementos.

flights %>% filter(dest %in% c("ILM", "ACK", "GRR", "PSP")) %>%
  mutate(Pct_distance=100*(distance/sum(distance,na.rm=TRUE)))

Resumos de Múltiplas Colunas (across())

Uma limitação de summarize() é que temos que pedir a média de cada variável separadamente. Se tivermos dezenas de variáveis, isso exige muito código. Não há jeito de calcular a média de diversas variáveis? Há sim! Podemos usar uma função de utilidade, across(), dentro de summarize().

O sintaxe de across() é um pouco diferente: nos parênteses, especificamos o nome da função/estatística sem parênteses (mean), e depois da vírgula qualquer outro argumento à função mean (aqui na.rm=TRUE):

flights %>% summarize(across(c(dep_time, dep_delay), 
                             mean, 
                             na.rm=TRUE))

Assim calculamos a média das duas variáves dep_time e dep_delay, ou quaisquer variáveis que quisemos.

Também é possível pedir múltiplos resumos do mesmo conjunto de variáveis, inserindo as funções de resumo dentro de list():

flights %>% summarize(across(c(dep_time, dep_delay), 
                             list(mean, 
                                  median), 
                             na.rm=TRUE))

O resultado contêm quatro colunas - as colunas que terminam em _1 se referem à primeira função mean e _2 à median. Para deixar mais claro, basta ‘nomear’ as funções na lista antes do sinal de igual:

flights %>% summarize(across(c(dep_time, dep_delay), 
                             list(media=mean, 
                                  mediana=median), 
                             na.rm=TRUE))

E se quisermos um resumo de todas as colunas? Existe mais uma função de utilidade que podemos inserir dentro de across(); everything(), que, naturalmente, aplica o resumo a todas as colunas.

flights %>% summarize(across(everything(), 
                             mean, 
                             na.rm=TRUE))

Salvamos 18 linhas de código comparado com o uso de summarize para cada variável individualmente!

Note que mean apenas funciona para variáveis numéricas; ele devolve NA para variáveis do tipo caractere ou factor.

Transformações de Múltiplas Colunas (across())

Podemos tentar o mesmo com mutate para mantermos a unidade de análise do banco de dados original, mas transformar cada variável com a mesma transformação. Por exemplo, na estatística frequentemente queremos padronizar cada variável, subtraindo a média e dividindo pelo desvio padrão. Esta função simples já existe, se chama scale. Vamos aplicar a todas as colunas do banco de dados flights:

flights %>% mutate(across(everything(), 
                          scale))

Não funcionou. Qual foi o erro? Aqui, a função scale apenas funciona com variáveis numéricas, e across() não é suficientemente flexível para pular as colunas que contém caracteres infelizmente. Agora temos duas alternativas. Uma é usar across() para selecionar as colunas numéricas manualmente antes de rodar mutate():

flights %>%
    mutate(across(c(year, month, day, dep_time, sched_dep_time, dep_delay, 
        arr_time, sched_arr_time, arr_delay, flight, air_time, distance, 
        hour, minute), scale))

Deu certo, mas um pouco chato para digitar. A segunda opção usa uma outra variedade de across() que permite especificar o tipo das variáveis. Combinamos três funções de utilidade, across(), where() e is.numeric: across() significa que queremos selectionar múltiplas colunas; where() significa que queremos selecionar um sub-conjunto de colunas de acordo com um critério, e is.numeric é o critério para identificar apenas as colunas númericas.

flights %>%
    mutate(across(where(is.numeric), scale))

Veja a beleza de programação mais eficiente - o mesmo resultado com menos digitação! Como interpretamos o código? “Pegue o banco de dados de flights, depois aplica uma transformação às variáveis onde a variável is.numeric (é númerica), e a transformação desejada é scale.”

Exercício 3: Resumos Avançados

  1. Calcule o total de distância dos voos que decolaram de cada aeroporto e inserir os cálculos na tabela original de flights como uma coluna nova.
Mostrar Código
flights %>%
    group_by(origin) %>%
    mutate(dist_total = sum(distance, na.rm = T))
  1. Calcule a média do atraso médio de cada aeroporto de origem em cada mês
Mostrar Código
flights %>%
    group_by(origin, month) %>%
    summarize(media_dep_delay = mean(dep_delay, na.rm = T)) %>%
    ungroup() %>%
    summarize(media_media_dep_delay = mean(media_dep_delay, na.rm = T))
  1. Qual a porcentagem dos voos em cada destino? Qual destino é o mais comum?
Mostrar Código
flights %>%
    group_by(dest) %>%
    tally() %>%
    mutate(Pct_dest = 100 * (n/sum(n))) %>%
    arrange(-Pct_dest)
  1. Qual a porcentagem do tempo de atraso por companhia aérea em cada aeroporto? Qual é a companhia aérea responsável pelo maior tempo atrasado no aeroporto de Newark (EWR)?
Mostrar Código
flights %>%
    group_by(origin, carrier) %>%
    summarize(total_atraso = sum(dep_delay, na.rm = T)) %>%
    group_by(origin) %>%
    mutate(Pct_total_atraso = 100 * (total_atraso/sum(total_atraso, na.rm = T))) %>%
    arrange(origin, -Pct_total_atraso)
  1. Transforme a ordem de grandeza de todas as variáveis dep_delay, arr_delay e air_time de minutos para horas. Escreva uma função nova para facilitar esta transformação em conjunto com across().
Mostrar Código
mins_to_horas <- function(x) {
    return(x/60)
}

flights %>%
    mutate(across(c(dep_delay, arr_delay, air_time), mins_to_horas))


Veja um resumo das funções principais até o final deste tutorial aqui