EAE1106 - Métodos Computacionais para Economia
Faculdade de Economia, Administração, Contabilidade e Atuária
Universidade de São Paulo

Prof. Arthur Viaro
1º semestre de 2026


Aula 26 — Visualização de Dados com ggplot2

A Gramática dos Gráficos

O ggplot2 é baseado na ideia de gramática dos gráficos: assim como uma frase em português é construída com sujeito + verbo + objeto, um gráfico é construído com dados + mapeamentos + geometria.

Isso significa que você descreve o gráfico que quer ver, em vez de desenhá-lo passo a passo. Na prática:

O que você quer mostrar? Componente no ggplot2
De onde vêm os dados? ggplot(data = ...)
Quais variáveis nos eixos? aes(x = ..., y = ...)
Como representar? geom_point(), geom_line(), geom_col()
Cor, tamanho, forma? aes(color = ..., size = ...) ou argumentos fixos
Subgráficos? facet_wrap() / facet_grid()
Aparência geral? theme_minimal(), theme_bw()
Títulos e rótulos? labs(...)
library(tidyverse)
library(palmerpenguins) # Dataset de pinguins da Antártica

glimpse(penguins)
## Rows: 344
## Columns: 8
## $ species           <fct> Adelie, Adelie, Adelie, Adelie, Adelie, Adelie, Adel…
## $ island            <fct> Torgersen, Torgersen, Torgersen, Torgersen, Torgerse…
## $ bill_length_mm    <dbl> 39.1, 39.5, 40.3, NA, 36.7, 39.3, 38.9, 39.2, 34.1, …
## $ bill_depth_mm     <dbl> 18.7, 17.4, 18.0, NA, 19.3, 20.6, 17.8, 19.6, 18.1, …
## $ flipper_length_mm <int> 181, 186, 195, NA, 193, 190, 181, 195, 193, 190, 186…
## $ body_mass_g       <int> 3750, 3800, 3250, NA, 3450, 3650, 3625, 4675, 3475, …
## $ sex               <fct> male, female, female, NA, female, male, female, male…
## $ year              <int> 2007, 2007, 2007, 2007, 2007, 2007, 2007, 2007, 2007…

Estrutura Básica

Todo gráfico ggplot2 tem a mesma estrutura: uma chamada ggplot() seguida de camadas adicionadas com +.

# Passo 1 — só definir dados e eixos: área em branco com escala correta
ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g))

# Passo 2 — adicionar uma geometria: os dados ganham forma
ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g)) +
  geom_point()

# Passo 3 — mapear cor para espécie: grupos visualmente distintos
ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g, color = species)) +
  geom_point()

# Passo 4 — adicionar rótulos e tema: gráfico pronto para publicação
ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g, color = species)) +
  geom_point(alpha = 0.7) +
  labs(
    title   = "Comprimento da nadadeira vs. massa corporal",
    x       = "Comprimento da nadadeira (mm)",
    y       = "Massa corporal (g)",
    color   = "Espécie",
    caption = "Fonte: Gorman et al. (2014)"
  ) +
  theme_minimal()

Esses quatro passos — dados, eixos, geometria, acabamento — são o ciclo de trabalho de qualquer gráfico ggplot2.


Mapeamentos Estéticos (aes)

As estéticas são o canal de comunicação entre variáveis nos dados e propriedades visuais do gráfico.

Estética O que controla
x, y Posição nos eixos
color Cor de pontos, linhas e contornos
fill Cor de preenchimento (barras, áreas)
size Tamanho de pontos e espessura de linhas
shape Forma dos pontos
alpha Transparência (0 = invisível, 1 = opaco)
linetype Tipo de linha (sólida, tracejada, etc.)

Estética mapeada vs. fixa

Esta é uma das confusões mais comuns no ggplot2:

  • Dentro de aes(): a estética varia conforme os dados (mapeia uma variável)
  • Fora de aes(): a estética é fixa, igual para todos os elementos
# MAPEADA: cada espécie recebe uma cor diferente automaticamente
ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g, color = species)) +
  geom_point()

