Bump Plots com ggplot2

Criando Visualizações Profissionais de Rankings

Aprenda a criar bump plots para visualizar rankings e comparações ao longo do tempo usando ggplot2 e o pacote ggbump.
visualizacao-dados
ggplot2
rankings
Autor

Equipe EKIO Academy

Data de Publicação

15 de janeiro de 2024

Introdução aos Bump Plots

Um bump plot mostra diferentes valores de uma variável em contextos distintos. É similar a um gráfico de tendências paralelas mas com linhas mais suaves. Este tutorial demonstra como criar bump plots profissionais no R usando ggplot2 e o pacote especializado ggbump.

Os bump plots são particularmente úteis para visualizar rankings e comparações. Eles ajudam os visualizadores a acompanhar como diferentes entidades (países, empresas, times) mudam de posição em relação umas às outras através de diferentes métricas ou períodos de tempo.

Setup

Usaremos o pacote especializado {ggbump} desenvolvido especificamente para criar bump charts. O pacote está disponível tanto no CRAN quanto no GitHub.

Neste tipo de gráfico, queremos comparar o valor de uma variável em diferentes contextos. Podemos ter uma comparação entre os mesmos grupos ao longo do tempo ou os mesmos grupos ao longo de variáveis distintas. Em geral, estes gráficos são organizados em forma de rankings e têm como objetivo facilitar a comparação entre grupos.

Aplicações Comuns

  • Medalhas de ouro nas olimpíadas por país ao longo dos anos
  • Gêneros musicais mais populares por usuário ao longo do tempo
  • Rankings populacionais de países ao longo de décadas
  • Rankings imobiliários através de diferentes critérios
  • Rankings de times em ligas esportivas semana a semana
  • Rankings de riqueza de países usando diferentes métricas

Pacotes Necessários

library(ggplot2)
library(ggbump)
library(dplyr)
library(tidyr)

Exemplo Básico

Este primeiro exemplo é emprestado da documentação do pacote e ilustra o básico da função geom_bump. Os dados devem estar em formato ‘tidy’ (longitudinal) onde as posições dos pontos são especificadas pelos argumentos x e y e os grupos identificados via estética group.

year <- rep(2019:2021, 4)
position <- c(4, 2, 2, 3, 1, 4, 2, 3, 1, 1, 4, 3)
player <- c("A", "A", "A",
            "B", "B", "B",
            "C", "C", "C",
            "D", "D", "D")

df <- data.frame(x = year,
                 y = position,
                 group = player)
x y group
2019 4 A
2020 2 A
2021 2 A
2019 3 B
2020 1 B
2021 4 B
2019 2 C
2020 3 C
2021 1 C
2019 1 D
2020 4 D
2021 3 D

Vale tirar um tempo para comparar, com calma, as entradas na tabela acima e o resultado no gráfico abaixo:

ggplot(df, aes(year, position, color = player)) +
  geom_bump()

Exemplo: Venda de Imóveis no Texas

Podemos analisar as cidades com maior número de vendas ao longo dos anos usando a já conhecida txhousing. Vamos criar um gráfico que mostra o ranking de vendas de imóveis ao longo dos anos.

Removo o ano de 2015, pois este ano não está completo na amostra. Como há mais de quarenta cidades na amostra, crio uma subamostra que contém apenas as cidades com maior número de vendas em 2014.

# Encontra as top-15 cidades com maior número de vendas em 2014
top_cities <- txhousing |>
  # Seleciona apenas o ano de 2014
  filter(year == 2014) |>
  # Calcula o total de vendas em cada cidade
  summarise(total = sum(listings, na.rm = TRUE), .by = "city") |>
  # Seleciona o top-15
  slice_max(total, n = 15) |>
  pull(city)

# Calcula o total de vendas anuais na subamostra de cidades e faz o ranking anual
rank_housing <- txhousing |>
  # Seleciona apenas cidades dentro da subamostra
  filter(city %in% top_cities, year > 2005, year < 2015) |>
  # Calcula o total de vendas a cada ano
  summarise(
    listing_year = sum(listings, na.rm = TRUE),
    .by = c("city", "year")
    ) |>
  # Faz o ranking das cidades dentro de cada ano
  mutate(rank = rank(-listing_year, "first"), .by = "year")

No gráfico abaixo, cada cidade tem uma cor diferente, mas omito a legenda de cores. Note o uso de scale_y_reverse já que, tipicamente, queremos mostrar os menores valores na parte superior do gráfico:

ggplot(rank_housing, aes(year, rank, group = city, color = city)) +
  geom_bump() +
  scale_y_reverse() +
  guides(color = "none")

Para melhorar a legibilidade do gráfico podemos colocar o nome das cidades ao lado das linhas usando geom_text:

ggplot() +
  geom_bump(
    data = rank_housing,
    aes(year, rank, group = city, color = city)
    ) +
  geom_text(
    data = filter(rank_housing, year == max(year)),
    aes(year, rank, label = city),
    nudge_x = 0.1,
    hjust = 0
  ) +
  scale_y_reverse() +
  scale_x_continuous(limits = c(NA, 2017)) +
  guides(color = "none")

