Violin, Ridgeline e Boxplot com Jitter além do boxplot padrão
Esta aula foi construída para que você escolha o gráfico certo para comunicar distribuições com precisão. Os dados usados são o conjunto
CO2, disponível no R base. Todo o código está funcional e pronto para reproduzir.
geom_violin()geom_density_ridges()geom_jitter()O boxplot representa exatamente cinco estatísticas: mínimo, primeiro quartil (Q1), mediana, terceiro quartil (Q3) e máximo.
O que ele não registra:
Dois datasets com estatísticas resumidas idênticas podem ter distribuições completamente distintas. O boxplot não diferencia os dois.
set.seed(42)
# Dataset A: distribuição unimodal simétrica
dist_a <- tibble(
grupo = "A",
valor = rnorm(200, mean = 50, sd = 10))
# Dataset B: distribuição bimodal
# mesma média e desvio padrão que A, mas com dois picos
dist_b <- tibble(
grupo = "B",
valor = c(rnorm(100, mean = 40, sd = 5),
rnorm(100, mean = 60, sd = 5)))
dados_demo <- bind_rows(dist_a, dist_b)
# Verificar: as estatísticas resumidas são quase idênticas
dados_demo |>
group_by(grupo) |>
summarise(
media = round(mean(valor), 1),
mediana = round(median(valor), 1),
dp = round(sd(valor), 1),
q1 = round(quantile(valor, 0.25), 1),
q3 = round(quantile(valor, 0.75), 1))# A tibble: 2 × 6
grupo media mediana dp q1 q3
<chr> <dbl> <dbl> <dbl> <dbl> <dbl>
1 A 49.7 49.8 9.7 43.9 56.3
2 B 50.1 51.7 11.2 39.9 59.7
dados_demo |>
ggplot(aes(x = grupo, y = valor, fill = grupo)) +
geom_boxplot(alpha = 0.85, width = 0.4) +
scale_fill_manual(values = c(
"A" = cores_cafe["azul_escuro"],
"B" = cores_cafe["marrom"])) +
labs(
title = "Os dois boxplots parecem idênticos",
subtitle = "Mesma mediana, mesmo IQR, mesma amplitude",
x = NULL, y = "Valor",
caption = "Jennifer Lopes | Café com R") +
tema +
theme(legend.position = "none")dados_demo |>
ggplot(aes(x = grupo, y = valor, fill = grupo)) +
geom_violin(alpha = 0.85) +
scale_fill_manual(values = c(
"A" = cores_cafe["azul_escuro"],
"B" = cores_cafe["marrom"])) +
labs(
title = "O violin revela o que o boxplot escondeu",
subtitle = "Grupo A: unimodal simétrico | Grupo B: bimodal",
x = NULL, y = "Valor",
caption = "Jennifer Lopes | Café com R") +
tema +
theme(legend.position = "none")O grupo B tem dois picos de frequência.
O boxplot não mostrava isso.
| Pergunta analítica | Gráfico indicado |
|---|---|
| A distribuição tem um pico ou dois? | Violin plot |
| Como se comparam muitos grupos simultaneamente? | Ridgeline plot |
| Quantas observações existem em cada grupo? | Boxplot com jitter |
| A distribuição é simétrica dentro de cada quartil? | Violin plot |
O violin plot é construído a partir de uma estimativa de densidade de kernel (KDE). A densidade é calculada ao longo do eixo y e espelhada para os dois lados, formando a silhueta do violino.
O violin não mostra observações individuais. Ele mostra a densidade estimada da distribuição inteira.
Leitura vertical:
Leitura horizontal:
co2 |>
ggplot(aes(x = Type, y = uptake, fill = Type)) +
geom_violin(alpha = 0.85) +
scale_fill_manual(values = c(
"Quebec" = cores_cafe["azul_escuro"],
"Mississippi" = cores_cafe["marrom"])) +
labs(
title = "Absorção de CO2 por origem da planta",
subtitle = "Distribuição estimada por densidade de kernel",
x = NULL,
y = "Absorção de CO2 (umol/m²/s)",
caption = "Jennifer Lopes | Café com R") +
tema +
theme(legend.position = "none")O que o violin revela sobre Quebec e Mississippi:
Quebec tem um violino mais largo na parte superior, indicando alta concentração de absorção em valores elevados. A distribuição é assimétrica negativa: a maioria das plantas absorve muito.
Mississippi tem o violino mais largo no centro e na parte inferior, indicando que a maioria das plantas absorve pouco. A cauda superior é estreita.
As duas distribuições têm formas completamente distintas, o que não seria visível em um boxplot simples.
co2 |>
ggplot(aes(x = Type, y = uptake, fill = Type)) +
geom_violin(alpha = 0.75, color = "white") +
geom_boxplot(
width = 0.12, fill = "white",
outlier.shape = NA) +
stat_summary(
fun = mean, geom = "point",
shape = 18, size = 3.5,
color = cores_cafe["azul_escuro"]) +
scale_fill_manual(values = c(
"Quebec" = cores_cafe["azul_claro"],
"Mississippi" = cores_cafe["marrom"])) +
labs(
title = "Absorção de CO2 por origem",
subtitle = "Violino = distribuição | Caixa = IQR | Losango = média",
x = NULL, y = "Absorção de CO2 (umol/m²/s)",
caption = "Jennifer Lopes | Café com R") +
tema +
theme(legend.position = "none")Violino: mostra a forma da distribuição. Onde há mais observações, onde há menos, se há bimodalidade.
Boxplot interno: localiza a mediana e o IQR dentro da forma do violino. Permite leitura precisa de posição sem perder a forma.
Losango (média): quando a média está acima da mediana, a distribuição é assimétrica positiva. Quando está abaixo, assimétrica negativa. A distância entre os dois indica o grau de assimetria.
Tip
Para Quebec, a média está abaixo da mediana, confirmando assimetria negativa: existe uma cauda inferior que puxa a média para baixo.
# bw controla a largura de banda da estimativa de densidade
# Valor menor: mais detalhes, risco de ruído
# Valor maior: mais suave, risco de perder estrutura real
p1 <- co2 |>
ggplot(aes(x = Type, y = uptake, fill = Type)) +
geom_violin(bw = 2, alpha = 0.85) +
scale_fill_manual(values = unname(cores_cafe)[1:2]) +
labs(title = "bw = 2 (detalhado)", x = NULL,
y = "Absorção (umol/m²/s)") +
tema + theme(legend.position = "none")
p2 <- co2 |>
ggplot(aes(x = Type, y = uptake, fill = Type)) +
geom_violin(bw = 10, alpha = 0.85) +
scale_fill_manual(values = unname(cores_cafe)[1:2]) +
labs(title = "bw = 10 (suavizado)", x = NULL, y = NULL) +
tema + theme(legend.position = "none")
p1 | p2bw muito pequeno cria picos que não existem nos dados. bw muito grande apaga estrutura real.
O valor padrão do ggplot2 usa a regra de Silverman, que é um ponto de partida adequado para a maioria dos casos.
O ridgeline plot empilha curvas de densidade de múltiplos grupos ao longo do eixo y, com sobreposição parcial controlada entre elas.
scale controla o grau de sobreposição entre gruposscale = 1, as curvas não se sobrepõem. Com scale > 1, as curvas do grupo superior se sobrepõem às do grupo inferiorÉ o gráfico mais eficiente para comparar distribuições de muitos grupos simultaneamente, algo que o violin torna difícil visualmente quando há mais de quatro ou cinco grupos.
library(ggridges)
# Combinar Type e Treatment para criar quatro grupos
co2 |>
mutate(grupo = paste(Type, Treatment, sep = "\n")) |>
ggplot(aes(x = uptake, y = grupo, fill = Type)) +
geom_density_ridges(
alpha = 0.75, scale = 0.9,
color = "white") +
scale_fill_manual(values = c(
"Quebec" = cores_cafe["azul_escuro"],
"Mississippi" = cores_cafe["marrom"])) +
labs(
title = "Absorção de CO2 por origem e tratamento",
subtitle = "Cada faixa representa a distribuição de um grupo",
x = "Absorção de CO2 (umol/m²/s)",
y = NULL,
caption = "Jennifer Lopes | Café com R") +
tema +
theme(legend.position = "none")O que o ridgeline revela sobre os quatro grupos:
co2 |>
mutate(grupo = paste(Type, Treatment, sep = "\n")) |>
ggplot(aes(x = uptake, y = grupo,
fill = after_stat(x))) +
geom_density_ridges_gradient(
scale = 0.9, color = "white") +
scale_fill_gradientn(
colors = c(
cores_cafe["marrom"],
cores_cafe["bege"],
cores_cafe["azul_claro"],
cores_cafe["azul_escuro"]),
name = "Absorção") +
labs(
title = "Absorção de CO2 com gradiente por valor",
subtitle = "Cor representa o valor de absorção ao longo da distribuição",
x = "Absorção de CO2 (umol/m²/s)",
y = NULL,
caption = "Jennifer Lopes | Café com R") +
temaO que o gradiente adiciona à leitura:
after_stat(x) mapeia o valor do eixo x como variável de preenchimento, calculado internamente pelo ggplot2 após a estimativa de densidade.
co2 |>
mutate(grupo = paste(Type, Treatment, sep = "\n")) |>
ggplot(aes(x = uptake, y = grupo, fill = Type)) +
geom_density_ridges(
alpha = 0.7, scale = 0.9,
color = "white",
jittered_points = TRUE,
point_size = 1.5,
point_alpha = 0.5,
position = position_raincloud(
adjust_vlines = TRUE)) +
scale_fill_manual(values = c(
"Quebec" = cores_cafe["azul_escuro"],
"Mississippi" = cores_cafe["marrom"])) +
labs(
title = "Absorção de CO2 com observações individuais",
subtitle = "Pontos representam cada planta individualmente",
x = "Absorção de CO2 (umol/m²/s)",
y = NULL,
caption = "Jennifer Lopes | Café com R") +
tema +
theme(legend.position = "none")Com jittered_points = TRUE, cada observação aparece como um ponto abaixo da curva. Isso confirma que a densidade estimada reflete dados reais, não artefatos da suavização.
Important
Atenção: grupos com poucos dados produzem curvas de densidade artificialmente suaves que não refletem a distribuição real. O ggplot2 emite um aviso quando o número de observações é menor que 30.
Com o CO2, cada combinação de Type e Treatment tem apenas 21 observações. As curvas são interpretativas, não definitivas. Adicionar os pontos individuais com jittered_points = TRUE é uma boa prática para transparência.
O jitter adiciona uma dispersão horizontal aleatória aos pontos para evitar sobreposição. Quando múltiplas observações têm o mesmo valor ou valores muito próximos, geom_point() as empilha exatamente na mesma posição, tornando-as invisíveis.
geom_jitter() é equivalente a geom_point(position = position_jitter())widthset.seed()set.seed(42)
co2 |>
ggplot(aes(x = Type, y = uptake, fill = Type)) +
geom_boxplot(
alpha = 0.5, width = 0.4,
outlier.shape = NA) +
geom_jitter(
width = 0.15, alpha = 0.6,
size = 1.8,
color = cores_cafe["azul_escuro"]) +
scale_fill_manual(values = c(
"Quebec" = cores_cafe["azul_claro"],
"Mississippi" = cores_cafe["marrom"])) +
labs(
title = "Absorção de CO2 por origem",
subtitle = "Cada ponto representa uma observação individual",
x = NULL, y = "Absorção de CO2 (umol/m²/s)",
caption = "Jennifer Lopes | Café com R") +
tema +
theme(legend.position = "none")O que os pontos revelam sobre o CO2:
set.seed(42)
co2 |>
ggplot(aes(x = Type, y = uptake, fill = Type)) +
geom_boxplot(
alpha = 0.4, width = 0.45,
outlier.shape = NA) +
geom_jitter(
aes(color = Treatment),
width = 0.18, alpha = 0.75,
size = 2.2) +
scale_fill_manual(values = c(
"Quebec" = cores_cafe["azul_claro"],
"Mississippi" = cores_cafe["marrom"])) +
scale_color_manual(values = c(
"Sem resfriamento" = cores_cafe["azul_escuro"],
"Com resfriamento" = cores_cafe["bege"]),
name = "Tratamento") +
labs(
title = "Absorção por origem — pontos coloridos por tratamento",
subtitle = "Cor dos pontos indica o tratamento aplicado",
x = NULL, y = "Absorção de CO2 (umol/m²/s)",
caption = "Jennifer Lopes | Café com R") +
temaset.seed(42)
co2 |>
ggplot(aes(x = Type, y = uptake, fill = Type)) +
geom_boxplot(
alpha = 0.45, width = 0.4,
outlier.shape = NA) +
geom_jitter(
width = 0.15, alpha = 0.55,
size = 1.8,
color = cores_cafe["azul_escuro"]) +
stat_summary(
fun = mean, geom = "point",
shape = 18, size = 4,
color = cores_cafe["marrom"]) +
scale_fill_manual(values = c(
"Quebec" = cores_cafe["azul_claro"],
"Mississippi" = cores_cafe["bege"])) +
labs(
title = "Absorção de CO2 por origem",
subtitle = "Caixa = IQR | Pontos = observações | Losango = média",
x = NULL, y = "Absorção de CO2 (umol/m²/s)",
caption = "Jennifer Lopes | Café com R") +
tema +
theme(legend.position = "none")O que cada camada acrescenta:
Important
Atenção: outlier.shape = NA remove os outliers do boxplot quando geom_jitter() já está presente. Sem isso, os outliers aparecem duplicados: uma vez no boxplot e uma vez como ponto no jitter.
set.seed(42)
p_violin <- co2 |>
ggplot(aes(x = Type, y = uptake, fill = Type)) +
geom_violin(alpha = 0.75, color = "white") +
geom_boxplot(width = 0.1, fill = "white", outlier.shape = NA) +
scale_fill_manual(values = c(
"Quebec" = cores_cafe["azul_claro"],
"Mississippi" = cores_cafe["marrom"])) +
labs(title = "Violin", x = NULL,
y = "Absorção (umol/m²/s)",
caption = "Jennifer Lopes | Café com R") +
tema + theme(legend.position = "none")
p_ridge <- co2 |>
mutate(grupo = paste(Type, Treatment, sep = "\n")) |>
ggplot(aes(x = uptake, y = grupo, fill = Type)) +
geom_density_ridges(alpha = 0.75, scale = 0.9, color = "white") +
scale_fill_manual(values = c(
"Quebec" = cores_cafe["azul_claro"],
"Mississippi" = cores_cafe["marrom"])) +
labs(title = "Ridgeline", x = "Absorção (umol/m²/s)", y = NULL) +
tema + theme(legend.position = "none")
p_jitter <- co2 |>
ggplot(aes(x = Type, y = uptake, fill = Type)) +
geom_boxplot(alpha = 0.45, width = 0.4, outlier.shape = NA) +
geom_jitter(width = 0.15, alpha = 0.55, size = 1.8,
color = cores_cafe["azul_escuro"]) +
scale_fill_manual(values = c(
"Quebec" = cores_cafe["azul_claro"],
"Mississippi" = cores_cafe["marrom"])) +
labs(title = "Boxplot com Jitter", x = NULL, y = NULL) +
tema + theme(legend.position = "none")
p_violin | p_ridge | p_jitterO que cada gráfico revelou que os outros não revelaram:
Violin: revelou que Quebec tem distribuição com cauda inferior extensa. Não é visível no boxplot e é difícil de ler no ridgeline com quatro grupos.
Ridgeline: revelou o efeito do tratamento de resfriamento dentro de cada origem de forma simultânea. Comparar quatro grupos ao mesmo tempo seria impossível com violin sem sobreposição visual excessiva.
Boxplot com jitter: revelou a estrutura em bandas de Quebec, reflexo direto das concentrações de CO2 testadas no experimento. Nenhum dos outros gráficos tornava isso tão evidente.
| Pergunta analítica | Gráfico indicado |
|---|---|
| Qual é a forma da distribuição? Há bimodalidade? | Violin |
| Como comparar muitos grupos simultaneamente? | Ridgeline |
| Quantas observações existem? Onde estão concentradas? | Boxplot com jitter |
| A distribuição é simétrica? Onde está a média em relação à mediana? | Violin + boxplot interno |
| Existe estrutura nos dados além da tendência central? | Boxplot com jitter |
| Como o valor de uma variável se distribui ao longo de grupos ordenados? | Ridgeline |
ggplot2: ggplot2.tidyverse.orgggridges: wilkelab.org/ggridgesContinue praticando e explorando!
Esta aula é parte do projeto Café com R. É open source. Use, compartilhe e adapte.
Que cada gole desperte uma nova ideia. Que cada script abra uma nova conversa. Que o Café com R se torne um ponto de encontro nosso.

Jennifer Lopes | Café com R