# FIXA: todos os pontos têm a mesma cor
ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g)) +
  geom_point(color = "steelblue", size = 2.5, alpha = 0.6)

Estética global vs. local

Estéticas definidas em ggplot() são herdadas por todos os geoms. Estéticas definidas dentro de um geom se aplicam apenas a ele.

# GLOBAL: a cor por espécie aparece tanto nos pontos quanto nas linhas
ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g, color = species)) +
  geom_point() +
  geom_smooth(method = "lm", se = FALSE)

# LOCAL: apenas os pontos têm cor por espécie; a linha é única para todos
ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g)) +
  geom_point(aes(color = species)) +
  geom_smooth(method = "lm", se = FALSE, color = "gray30")

Qual usar? Se a estética faz sentido para todos os geoms, defina globalmente. Se for específica de um geom, defina localmente.


Geometrias (geoms)

geom_point() — Dispersão

Ideal para visualizar relações entre duas variáveis numéricas.

# Linha de tendência por grupo com geom_smooth
ggplot(penguins, aes(x = bill_length_mm, y = bill_depth_mm, color = species)) +
  geom_point(alpha = 0.7) +
  geom_smooth(method = "lm", se = FALSE) +
  labs(
    title = "Comprimento vs. profundidade do bico por espécie",
    x = "Comprimento do bico (mm)", y = "Profundidade do bico (mm)", color = "Espécie"
  ) +
  theme_minimal()

# O Paradoxo de Simpson — um exemplo clássico de visualização enganosa
# A correlação agregada (linha preta) vai em direção oposta à de cada grupo
ggplot(penguins, aes(x = bill_length_mm, y = bill_depth_mm)) +
  geom_point(aes(color = species), alpha = 0.6) +
  geom_smooth(method = "lm", color = "black", se = TRUE, linewidth = 1.2) +
  geom_smooth(aes(color = species), method = "lm", se = FALSE, linewidth = 0.8) +
  labs(
    title = "Paradoxo de Simpson",
    subtitle = "A correlação agregada (preta) é negativa — o oposto da correlação dentro de cada espécie",
    x = "Comprimento do bico (mm)", y = "Profundidade do bico (mm)", color = "Espécie"
  ) +
  theme_minimal()

geom_line() — Séries Temporais

Ideal para dados com ordem no eixo x, especialmente séries temporais.

# PIB do Brasil em trilhões de USD (2000–2023)
pib_brasil <- tibble(
  ano = 2000:2023,
  pib = c(
    1.20, 1.32, 1.49, 1.58, 1.67, 1.89, 2.09, 2.19, 2.28, 2.02,
    2.21, 2.61, 2.46, 2.47, 2.46, 1.80, 1.80, 2.06, 1.89, 1.87,
    1.44, 1.61, 1.92, 2.08
  )
)

ggplot(pib_brasil, aes(x = ano, y = pib)) +
  geom_line(color = "#1a6eb5", linewidth = 1.1) +
  geom_point(color = "#1a6eb5", size = 2) +
  scale_x_continuous(breaks = seq(2000, 2023, 4)) +
  scale_y_continuous(
    name = "Trilhões de USD (nominal)", # Nome do eixo
    breaks = seq(1, 3, by = 0.5), # Linhas de grade
    limits = c(1, 3), # Valores mínimo e máximo do eixo
    labels = scales::label_dollar() # Transforma "1.5" em "$1.5"
  ) +
  labs(
    title = "PIB do Brasil (2000–2023)",
    x = NULL,
    caption = "Fonte: Banco Mundial"
  ) +
  theme_minimal()

# Múltiplas séries: note o uso de group para conectar pontos do mesmo país
pib_latam <- tibble(
  ano = rep(2015:2022, 3),
  pais = rep(c("Brasil", "México", "Colômbia"), each = 8),
  pib = c(
    1.80, 1.80, 2.06, 1.89, 1.87, 1.44, 1.61, 1.92,
    1.17, 1.08, 1.16, 1.15, 1.32, 1.08, 1.28, 1.30,
    0.29, 0.28, 0.31, 0.33, 0.33, 0.27, 0.31, 0.34
  )
)