Mesmo com o nome das cidades, há muitas linhas para acompanhar no gráfico. Vamos destacar apenas algumas cidades. Em vez do top 4 (Houston, Dallas, San Antonio e Austin), que praticamente não se alternam no ranking, vamos destacar Bay Area, El Paso, Corpus Christi e Tyler.

sel_cities <- c("Bay Area", "El Paso", "Corpus Christi", "Tyler")

rank_housing <- rank_housing |>
  mutate(
    highlight = if_else(city %in% sel_cities, city, ""),
    is_highlight = factor(if_else(city %in% sel_cities, 1L, 0L))
  )

O gráfico abaixo exige um código consideravelmente mais longo, mas melhora o gráfico original em vários aspectos. Agora temos um maior destaque para as cidades de interesse, eixos melhor definidos e linhas de fundo redundantes foram removidas:

Código
ggplot() +
  # Linhas em cinza (sem destaque)
  geom_bump(
    data = filter(rank_housing, is_highlight == 0),
    aes(year, rank, group = city, color = highlight),
    linewidth = 0.8,
    smooth = 8
    ) +
  # Linhas coloridas (com destaque)
  geom_bump(
    data = filter(rank_housing, is_highlight == 1),
    aes(year, rank, group = city, color = highlight),
    linewidth = 2,
    smooth = 8
  ) +
  # Pontos
  geom_point(
    data = rank_housing,
    aes(year, rank, color = highlight),
    size = 4
  ) +
  # Nomes sem destaque
  geom_text(
    data = filter(rank_housing, year == max(year), !(city %in% sel_cities)),
    aes(year, rank, label = city),
    nudge_x = 0.2,
    hjust = 0,
    color = "gray20"
  ) +
  # Nome com destaque (em negrito)
  geom_text(
    data = filter(rank_housing, year == max(year), city %in% sel_cities),
    aes(year, rank, label = city),
    nudge_x = 0.2,
    hjust = 0,
    fontface = "bold"
  ) +
  # Adiciona os eixos para melhorar leitura do gráfico
  scale_y_reverse(breaks = 1:15) +
  scale_x_continuous(limits = c(NA, 2017), breaks = 2006:2014) +
  # Cores
  scale_color_manual(
    values = c("gray70", "#2f4858", "#86bbd8", "#f6ae2d", "#f26419")
  ) +
  # Elementos temáticos
  labs(x = NULL, y = NULL) +
  theme_minimal() +
  theme(
    panel.grid = element_blank(),
    legend.position = "none"
  )

Replicando The Economist

Como exercício final vamos replicar um gráfico da revista The Economist. O artigo original discute diferentes maneiras de mensurar e comparar a riqueza entre países.

O gráfico mostra um ranking dos países mais ricos do mundo segundo três métricas:

  1. PIB per capita a preços correntes - a medida mais básica de riqueza
  2. PIB per capita em PPC (paridade do poder de compra) - ajustado pelo custo de vida
  3. PIB per capita em PPC ajustado por horas trabalhadas - mostra quão produtivos são os países por hora

Gráfico original do The Economist

Note como esta medida eleva consideravelmente a posição de países europeus como Bélgica, Alemanha, Áustria e Dinamarca, enquanto derruba alguns países como EUA e Singapura.

Os dados originais estão disponíveis no GitHub do The Economist, mas não consegui encontrar o código específico para este gráfico. Vou utilizar a fonte Lato do Google Fonts em vez da fonte proprietária do The Economist.

library(showtext)

font_add_google("Lato", "Lato")
showtext_auto()

Preparação dos Dados

Boa parte da construção da visualização acima está na manipulação dos dados. Vamos fazer um passo-a-passo.

Primeiro defino alguns objetos úteis como o nome dos países que serão destacados e o nome das colunas que contém as variáveis de PIB:

countries_sel <- c("Norway", "Belgium", "Austria", "United States", "Germany")

measures <- c("gdp_over_pop", "gdp_ppp_over_pop", "gdp_ppp_over_k_hours_worked")

sub <- dat |>
  select(country, year, all_of(measures)) |>
  na.omit()

A transformação essencial é converter os dados em formato tidy e ranquear as observações dentro de cada métrica de PIB:

ranking <- sub |>
  filter(year == max(year)) |>
  pivot_longer(cols = -c(country, year), names_to = "measure") |>
  mutate(rank = rank(-value), .by = "measure")

Agora, mais por conveniência, eu crio algumas variáveis auxiliares que serão úteis para mapear os diferentes elementos estéticos:

ranking <- ranking |>
  mutate(
    highlight = if_else(country %in% countries_sel, country, ""),
    highlight = factor(highlight, levels = c(countries_sel, "")),
    is_highlight = factor(if_else(country %in% countries_sel, 1L, 0L)),
    rank_labels = if_else(rank %in% c(1, 5, 10, 15, 20), rank, NA),
    rank_labels = stringr::str_replace(rank_labels, "^1$", "1st"),
    measure = factor(measure, levels = measures)
    )