ggplot(pib_latam, aes(x = ano, y = pib, color = pais, group = pais)) +
  geom_line(linewidth = 1.1) +
  geom_point(size = 2.5) +
  scale_color_manual(values = c("#125689", "#9E0000", "#F4A261")) +
  labs(
    title = "PIB nominal — Brasil, México e Colômbia (2015–2022)",
    x = NULL, y = "Trilhões de USD", color = NULL
  ) +
  theme_minimal() +
  theme(legend.position = "bottom")

geom_col() e geom_bar() — Barras

  • geom_bar(): conta as observações automaticamente (sem y)
  • geom_col(): usa valores pré-calculados (precisa de y)
# geom_bar: conta por espécie automaticamente
ggplot(penguins, aes(x = species, fill = species)) +
  geom_bar(show.legend = FALSE) +
  scale_fill_brewer(palette = "Set2") +
  labs(title = "Número de pinguins por espécie", x = NULL, y = "Contagem") +
  theme_minimal()

# geom_col com reorder: ordenar barras por valor (muito mais legível!)
resumo_pib <- tibble(
  pais = c("Brasil", "México", "Argentina", "Colômbia", "Chile", "Peru"),
  pib  = c(2.08, 1.30, 0.63, 0.34, 0.30, 0.24)
)

ggplot(resumo_pib, aes(x = pib, y = reorder(pais, pib))) +
  geom_col(fill = "#2c7bb6", width = 0.7) +
  geom_text(
    aes(label = scales::label_dollar()(pib)),
    hjust = -0.1,
    size = 3.5
  ) +
  scale_x_continuous(
    expand = expansion(mult = c(0, 0.15)), # Evita que as barras encostem na borda e deixa espaço para os rótulos
    labels = scales::label_dollar() # Adiciona $ no eixo horizontal
  ) +
  labs(
    title = "PIB nominal — América do Sul e México (2023)",
    x = "Trilhões de USD", y = NULL
  ) +
  theme_minimal()

# Barras empilhadas e normalizadas para comparar proporções
ggplot(penguins, aes(x = species, fill = sex)) +
  geom_bar(position = "fill") + # Transforma contagens em proporções
  scale_y_continuous(labels = scales::percent) +
  scale_fill_brewer(palette = "Set1", na.value = "gray70") +
  labs(title = "Proporção de sexos por espécie", x = NULL, y = NULL, fill = "Sexo") +
  theme_minimal()

geom_histogram() — Distribuição de Uma Variável

# O parâmetro bins (ou binwidth) afeta muito a interpretação — teste diferentes valores
ggplot(penguins, aes(x = body_mass_g, fill = species)) +
  geom_histogram(
    binwidth = 250,
    alpha = 0.7,
    position = "identity", # sobreposição de barras para comparar distribuições
    color = "white"
  ) +
  scale_fill_brewer(palette = "Set2") +
  labs(
    title = "Distribuição da massa corporal por espécie",
    x = "Massa corporal (g)",
    y = "Contagem",
    fill = "Espécie"
  ) +
  theme_minimal()

geom_boxplot() e geom_violin() — Comparando Distribuições

O boxplot resume a distribuição em 5 números (mínimo, Q1, mediana, Q3, máximo). O violin mostra a distribuição completa.

# Boxplot + pontos reais: você vê o resumo E os dados individuais
ggplot(penguins, aes(x = species, y = body_mass_g, fill = species)) +
  geom_boxplot(alpha = 0.5, outlier.shape = NA, show.legend = FALSE) +
  geom_jitter(
    aes(color = species),
    width = 0.15,
    alpha = 0.4,
    size = 1.5,
    show.legend = FALSE
  ) +
  scale_fill_brewer(palette = "Set2") +
  scale_color_brewer(palette = "Set2") +
  labs(
    title = "Massa corporal por espécie",
    x = NULL,
    y = "Massa corporal (g)"
  ) +
  theme_minimal()

# Violin + boxplot: a combinação mais informativa
ggplot(penguins, aes(x = species, y = flipper_length_mm, fill = species)) +
  geom_violin(alpha = 0.4, show.legend = FALSE) +
  geom_boxplot(width = 0.15, outlier.shape = NA, show.legend = FALSE) +
  scale_fill_brewer(palette = "Set2") +
  labs(
    title = "Comprimento da nadadeira por espécie",
    subtitle = "Violin mostra a distribuição completa; boxplot resume as estatísticas",
    x = NULL, y = "Comprimento da nadadeira (mm)"
  ) +
  theme_minimal()

geom_density() — Curva de Densidade

Alternativa mais suave ao histograma. Útil quando há sobreposição de grupos.

ggplot(penguins, aes(x = body_mass_g, fill = species)) +
  geom_density(alpha = 0.45) +
  scale_fill_brewer(palette = "Set2") +
  labs(
    title = "Distribuição de massa corporal por espécie",
    x = "Massa corporal (g)", y = "Densidade", fill = "Espécie"
  ) +
  theme_minimal()

geom_smooth() — Linha de Tendência

# method = "lm": regressão linear com intervalo de confiança
ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g)) +
  geom_point(alpha = 0.4) +
  geom_smooth(method = "lm", color = "#e74c3c", linewidth = 1.2) +
  labs(
    title = "Comprimento da nadadeira vs. massa corporal",
    subtitle = "Linha de regressão linear com intervalo de confiança de 95%",
    x = "Comprimento da nadadeira (mm)", y = "Massa corporal (g)"
  ) +
  theme_minimal()


Múltiplas Camadas

A força do ggplot2 está em sobrepor camadas. Cada geom_*() é uma camada independente que compartilha o mesmo sistema de coordenadas.

# Série temporal com anotações — cada elemento é uma camada
library(nycflights13)

flights |>
  filter(!is.na(arr_delay)) |>
  group_by(month) |>
  summarize(atraso_med = mean(arr_delay), n = n()) |>
  ggplot(aes(x = month, y = atraso_med)) +
  # Camada 1: área sombreada
  geom_area(fill = "#74b9ff", alpha = 0.3) +
  # Camada 2: linha principal
  geom_line(color = "#0984e3", linewidth = 1.2) +
  # Camada 3: pontos (tamanho proporcional ao volume de voos)
  geom_point(aes(size = n), color = "#0984e3", alpha = 0.8) +
  # Camada 4: linha de referência no zero
  geom_hline(yintercept = 0, linetype = "dashed", color = "gray40") +
  # Camada 5: rótulos de valor
  geom_text(aes(label = round(atraso_med, 1)), vjust = -1.2, size = 3) +
  scale_x_continuous(breaks = 1:12, labels = month.abb) +
  labs(
    title = "Atraso médio de chegada por mês — NYC (2013)",
    x = NULL, y = "Minutos", size = "Nº de voos"
  ) +
  theme_minimal()


Facetamento

Facetamento cria pequenos múltiplos — o mesmo gráfico repetido para subconjuntos dos dados. É uma das formas mais eficazes de mostrar padrões em múltiplos grupos.

# facet_wrap: grade automática com uma variável
ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g, color = sex)) +
  geom_point(alpha = 0.6) +
  geom_smooth(method = "lm", se = FALSE) +
  facet_wrap(~species) +
  scale_color_brewer(palette = "Set1", na.value = "gray60") +
  labs(
    title = "Massa corporal vs. nadadeira por espécie e sexo",
    x = "Comprimento da nadadeira (mm)", y = "Massa corporal (g)", color = "Sexo"
  ) +
  theme_minimal()

# facet_grid: grade com duas variáveis (linhas ~ colunas)
ggplot(penguins, aes(x = bill_length_mm, y = bill_depth_mm)) +
  geom_point(alpha = 0.5, color = "steelblue") +
  facet_grid(sex ~ species) +
  labs(
    title = "Medidas do bico por espécie (colunas) e sexo (linhas)",
    x = "Comprimento do bico (mm)", y = "Profundidade do bico (mm)"
  ) +
  theme_minimal()