Por fim, eu defino as cores das linhas e crio uma tabela auxiliar que contém apenas o texto que vai em cima do gráfico:

cores <- c("#101010", "#f7443e", "#8db0cc", "#fa9494", "#225d9f", "#c7c7c7")

df_gdp <- tibble(
  measure = measures,
  measure_label = c(
    "PIB per capita a preços de mercado",
    "Ajustado por diferenças de custo*",
    "Ajustado por custos e horas trabalhadas"
  ),
  position = -1.2
)

df_gdp <- df_gdp |>
  mutate(
    measure = factor(measure, levels = measures),
    measure_label = stringr::str_wrap(measure_label, width = 12),
    measure_label = paste0("  ", measure_label)
    )

A versão simplificada do gráfico está resumida no código abaixo. Vale notar o uso da coord_cartesian para “cortar o gráfico” sem perder informação. Não é muito usual utilizar linewidth como um elemento estético dentro de aes mas pode-se ver como isto é bastante simples:

ggplot(ranking, aes(measure, rank, group = country)) +
  geom_bump(aes(color = highlight, linewidth = is_highlight)) +
  geom_point(shape = 21, color = "white", aes(fill = highlight), size = 3) +
  geom_text(
    data = filter(ranking, measure == measures[[3]]),
    aes(x = measure, y = rank, label = country),
    nudge_x = 0.05,
    hjust = 0,
    family = "Lato"
  ) +
  coord_cartesian(ylim = c(21, -2)) +
  scale_color_manual(values = cores) +
  scale_fill_manual(values = cores) +
  scale_linewidth_manual(values = c(0.5, 1.2))

O código final é bastante extenso, mas o resultado é muito satisfatório:

Código
ggplot(ranking, aes(measure, rank, group = country)) +
  geom_bump(aes(color = highlight, linewidth = is_highlight)) +
  geom_point(shape = 21, color = "white", aes(fill = highlight), size = 3) +
  # Nome dos países sem destaque
  geom_text(
    data = filter(ranking, measure == measures[[3]], is_highlight != 1L),
    aes(x = measure, y = rank, label = country),
    nudge_x = 0.05,
    hjust = 0,
    family = "Lato"
  ) +
  # Nome dos países com destaque (em negrito)
  geom_text(
    data = filter(ranking, measure == measures[[3]], is_highlight == 1L),
    aes(x = measure, y = rank, label = country),
    nudge_x = 0.05,
    hjust = 0,
    family = "Lato",
    fontface = "bold"
  ) +
  # "Eixo" na esquerda (1st, 5, 10, 15, 20)
  geom_text(
    data = filter(ranking, measure == measures[[1]]),
    aes(x = measure, y = rank, label = rank_labels),
    nudge_x = -0.15,
    hjust = 0,
    family = "Lato"
  ) +
  # Texto descritivo acima do gráfico
  geom_text(
    data = df_gdp,
    aes(x = measure, y = position, label = measure_label),
    inherit.aes = FALSE,
    hjust = 0,
    family = "Lato",
    fontface = "bold"
  ) +
  # Posiciona as flechas apontando para baixo
  annotate("text", x = 1, y = -2.2, label = expression("\u2193")) +
  annotate("text", x = 2, y = -2.2, label = expression("\u2193")) +
  annotate("text", x = 3, y = -2.2, label = expression("\u2193")) +
  # Corta o gráfico
  coord_cartesian(ylim = c(21, -2)) +
  # Cores
  scale_color_manual(values = cores) +
  scale_fill_manual(values = cores) +
  # Espessura das linhas
  scale_linewidth_manual(values = c(0.5, 1.2)) +
  # Elementos temáticos
  labs(x = NULL, y = NULL) +
  theme_minimal() +
  theme(
    panel.background = element_rect(fill = "#ffffff", color = NA),
    plot.background = element_rect(fill = "#ffffff", color = NA),
    panel.grid = element_blank(),
    legend.position = "none",
    axis.text = element_blank()
  )

Resumo

Os bump plots são excelentes para visualizar rankings e comparações em diferentes contextos. Pontos principais:

  • Use o pacote ggbump para bump charts suaves e profissionais
  • Dados devem estar em formato tidy/longitudinal
  • scale_y_reverse() é útil para exibições típicas de ranking
  • Destacar grupos específicos melhora a legibilidade em gráficos complexos
  • Considere usar coord_cartesian() para focar em ranges relevantes

O objetivo destes posts é de sempre fazer o máximo possível usando ggplot2 mas, na prática, as caixas de texto acima do gráfico podem ser feitas num software externo para designs mais complexos.


Pronto para criar visualizações mais avançadas? Confira nosso tutorial de Dashboards Interativos ou explore nosso guia completo de ggplot2.