# scales = "free": cada painel tem sua própria escala — use com cuidado!
ggplot(pib_latam, aes(x = ano, y = pib)) +
  geom_line(color = "steelblue", linewidth = 1) +
  geom_point(color = "steelblue") +
  facet_wrap(~pais, scales = "free_y") +
  labs(
    title = "PIB por país (escalas independentes)",
    subtitle = "Atenção: eixos y diferentes dificultam comparação direta entre países",
    x = NULL, y = "Trilhões de USD"
  ) +
  theme_minimal()


Escalas

As escalas controlam como os dados são mapeados para propriedades visuais — e como os eixos e legendas são formatados.

# scale_color_manual: cores definidas por você
ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g, color = species)) +
  geom_point(alpha = 0.7) +
  scale_color_manual(values = c(
    "Adelie"    = "#e74c3c",
    "Chinstrap" = "#9b59b6",
    "Gentoo"    = "#27ae60"
  )) +
  labs(x = "Nadadeira (mm)", y = "Massa (g)", color = "Espécie") +
  theme_minimal()

# scale_color_brewer: paletas testadas e acessíveis a daltônicos
ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g, color = species)) +
  geom_point(alpha = 0.7) +
  scale_color_brewer(palette = "Dark2") +
  labs(x = "Nadadeira (mm)", y = "Massa (g)", color = "Espécie") +
  theme_minimal()

# scale_color_viridis_c: para variáveis contínuas (boa para impressão)
ggplot(penguins, aes(x = bill_length_mm, y = bill_depth_mm, color = body_mass_g)) +
  geom_point(size = 2.5, alpha = 0.8) +
  scale_color_viridis_c(option = "plasma", name = "Massa (g)") +
  labs(
    title = "Medidas do bico coloridas por massa corporal",
    x = "Comprimento do bico (mm)", y = "Profundidade do bico (mm)"
  ) +
  theme_minimal()

# Controle de eixos: breaks, labels, limites
ggplot(pib_brasil, aes(x = ano, y = pib)) +
  geom_line(color = "steelblue", linewidth = 1) +
  scale_x_continuous(breaks = seq(2000, 2023, by = 4)) +
  scale_y_continuous(
    labels = scales::label_dollar(accuracy = 0.01),
    limits = c(0, NA) # Forçar zero no eixo y
  ) +
  labs(title = "PIB do Brasil (2000–2023)", x = NULL, y = NULL) +
  theme_minimal()

# Escala logarítmica — essencial quando os dados cobrem várias ordens de grandeza
top_paises <- tibble(
  pais = c("EUA", "China", "Alemanha", "Japão", "Brasil", "Índia"),
  pib  = c(25.46, 17.96, 4.07, 4.23, 2.08, 3.39),
  pop  = c(335, 1412, 84, 125, 215, 1429)
)

ggplot(top_paises, aes(x = pop, y = pib, label = pais)) +
  geom_point(aes(size = pib), color = "steelblue", alpha = 0.7, show.legend = FALSE) +
  geom_text(vjust = -0.9, size = 3.5, fontface = "bold") +
  scale_x_log10(labels = scales::comma) +
  scale_y_log10(labels = scales::label_dollar(accuracy = 0.01)) +
  labs(
    title = "PIB vs. População — escala log-log",
    subtitle = "Na escala log, distâncias iguais representam razões iguais",
    x = "População (milhões, escala log)", y = "PIB (trilhões USD, escala log)"
  ) +
  theme_minimal()


Temas

O tema controla toda a aparência que não é dado: fundo, grade, fontes, margens.

p_base <- ggplot(penguins, aes(x = species, y = body_mass_g, fill = species)) +
  geom_boxplot(show.legend = FALSE) +
  scale_fill_brewer(palette = "Set2") +
  labs(title = "Massa corporal por espécie", x = NULL, y = "Massa corporal (g)")

# Os temas mais usados
p_base + theme_minimal() + labs(subtitle = "theme_minimal()")

p_base + theme_bw() + labs(subtitle = "theme_bw()")

p_base + theme_classic() + labs(subtitle = "theme_classic()")

# Personalizações manuais com theme()
p_base +
  theme_minimal(base_size = 13) +
  theme(
    plot.title       = element_text(face = "bold", size = 15),
    plot.subtitle    = element_text(color = "gray50"),
    axis.text        = element_text(size = 11),
    panel.grid.major = element_line(color = "gray90"),
    panel.grid.minor = element_blank(),
    legend.position  = "bottom"
  )


Rótulos e Anotações

# geom_text e geom_label para anotar pontos
ggplot(top_paises, aes(x = pop, y = pib)) +
  geom_point(aes(size = pib), color = "#2980b9", alpha = 0.7, show.legend = FALSE) +
  geom_label(aes(label = pais),
    vjust = -0.8, size = 3.2, fill = "white",
    label.padding = unit(0.15, "lines")
  ) +
  scale_size_continuous(range = c(3, 12)) +
  labs(
    title = "Maiores economias do mundo (2023)",
    x = "População (milhões)", y = "PIB (trilhões USD)"
  ) +
  theme_minimal()

# annotate() para anotações livres (não amarradas a dados)
ggplot(pib_brasil, aes(x = ano, y = pib)) +
  geom_line(color = "#1a6eb5", linewidth = 1.1) +
  annotate("rect",
    xmin = 2015, xmax = 2016, ymin = -Inf, ymax = Inf,
    fill = "#e74c3c", alpha = 0.15
  ) +
  annotate("text",
    x = 2015.5, y = 2.55,
    label = "Recessão\n2015–16", color = "#e74c3c", size = 3.2, hjust = 0.5
  ) +
  annotate("rect",
    xmin = 2020, xmax = 2020.5, ymin = -Inf, ymax = Inf,
    fill = "#8e44ad", alpha = 0.15
  ) +
  annotate("text",
    x = 2020.25, y = 2.55,
    label = "COVID\n2020", color = "#8e44ad", size = 3.2, hjust = 0.5
  ) +
  scale_x_continuous(breaks = seq(2000, 2023, 4)) +
  labs(
    title = "PIB do Brasil (2000–2023)",
    x = NULL, y = "Trilhões de USD (nominal)"
  ) +
  theme_minimal()


Salvando Gráficos

p_final <- ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g, color = species)) +
  geom_point(alpha = 0.7) +
  geom_smooth(method = "lm", se = FALSE) +
  scale_color_brewer(palette = "Set2") +
  labs(
    title   = "Pinguins das Ilhas Palmer",
    x       = "Comprimento da nadadeira (mm)",
    y       = "Massa corporal (g)",
    color   = "Espécie"
  ) +
  theme_minimal(base_size = 12)

# PNG para apresentações e web (dpi = 300 para impressão)
ggsave("pinguins.png", plot = p_final, width = 8, height = 5, dpi = 300)

# PDF vetorial para artigos e relatórios
ggsave("pinguins.pdf", plot = p_final, width = 8, height = 5)

Comparação com Python

Conceito ggplot2 (R) Python
Filosofia Declarativo (descreva o gráfico) Imperativo (matplotlib) / misto (seaborn)
Dispersão geom_point() sns.scatterplot()
Linha geom_line() sns.lineplot()
Barras geom_col() / geom_bar() sns.barplot()
Histograma geom_histogram() sns.histplot()
Boxplot geom_boxplot() sns.boxplot()
Cor por grupo aes(color = variavel) hue = "variavel" (seaborn)
Subgráficos facet_wrap() / facet_grid() sns.FacetGrid()
Temas theme_minimal() etc. sns.set_theme() / plt.style.use()

Exercícios

1. Usando o dataset penguins, crie um gráfico de dispersão de bill_length_mm vs. bill_depth_mm. Mapeie a cor para species e o tamanho para body_mass_g. Adicione linhas de tendência linear por espécie e títulos em português.

2. Com flights, crie um gráfico de barras horizontais mostrando os 10 destinos mais frequentes. Ordene as barras por frequência com reorder().

3. Crie um histograma do atraso de partida (dep_delay) dos voos com facet_wrap() por origem (EWR, JFK, LGA). Use scales = "free_y" e explique por que as distribuições diferem.


Material baseado em: Wickham, H., Çetinkaya-Rundel, M. & Grolemund, G. (2023). R for Data Science, 2ª ed. — O’Reilly.