Existe um gráfico que todo relatório estatístico tem e que quase ninguém lê inteiro. O leitor olha a caixa, localiza a mediana, vê os pontinhos soltos e segue em frente — sem saber que a ponta do bigode não é o valor máximo, que o número 1,5 da fórmula das cercas foi calibrado para uma situação específica, e que dois softwares podem desenhar caixas diferentes para os mesmos dados.
Este post desmonta o boxplot peça por peça. Para cada elemento: o que é, como se calcula, como se interpreta — e, no fim, o que ele esconde, as variantes que resolvem cada limitação e o código em Julia para gerar tudo.
1. O boxplot em uma figura
O boxplot — diagrama de caixa, box-and-whisker plot — é o gráfico que mais aparece em relatórios estatísticos e o que mais gente lê pela metade. Quase todo mundo sabe apontar a mediana. Bem menos gente sabe dizer o que a ponta do bigode representa, por que o número 1,5 aparece na conta das cercas, ou por que dois softwares desenham caixas ligeiramente diferentes para os mesmos dados.
Este post desmonta o gráfico peça por peça. Cada elemento tem três perguntas a responder: o que é, como se calcula e como se interpreta.
A figura abaixo é o mapa do post. Ela mostra, sobre o mesmo eixo horizontal: a distribuição dos dados (em cima), o boxplot com todos os elementos anotados (no meio) e os dados brutos que o resumo esconde (embaixo).
O boxplot troca 100 números por 5, escolhidos de modo que a troca sobreviva a valores extremos — e marca separadamente tudo o que não coube nesse resumo.
2. Os cinco números de Tukey
O boxplot foi apresentado por John W. Tukey em Exploratory Data Analysis (1977) com o nome de schematic plot. Wickham e Stryjewski (2011) mostram que ele tem um precursor direto: o range bar de Mary Eleanor Spear (1952), que já desenhava caixa e extremos. A contribuição de Tukey foi tornar o gráfico resistente: separar o corpo da distribuição dos pontos extremos, em vez de deixar que os extremos definam o desenho.
A base é o resumo de cinco números:
| Elemento | O que é | No exemplo (minutos) |
|---|---|---|
| Mínimo | Menor valor observado | 8,5 |
| Q1 (1º quartil) | Valor abaixo do qual estão 25% das observações | 13,4 |
| Q2 (mediana) | Valor que divide a amostra ao meio | 16,9 |
| Q3 (3º quartil) | Valor abaixo do qual estão 75% das observações | 21,7 |
| Máximo | Maior valor observado | 49,6 |
Repare que o gráfico não desenha o mínimo e o máximo. Ele desenha os quartis e, no lugar dos extremos, desenha os valores adjacentes — que quase sempre são outra coisa. É a fonte de erro de leitura mais comum, e a seção 5 trata dela.
3. A caixa: Q1, mediana, Q3 e o IQR
3.1 A caixa
A caixa vai de $Q_1$ a $Q_3$. Ela contém, por definição, os 50% centrais dos dados. No painel de cima da figura, é exatamente a soma dos dois quartos escuros da área sob a curva.
3.2 A linha interna
A linha grossa dentro da caixa é a mediana, não a média. A distinção importa: a mediana só depende da ordem dos valores, e por isso não se move quando um valor extremo fica ainda mais extremo. A média se move.
No exemplo, média = 18,4 e mediana = 16,9. Sempre que a média é sensivelmente maior que a mediana, há uma cauda pesada à direita puxando a média.
3.3 O IQR
A largura da caixa é a amplitude interquartil:
\[\mathrm{IQR} = Q_3 - Q_1\]No exemplo, $\mathrm{IQR} = 21{,}700 - 13{,}375 = 8{,}325$ minutos — os 8,3 que a figura mostra, arredondados para uma casa. Metade dos atendimentos se concentra numa faixa de pouco mais de oito minutos.
O IQR é a medida de dispersão do boxplot, e cumpre o mesmo papel do desvio-padrão com uma vantagem: seu ponto de ruptura é 25%. Você pode corromper até um quarto das observações de cada lado que o IQR continua descrevendo o miolo dos dados. Com o desvio-padrão, um único valor absurdo basta para inflar a medida.
Posição da caixa = onde estão os valores típicos. Largura da caixa = quão espalhados eles são. Posição da mediana dentro da caixa = assimetria.
3.4 A assimetria já aparece dentro da caixa
Compare as duas metades da caixa:
\[Q_2 - Q_1 = 3{,}475 \qquad\text{e}\qquad Q_3 - Q_2 = 4{,}850\]A metade de cima é mais larga: entre a mediana e $Q_3$ os dados estão mais espalhados do que entre $Q_1$ e a mediana. Isso é assimetria à direita, e ela é visível sem calcular nenhum coeficiente — basta ver que a linha da mediana não está no centro da caixa.
4. Como se calcula um quartil (e por que softwares discordam)
Aqui está o detalhe que a maioria dos textos didáticos omite: não existe uma única definição de quartil amostral. Hyndman e Fan (1996) catalogaram nove definições diferentes em uso nos pacotes estatísticos.
O Julia, o R e o NumPy usam por padrão o tipo 7. Para uma proporção $p$ e uma amostra ordenada $x_{(1)} \le \dots \le x_{(n)}$:
\[h = (n-1)\,p + 1, \qquad Q(p) = x_{(\lfloor h \rfloor)} + \bigl(h - \lfloor h \rfloor\bigr)\left(x_{(\lceil h \rceil)} - x_{(\lfloor h \rfloor)}\right)\]Ou seja: encontra-se a posição $h$ e interpola-se linearmente entre os dois valores vizinhos.
Já o boxplot original de Tukey não usa esse quartil. Ele usa os hinges (quartos): a mediana da metade inferior e a mediana da metade superior da amostra — incluindo a mediana global nas duas metades quando $n$ é ímpar. Os dois resultados são próximos, mas não idênticos.
Em amostras grandes a diferença é invisível. Em amostras pequenas (n < 30), a mesma amostra pode gerar caixas visivelmente diferentes em softwares diferentes — e, como as cercas dependem dos quartis, isso muda também quais pontos são marcados como outliers. Frigge, Hoaglin e Iglewicz (1989) documentaram essa divergência entre implementações. Ao publicar, diga qual definição você usou.
5. Os bigodes e as cercas: de onde vem o 1,5
Esta é a parte do gráfico mais mal interpretada. São duas coisas diferentes com nomes parecidos.
5.1 As cercas (fences) — valores calculados
As cercas são limiares aritméticos. Elas não são desenhadas na maioria dos softwares (na figura deste post elas aparecem pontilhadas, de propósito):
\[\text{cerca inferior} = Q_1 - 1{,}5 \cdot \mathrm{IQR} \qquad \text{cerca superior} = Q_3 + 1{,}5 \cdot \mathrm{IQR}\]No exemplo, com os quartis sem arredondar ($Q_1 = 13{,}375$ e $\mathrm{IQR} = 8{,}325$):
\[13{,}375 - 1{,}5 \times 8{,}325 = 0{,}89 \qquad\text{e}\qquad 21{,}700 + 1{,}5 \times 8{,}325 = 34{,}19\]que são os 0,9 e 34,2 exibidos na figura, arredondados para uma casa decimal.
5.2 Os bigodes (whiskers) — valores observados
O bigode não vai até a cerca. Ele para no valor adjacente: a observação mais extrema que ainda está dentro da cerca.
No exemplo, a cerca inferior está em 0,9 minuto, mas nenhum atendimento durou tão pouco — o bigode esquerdo para em 8,5, que é o menor tempo observado. Do lado direito, a cerca está em 34,2 e o bigode para em 29,3, o maior tempo abaixo da cerca.
❌ "O bigode é o mínimo e o máximo." — Só por coincidência, quando não há outliers.
❌ "O bigode é um desvio-padrão." — O boxplot não usa desvio-padrão em lugar nenhum.
❌ "O bigode marca 1,5 × IQR." — Isso é a cerca. O bigode para no último dado antes dela.
5.3 Por que 1,5?
O 1,5 é uma escolha de Tukey, calibrada — não uma dedução. Se os dados vierem de uma distribuição normal, as cercas caem a aproximadamente $\pm 2{,}698\,\sigma$ da média, e a probabilidade de uma observação cair fora delas é de cerca de 0,70%.
Ou seja: numa amostra normal de 100 observações, espera-se menos de um “outlier” por puro acaso. O critério foi escolhido para ser raro sob normalidade e ainda assim sensível o suficiente para sinalizar caudas pesadas.
Tukey também definiu uma cerca externa, em $Q_3 + 3 \cdot \mathrm{IQR}$, para separar pontos “distantes” (far out) dos meramente atípicos. Sob normalidade ela corresponde a $\pm 4{,}72\,\sigma$ — cerca de 2 em cada milhão de observações.
6. Outliers: o que o ponto solto significa
Cada ponto além das cercas é desenhado individualmente. No exemplo são cinco: 37,8; 38,5; 38,5; 44,0 e 49,6 minutos.
O que esses pontos significam:
| O ponto é… | Leitura correta |
|---|---|
| um valor incomum em relação ao IQR desta amostra | É uma marcação relativa. Muda se a dispersão do resto mudar. |
| um candidato à investigação | Vale olhar o caso: erro de digitação? Outra população? Evento real e raro? |
| não um erro comprovado | O critério é geométrico, não diagnóstico. Ele não sabe nada sobre o fenômeno. |
| não licença para excluir o dado | Excluir observação porque o gráfico a destacou é como apagar o termômetro por causa da febre. |
Um detalhe que muda a leitura: sob normalidade esperaríamos 0,7 outlier em $n = 100$; encontramos 5. Isso não indica cinco erros de medição — indica que a distribuição não é normal: ela tem cauda direita pesada, como é típico de tempos de espera e atendimento.
Quando existem muitos valores extremos juntos, eles inflam o próprio IQR, empurram as cercas para longe e podem deixar de ser marcados. O boxplot é resistente, não infalível: com caudas muito pesadas, use também a seção 9.
7. Como ler um boxplot em seis passos
Comparando vários grupos, acrescente: as caixas se sobrepõem? As medianas de um grupo caem fora da caixa do outro? A dispersão muda junto com o nível (caixas maiores onde a mediana é maior indicam que uma escala logarítmica pode ser mais adequada)?
8. O que o boxplot esconde
O boxplot é um resumo, e todo resumo perde informação. A pergunta é: quanta?
A figura abaixo mostra quatro amostras de $n = 400$ construídas de propósito para terem a mesma mediana e o mesmo IQR. À esquerda, só os boxplots. À direita, os mesmos dados com violino e pontos sobrepostos.
O caso grave é o bimodal: uma distribuição com dois grupos separados produz um boxplot de aparência perfeitamente normal, com a mediana caindo justamente na região onde não há quase nenhum dado. É o mesmo argumento do Quarteto de Anscombe, transportado dos resumos bivariados para os univariados.
Até algumas centenas de pontos, sobreponha os dados ao boxplot (com jitter). Custa uma linha de código e elimina a classe inteira de erro acima. Krzywinski e Altman (2014) fazem essa recomendação explicitamente em Nature Methods.
9. Truques e variantes
Três variantes clássicas resolvem três limitações reais do boxplot padrão. Todas estão na figura, e todas estão implementadas no código da seção 11.
9.1 Notch: comparar medianas sem fazer teste
O notch é um entalhe na altura da mediana, com meia-largura
\[\pm\, 1{,}58 \cdot \frac{\mathrm{IQR}}{\sqrt{n}}\]A constante 1,58 merece um cuidado: ela não é o intervalo de confiança da mediana de um grupo isolado — é calibrada para a comparação entre dois grupos. A construção supõe normalidade assintótica da mediana e tamanhos de amostra parecidos, e faz com que a não sobreposição entre dois entalhes corresponda a aproximadamente 95% de confiança para a diferença entre as duas medianas. É a mesma conta que aparece com o valor 1,57 em Chambers et al. (1983, p. 62) e é o que o R implementa em boxplot.stats.
A leitura, então, é direta: se os entalhes de dois grupos não se sobrepõem, há evidência de que as medianas diferem. É uma aproximação e não substitui um teste formal, mas resolve a maior parte das comparações visuais.
9.2 Largura proporcional a √n
O boxplot padrão desenha uma caixa de $n = 12$ do mesmo tamanho de uma caixa de $n = 900$, e o leitor confia igualmente nas duas. Fazendo a largura proporcional a $\sqrt{n}$ — que é como a precisão de fato cresce — o tamanho da amostra volta ao gráfico.
9.3 Letter-value plot: para amostras grandes
Com $n$ muito grande, a regra 1,5 · IQR desmonta: ela foi calibrada para ser rara sob normalidade, e em dados reais assimétricos ela marca uma multidão. No exemplo da figura (lognormal, $n = 20.000$), o boxplot padrão marca 970 pontos — 4,85% da amostra — como outliers. Não é informação; é entulho.
O letter-value plot substitui a decisão binária “dentro ou fora” por uma sequência de quantis cada vez mais extremos — a mediana (M), os quartos (F, de fourths), os oitavos (E), os dezesseis avos (D), e assim por diante. Cada caixa cobre metade da cauda restante da anterior. O número de letras cresce com $n$, exatamente porque uma amostra maior permite estimar a cauda com mais confiança.
9.4 Outros truques que valem a pena
| Situação | O que fazer |
|---|---|
| n muito pequeno (menos de ~10 por grupo) | Não use boxplot: mostre os pontos. Cinco pontos não sustentam quartis. |
| Vários grupos sem ordem natural | Ordene as caixas pela mediana. O gráfico passa a ser lido em um passe. |
| Dados positivos muito assimétricos (tempo, renda, contagens) | Escala logarítmica no eixo dos valores. Metade dos "outliers" costuma desaparecer. |
| Suspeita de bimodalidade | Violino ou raincloud (densidade + boxplot + pontos). |
| Boxplot horizontal vs. vertical | Horizontal quando os rótulos dos grupos são longos — nomes ficam legíveis sem rotacionar texto. |
| Publicação científica | Declare na legenda: o que os bigodes representam, qual definição de quartil e o n de cada grupo. |
10. Erros comuns
11. Código em Julia com CairoMakie
Todas as figuras deste post foram geradas em Julia com CairoMakie, que exporta PNG em 300 dpi e PDF vetorial — os dois formatos que uma revista costuma pedir.
Para instalar os pacotes:
using Pkg
Pkg.add(["CairoMakie", "Distributions", "KernelDensity", "StatsBase", "Colors"])
11.1 A função que calcula todos os elementos
Se você levar uma única coisa deste post para o seu código, que seja esta função. Ela devolve, de uma vez, tudo o que o boxplot desenha — inclusive a distinção entre cerca e valor adjacente:
"""
cinco_numeros(x)
Devolve os elementos que o boxplot desenha:
quartis, IQR, cercas internas (1,5·IQR), valores adjacentes (pontas dos
bigodes = valores observados mais extremos DENTRO das cercas) e outliers.
Nota: `quantile` do Julia usa o tipo 7 de Hyndman & Fan (1996), o mesmo do R e
do numpy; Tukey (1977) usa *hinges* (medianas das metades), que diferem em
até 1/4 de observação. Em amostras pequenas isso desloca visivelmente a caixa.
"""
function cinco_numeros(x)
q1, q2, q3 = quantile(x, [0.25, 0.5, 0.75])
iqr = q3 - q1
cerca_inf, cerca_sup = q1 - 1.5iqr, q3 + 1.5iqr
dentro = x[(x .>= cerca_inf) .& (x .<= cerca_sup)]
(; q1, q2, q3, iqr, cerca_inf, cerca_sup,
adj_inf = minimum(dentro), adj_sup = maximum(dentro),
outliers = x[(x .< cerca_inf) .| (x .> cerca_sup)],
n = length(x))
end
Aplicada aos dados do exemplo, ela produz exatamente os números que aparecem na figura da seção 1:
mínimo 8.5
valor adj. inferior 8.5 (ponta do bigode esquerdo)
Q1 13.4
mediana (Q2) 16.9
Q3 21.7
IQR 8.3
cerca inferior 0.9 (Q1 − 1,5·IQR)
cerca superior 34.2 (Q3 + 1,5·IQR)
valor adj. superior 29.3 (ponta do bigode direito)
máximo 49.6
outliers 5 [37.8, 38.5, 38.5, 44.0, 49.6]
assimetria 1.70 (média 18.4 > mediana 16.9)
11.2 O script completo
O script abaixo gera as quatro figuras do post (anatomia, armadilha, variantes e capa), em PNG e PDF. As funções de desenho — boxplot_h!, caixa_notch! e letter_values — são independentes e podem ser reaproveitadas com seus próprios dados.
# =============================================================================
# ANATOMIA DO BOXPLOT — figuras didáticas para artigo
# Julia 1.12 · CairoMakie
#
# Gera três figuras (PNG 300 dpi + PDF vetorial):
# 1. boxplot-anatomia → cada elemento do boxplot, anotado e ligado
# à distribuição que ele resume
# 2. boxplot-armadilha → quatro distribuições muito diferentes com
# o MESMO boxplot (por que sobrepor os dados)
# 3. boxplot-variantes → notch, largura variável e letter-value plot
#
# Referências
# Tukey, J. W. (1977). Exploratory Data Analysis. Addison-Wesley.
# → o box-and-whisker original ("schematic plot"), hinges e a regra 1,5·IQR
# McGill, R., Tukey, J. W. & Larsen, W. A. (1978). Variations of Box Plots.
# The American Statistician, 32(1), 12-16. → notch e largura variável
# Frigge, M., Hoaglin, D. C. & Iglewicz, B. (1989). Some Implementations of
# the Boxplot. The American Statistician, 43(1), 50-54.
# Hyndman, R. J. & Fan, Y. (1996). Sample Quantiles in Statistical Packages.
# The American Statistician, 50(4), 361-365. → os 9 tipos de quantil
# Hintze, J. L. & Nelson, R. D. (1998). Violin Plots. TAS, 52(2), 181-184.
# Hofmann, H., Wickham, H. & Kafadar, K. (2017). Letter-Value Plots: Boxplots
# for Large Data. J. Comput. Graph. Stat., 26(3), 469-477.
# Krzywinski, M. & Altman, N. (2014). Visualizing samples with box plots.
# Nature Methods, 11(2), 119-120.
# Wickham, H. & Stryjewski, L. (2011). 40 years of boxplots. (technical report)
# =============================================================================
using CairoMakie, Statistics, StatsBase, Random, Distributions, KernelDensity
using Colors, Printf
CairoMakie.activate!(type = "png", px_per_unit = 3)
const OUT = joinpath(@__DIR__, "assets", "images")
mkpath(OUT)
# ── paleta (validada para daltonismo; ver referências de dataviz) ────────────
const SURF = parse(Colorant, "#fcfcfb") # superfície do gráfico
const INK = parse(Colorant, "#0b0b0b") # tinta primária
const INK2 = parse(Colorant, "#52514e") # tinta secundária
const MUTED = parse(Colorant, "#898781") # eixos, rótulos discretos
const GRID = parse(Colorant, "#e1e0d9") # hairline
const BLUE = parse(Colorant, "#2a78d6") # série 1
const BLUE_L = parse(Colorant, "#cde2fb") # azul 100
const BLUE_M = parse(Colorant, "#9ec5f4") # azul 200
const BLUE_D = parse(Colorant, "#184f95") # azul 600
const ORANGE = parse(Colorant, "#eb6834") # série 2
const AQUA = parse(Colorant, "#1baf7a") # série 3
const RED = parse(Colorant, "#e34948") # outliers / atenção
# ── estatísticas resumo de cinco números + cercas de Tukey ───────────────────
"""
cinco_numeros(x)
Devolve os elementos que o boxplot desenha:
quartis, IQR, cercas internas (1,5·IQR), valores adjacentes (pontas dos
bigodes = valores observados mais extremos DENTRO das cercas) e outliers.
Nota: `quantile` do Julia usa o tipo 7 de Hyndman & Fan (1996), o mesmo do R e
do numpy; Tukey (1977) usa *hinges* (medianas das metades), que diferem em
até 1/4 de observação. Em amostras pequenas isso desloca visivelmente a caixa.
"""
function cinco_numeros(x)
q1, q2, q3 = quantile(x, [0.25, 0.5, 0.75])
iqr = q3 - q1
cerca_inf, cerca_sup = q1 - 1.5iqr, q3 + 1.5iqr
dentro = x[(x .>= cerca_inf) .& (x .<= cerca_sup)]
(; q1, q2, q3, iqr, cerca_inf, cerca_sup,
adj_inf = minimum(dentro), adj_sup = maximum(dentro),
outliers = x[(x .< cerca_inf) .| (x .> cerca_sup)],
n = length(x))
end
# ── primitivas de desenho ────────────────────────────────────────────────────
"Caixa + bigodes + outliers, na horizontal, centrada em `y` com meia-altura `h`."
function boxplot_h!(ax, s, y, h; cor = BLUE, fundo = BLUE_L, lw = 1.8, pontos = true)
lines!(ax, [s.adj_inf, s.q1], [y, y]; color = cor, linewidth = lw)
lines!(ax, [s.q3, s.adj_sup], [y, y]; color = cor, linewidth = lw)
for v in (s.adj_inf, s.adj_sup) # tampas dos bigodes
lines!(ax, [v, v], [y - 0.55h, y + 0.55h]; color = cor, linewidth = lw)
end
poly!(ax, Rect2f(s.q1, y - h, s.q3 - s.q1, 2h);
color = fundo, strokecolor = cor, strokewidth = lw)
lines!(ax, [s.q2, s.q2], [y - h, y + h]; color = cor, linewidth = 3.2)
pontos && scatter!(ax, s.outliers, fill(y, length(s.outliers));
color = RED, markersize = 10, strokecolor = SURF,
strokewidth = 1.5)
return nothing
end
"Seta dupla horizontal em `y`, de `x1` a `x2` (usada para medir o IQR)."
function seta_dupla!(ax, x1, x2, y; cor = INK2, lw = 1.3, ms = 9)
lines!(ax, [x1, x2], [y, y]; color = cor, linewidth = lw)
scatter!(ax, [x1], [y]; marker = :ltriangle, color = cor, markersize = ms)
scatter!(ax, [x2], [y]; marker = :rtriangle, color = cor, markersize = ms)
end
"Linha-guia (leader) fina ligando um elemento ao seu rótulo."
guia!(ax, x, y1, y2; cor = MUTED) =
lines!(ax, [x, x], [y1, y2]; color = cor, linewidth = 0.9)
"Número formatado com vírgula decimal (padrão pt-BR)."
rot(x; d = 1) = replace(Printf.format(Printf.Format("%.$(d)f"), x), "." => ",")
# =============================================================================
# FIGURA 1 — anatomia
# =============================================================================
Random.seed!(42)
# tempo de atendimento (min): assimétrico à direita, como quase todo tempo real
dados = round.(rand(LogNormal(log(17.5), 0.34), 100), digits = 1)
dados[[7, 33, 61]] .= [38.5, 44.0, 49.6] # três casos extremos plausíveis
s = cinco_numeros(dados)
xmin, xmax = 0.0, 54.0
fig1 = Figure(size = (1180, 900), backgroundcolor = SURF,
figure_padding = (30, 30, 22, 18))
Label(fig1[1, 1], "Anatomia de um boxplot";
fontsize = 24, font = :bold, color = INK, halign = :left)
Label(fig1[2, 1],
"Tempo de atendimento em uma unidade de saúde · n = $(s.n) · cinco números " *
"resumem a distribuição e separam o típico do extremo";
fontsize = 13.5, color = INK2, halign = :left)
# — painel A: a distribuição por trás do resumo ------------------------------
axA = Axis(fig1[3, 1]; backgroundcolor = SURF, ylabel = "densidade",
ylabelsize = 12, ylabelcolor = MUTED)
hidespines!(axA); hidedecorations!(axA, label = false)
k = kde(dados; boundary = (xmin, xmax))
xs, ys = collect(k.x), collect(k.density)
regioes = [(s.adj_inf, s.q1, BLUE_L), (s.q1, s.q2, BLUE_M),
(s.q2, s.q3, BLUE_M), (s.q3, s.adj_sup, BLUE_L)]
for (a, b, cor) in regioes
m = (xs .>= a) .& (xs .<= b)
band!(axA, xs[m], zeros(count(m)), ys[m]; color = cor)
text!(axA, (a + b) / 2, 0.004; text = "25%", align = (:center, :bottom),
fontsize = 12, color = INK2)
end
lines!(axA, xs, ys; color = BLUE, linewidth = 2)
for v in (s.q1, s.q2, s.q3)
i = argmin(abs.(xs .- v))
lines!(axA, [v, v], [0, ys[i]]; color = SURF, linewidth = 2)
end
text!(axA, s.q2, maximum(ys) * 1.04;
text = "cada quarto da área ≈ 25% das observações",
align = (:center, :bottom), fontsize = 12, color = MUTED)
ylims!(axA, 0, maximum(ys) * 1.36)
# — painel B: o boxplot anotado ----------------------------------------------
axB = Axis(fig1[4, 1]; backgroundcolor = SURF)
hidespines!(axB); hidedecorations!(axB)
ylims!(axB, 0, 1)
# cercas internas (limiares — únicas linhas pontilhadas da figura)
for (v, lab, al) in ((s.cerca_inf, "cerca inferior = $(rot(s.cerca_inf))\nQ1 − 1,5·IQR", :left),
(s.cerca_sup, "cerca superior = $(rot(s.cerca_sup))\nQ3 + 1,5·IQR", :right))
lines!(axB, [v, v], [0.06, 0.66]; color = MUTED, linewidth = 1.1,
linestyle = :dot)
text!(axB, v + (al == :left ? 0.6 : -0.6), 0.045; text = lab,
align = (al, :top), fontsize = 11.5, color = INK2)
end
boxplot_h!(axB, s, 0.52, 0.15)
# acima: mediana e as pontas dos bigodes
guia!(axB, s.q2, 0.68, 0.715)
text!(axB, s.q2, 0.725; text = "mediana (Q2) = $(rot(s.q2))\n50% dos dados abaixo",
align = (:center, :bottom), fontsize = 13, color = BLUE_D, font = :bold)
for (v, lab) in ((s.adj_inf, "valor adjacente inferior = $(rot(s.adj_inf))"),
(s.adj_sup, "valor adjacente superior = $(rot(s.adj_sup))"))
guia!(axB, v, 0.61, 0.875)
text!(axB, v, 0.885; text = lab * "\núltimo valor dentro da cerca",
align = (:center, :bottom), fontsize = 11.5, color = INK2)
end
# abaixo: quartis, IQR
for (v, lab) in ((s.q1, "Q1 = $(rot(s.q1))\n25% abaixo"),
(s.q3, "Q3 = $(rot(s.q3))\n75% abaixo"))
guia!(axB, v, 0.37, 0.325)
text!(axB, v, 0.315; text = lab, align = (:center, :top),
fontsize = 12.5, color = INK2)
end
seta_dupla!(axB, s.q1, s.q3, 0.185)
text!(axB, (s.q1 + s.q3) / 2, 0.16;
text = "IQR = Q3 − Q1 = $(rot(s.iqr))\na caixa contém os 50% centrais",
align = (:center, :top), fontsize = 12.5, color = INK2)
# caixa e outliers
text!(axB, mean(s.outliers), 0.57;
text = "outliers: $(length(s.outliers)) pontos além das cercas",
align = (:center, :bottom), fontsize = 12, color = RED)
text!(axB, xmax - 0.8, 0.42;
text = "o bigode não é o mínimo nem o máximo,\ne não é um desvio-padrão",
align = (:right, :top), fontsize = 11.5, color = MUTED)
# — painel C: os dados brutos ------------------------------------------------
axC = Axis(fig1[5, 1]; backgroundcolor = SURF, xlabel = "tempo de atendimento (minutos)",
xlabelsize = 13.5, xlabelcolor = INK2, xticklabelsize = 12,
xticklabelcolor = INK2, xgridcolor = GRID, xgridwidth = 1,
ygridvisible = false, bottomspinecolor = GRID,
xtickcolor = MUTED, xticksize = 4)
hidespines!(axC, :l, :r, :t); hideydecorations!(axC)
Random.seed!(7)
jit = 0.48 .+ 0.34 .* (rand(length(dados)) .- 0.5)
eh_out = (dados .< s.cerca_inf) .| (dados .> s.cerca_sup)
scatter!(axC, dados[.!eh_out], jit[.!eh_out]; color = (BLUE, 0.55),
markersize = 9, strokewidth = 0)
scatter!(axC, dados[eh_out], jit[eh_out]; color = RED, markersize = 10,
strokecolor = SURF, strokewidth = 1.5)
text!(axC, xmin + 0.8, 0.97; text = "os dados que o resumo esconde (jitter)",
align = (:left, :top), fontsize = 12, color = MUTED)
ylims!(axC, 0, 1)
linkxaxes!(axA, axB, axC)
xlims!(axC, xmin, xmax)
Label(fig1[6, 1],
"Regra 1,5·IQR e o gráfico esquemático: Tukey (1977). " *
"Quantis pelo tipo 7 de Hyndman & Fan (1996) — o mesmo de R e numpy.";
fontsize = 11, color = MUTED, halign = :left)
rowsize!(fig1.layout, 3, Fixed(150))
rowsize!(fig1.layout, 4, Fixed(350))
rowsize!(fig1.layout, 5, Fixed(105))
rowgap!(fig1.layout, 1, 4); rowgap!(fig1.layout, 2, 16)
rowgap!(fig1.layout, 3, 2); rowgap!(fig1.layout, 4, 2)
rowgap!(fig1.layout, 5, 12)
save(joinpath(OUT, "boxplot-anatomia.png"), fig1)
save(joinpath(OUT, "boxplot-anatomia.pdf"), fig1)
# =============================================================================
# FIGURA 2 — a armadilha: mesmo boxplot, distribuições diferentes
# =============================================================================
Random.seed!(11)
n = 400
amostras = [
("simétrica", randn(n) .* 1.0 .+ 10),
("bimodal", vcat(randn(n ÷ 2) .* 0.42 .+ 8.35, randn(n ÷ 2) .* 0.42 .+ 11.65)),
("uniforme", rand(Uniform(7.6, 12.4), n)),
("assimétrica", 10 .+ (rand(Gamma(1.6, 1.0), n) .- 1.6) .* 0.86),
]
# padroniza mediana e IQR: os quatro boxplots ficam praticamente idênticos
amostras = [(nome, begin
q1, q2, q3 = quantile(v, [0.25, 0.5, 0.75])
(v .- q2) .* (1.35 / (q3 - q1)) .+ 10.0
end) for (nome, v) in amostras]
fig2 = Figure(size = (1180, 620), backgroundcolor = SURF,
figure_padding = (30, 30, 22, 18))
Label(fig2[1, 1:2], "O boxplot não é único: quatro distribuições, o mesmo resumo";
fontsize = 22, font = :bold, color = INK, halign = :left)
Label(fig2[2, 1:2],
"n = 400 por linha, com mediana e IQR idênticos por construção: as caixas saem iguais\n" *
"e a forma da distribuição fica invisível. À direita, o mesmo dado com violino e pontos.";
fontsize = 13, color = INK2, halign = :left)
axL = Axis(fig2[3, 1]; backgroundcolor = SURF, title = "só o boxplot",
titlesize = 14, titlecolor = INK2, titlealign = :left,
yticks = (1:4, reverse(first.(amostras))), yticklabelsize = 13,
yticklabelcolor = INK2, xticklabelsize = 12, xticklabelcolor = INK2,
xgridcolor = GRID, ygridvisible = false, xlabel = "valor",
xlabelsize = 13, xlabelcolor = INK2)
axR = Axis(fig2[3, 2]; backgroundcolor = SURF,
title = "boxplot + violino + dados", titlesize = 14,
titlecolor = INK2, titlealign = :left,
yticksvisible = false, yticklabelsvisible = false,
xticklabelsize = 12, xticklabelcolor = INK2, xgridcolor = GRID,
ygridvisible = false, xlabel = "valor", xlabelsize = 13,
xlabelcolor = INK2)
for a in (axL, axR); hidespines!(a, :t, :r, :l); a.bottomspinecolor = GRID; end
for (i, (nome, v)) in enumerate(amostras)
si = cinco_numeros(v)
pos = Float64(length(amostras) + 1 - i) # 1ª amostra no topo
boxplot_h!(axL, si, pos, 0.20)
# direita: violino (densidade espelhada) + jitter + box fino
ki = kde(v)
dens = ki.density ./ maximum(ki.density) .* 0.34
band!(axR, collect(ki.x), pos .- dens, pos .+ dens; color = (BLUE_M, 0.75))
Random.seed!(100 + i)
idx = rand(1:length(v), 120)
scatter!(axR, v[idx], pos .+ 0.30 .* (rand(120) .- 0.5);
color = (INK2, 0.35), markersize = 5, strokewidth = 0)
boxplot_h!(axR, si, pos, 0.075; cor = INK, fundo = SURF,
lw = 1.2, pontos = false)
end
for a in (axL, axR); ylims!(a, 0.4, 4.75); xlims!(a, 6.2, 13.8); end
Label(fig2[4, 1:2],
"Mesma lição dos quartetos de Anscombe (1973), aplicada a resumos de " *
"uma variável; sobreposição de dados: Krzywinski & Altman (2014); " *
"violino: Hintze & Nelson (1998).";
fontsize = 11, color = MUTED, halign = :left)
rowgap!(fig2.layout, 1, 4); rowgap!(fig2.layout, 2, 18); rowgap!(fig2.layout, 3, 12)
colgap!(fig2.layout, 26)
save(joinpath(OUT, "boxplot-armadilha.png"), fig2)
save(joinpath(OUT, "boxplot-armadilha.pdf"), fig2)
# =============================================================================
# FIGURA 3 — três variantes que resolvem problemas reais
# =============================================================================
"Polígono da caixa com entalhe (notch) em torno da mediana — horizontal."
function caixa_notch!(ax, s, y, h; cor = BLUE, fundo = BLUE_L, lw = 1.6)
nlo = s.q2 - 1.58 * s.iqr / sqrt(s.n)
nhi = s.q2 + 1.58 * s.iqr / sqrt(s.n)
nlo, nhi = max(nlo, s.q1), min(nhi, s.q3)
hn = 0.45h
pts = Point2f[(s.q1, y - h), (nlo, y - h), (s.q2, y - hn), (nhi, y - h),
(s.q3, y - h), (s.q3, y + h), (nhi, y + h), (s.q2, y + hn),
(nlo, y + h), (s.q1, y + h)]
lines!(ax, [s.adj_inf, s.q1], [y, y]; color = cor, linewidth = lw)
lines!(ax, [s.q3, s.adj_sup], [y, y]; color = cor, linewidth = lw)
poly!(ax, pts; color = fundo, strokecolor = cor, strokewidth = lw)
lines!(ax, [s.q2, s.q2], [y - hn, y + hn]; color = cor, linewidth = 3)
scatter!(ax, s.outliers, fill(y, length(s.outliers)); color = RED,
markersize = 8, strokecolor = SURF, strokewidth = 1.2)
return (nlo, nhi)
end
"""
letter_values(x; k)
Profundidades e valores das letras (M = mediana, F = quartos, E = oitavos, ...)
usadas no letter-value plot de Hofmann, Wickham & Kafadar (2017).
"""
function letter_values(x; k = nothing)
xs = sort(x); n = length(xs)
kmax = isnothing(k) ? max(2, ceil(Int, log2(n)) - 3) : k
d = (n + 1) / 2
depths = [d]
while length(depths) < kmax
d = (floor(d) + 1) / 2
d < 1 && break
push!(depths, d)
end
lo = [(xs[floor(Int, d)] + xs[ceil(Int, d)]) / 2 for d in depths]
hi = [(xs[n + 1 - floor(Int, d)] + xs[n + 1 - ceil(Int, d)]) / 2 for d in depths]
cob = [1 - 2.0^-(i) for i in 1:length(depths)] # cobertura aproximada
(; depths, lo, hi, cob)
end
fig3 = Figure(size = (1180, 700), backgroundcolor = SURF,
figure_padding = (30, 30, 22, 18))
Label(fig3[1, 1:3], "Três variantes que resolvem problemas do boxplot padrão";
fontsize = 22, font = :bold, color = INK, halign = :left)
# (a) notch — comparar medianas ----------------------------------------------
Random.seed!(3)
grupos = [("A", randn(120) .* 2.3 .+ 20.0),
("B", randn(120) .* 2.3 .+ 21.0),
("C", randn(120) .* 2.3 .+ 24.5)]
axa = Axis(fig3[2, 1]; backgroundcolor = SURF,
title = "(a) notch: teste visual de medianas", titlesize = 14,
titlealign = :left, titlecolor = INK,
yticks = (1:3, reverse(first.(grupos))), yticklabelsize = 13,
yticklabelcolor = INK2, xticklabelsize = 11, xticklabelcolor = INK2,
xgridcolor = GRID, ygridvisible = false, xlabel = "valor",
xlabelsize = 12, xlabelcolor = INK2)
for (i, (_, v)) in enumerate(grupos)
caixa_notch!(axa, cinco_numeros(v), Float64(4 - i), 0.24)
end
text!(axa, 31.5, 4.25;
text = "entalhes que não se sobrepõem ⇒\nmedianas diferentes (≈95%)",
align = (:right, :top), fontsize = 11, color = INK2)
ylims!(axa, 0.5, 4.3)
# (b) largura variável — mostrar n -------------------------------------------
Random.seed!(5)
gr2 = [("n = 12", randn(12) .* 2.4 .+ 20), ("n = 60", randn(60) .* 2.4 .+ 21),
("n = 900", randn(900) .* 2.4 .+ 20.5)]
axb = Axis(fig3[2, 2]; backgroundcolor = SURF,
title = "(b) largura ∝ √n: quanto pesa cada caixa", titlesize = 14,
titlealign = :left, titlecolor = INK,
yticks = (1:3, reverse(first.(gr2))), yticklabelsize = 12.5,
yticklabelcolor = INK2, xticklabelsize = 11, xticklabelcolor = INK2,
xgridcolor = GRID, ygridvisible = false, xlabel = "valor",
xlabelsize = 12, xlabelcolor = INK2)
ns = [length(v) for (_, v) in gr2]
for (i, (_, v)) in enumerate(gr2)
h = 0.32 * sqrt(ns[i]) / sqrt(maximum(ns))
boxplot_h!(axb, cinco_numeros(v), Float64(4 - i), max(h, 0.055))
end
text!(axb, 28.2, 4.25;
text = "a largura mostra o n: uma caixa estreita\né uma caixa para não levar a sério",
align = (:right, :top), fontsize = 11, color = INK2)
ylims!(axb, 0.5, 4.3)
# (c) letter-value plot — n grande -------------------------------------------
Random.seed!(9)
grande = rand(LogNormal(3.0, 0.62), 20_000)
sg = cinco_numeros(grande)
axc = Axis(fig3[2, 3]; backgroundcolor = SURF,
title = "(c) letter-value plot: n = 20.000", titlesize = 14,
titlealign = :left, titlecolor = INK,
yticks = ([1, 2], ["letter-value", "boxplot"]), yticklabelsize = 12.5,
yticklabelcolor = INK2, xticklabelsize = 11, xticklabelcolor = INK2,
xgridcolor = GRID, ygridvisible = false, xlabel = "valor",
xlabelsize = 12, xlabelcolor = INK2)
boxplot_h!(axc, sg, 2.0, 0.22)
lv = letter_values(grande)
rampa = [BLUE_D, parse(Colorant, "#256abf"), BLUE, parse(Colorant, "#5598e7"),
parse(Colorant, "#86b6ef"), BLUE_M, BLUE_L, parse(Colorant, "#e7f1fd")]
for i in length(lv.depths):-1:1
h = 0.30 * (1 - 0.085 * (i - 1))
poly!(axc, Rect2f(lv.lo[i], 1 - h, lv.hi[i] - lv.lo[i], 2h);
color = rampa[min(i, end)], strokecolor = SURF, strokewidth = 1)
end
letras = ["M", "F", "E", "D", "C", "B", "A"]
for i in 1:min(4, length(lv.depths))
text!(axc, lv.hi[i], 1.34; text = letras[i], align = (:center, :bottom),
fontsize = 11, color = INK2)
end
text!(axc, quantile(grande, 0.999), 0.62;
text = "o boxplot marca centenas de pontos como \"outliers\";\n" *
"as letras (M, F, E, D…) mostram a cauda\nem vez de descartá-la",
align = (:right, :top), fontsize = 10.5, color = INK2)
ylims!(axc, 0.02, 2.55); xlims!(axc, 0, quantile(grande, 0.999))
for a in (axa, axb, axc)
hidespines!(a, :t, :r, :l); a.bottomspinecolor = GRID
a.xtickcolor = MUTED; a.xticksize = 4
end
Label(fig3[3, 1:3],
"(a) e (b): McGill, Tukey & Larsen (1978) — notch ≈ mediana ± 1,58·IQR/√n. " *
"(c): Hofmann, Wickham & Kafadar (2017).";
fontsize = 11, color = MUTED, halign = :left)
rowgap!(fig3.layout, 1, 14); rowgap!(fig3.layout, 2, 12); colgap!(fig3.layout, 30)
save(joinpath(OUT, "boxplot-variantes.png"), fig3)
save(joinpath(OUT, "boxplot-variantes.pdf"), fig3)
# =============================================================================
# Números para o texto do artigo
# =============================================================================
println("\n── Figura 1: cinco números (tempo de atendimento, n = $(s.n)) ──")
@printf("mínimo %6.1f\n", minimum(dados))
@printf("valor adj. inferior %6.1f (ponta do bigode esquerdo)\n", s.adj_inf)
@printf("Q1 %6.1f\n", s.q1)
@printf("mediana (Q2) %6.1f\n", s.q2)
@printf("Q3 %6.1f\n", s.q3)
@printf("IQR %6.1f\n", s.iqr)
@printf("cerca inferior %6.1f (Q1 − 1,5·IQR)\n", s.cerca_inf)
@printf("cerca superior %6.1f (Q3 + 1,5·IQR)\n", s.cerca_sup)
@printf("valor adj. superior %6.1f (ponta do bigode direito)\n", s.adj_sup)
@printf("máximo %6.1f\n", maximum(dados))
@printf("outliers %6d %s\n", length(s.outliers), string(sort(s.outliers)))
@printf("assimetria %6.2f (média %.1f > mediana %.1f)\n",
skewness(dados), mean(dados), s.q2)
println("\n── Figura 2: mediana e IQR por construção iguais ──")
for (nome, v) in amostras
si = cinco_numeros(v)
@printf("%-12s mediana %.2f IQR %.2f bigodes [%.2f, %.2f] outliers %d\n",
nome, si.q2, si.iqr, si.adj_inf, si.adj_sup, length(si.outliers))
end
println("\nFiguras salvas em: $OUT")
# =============================================================================
# FIGURA 4 — capa do post (1200 × 630, proporção de card social)
# =============================================================================
figc = Figure(size = (1200, 630), backgroundcolor = SURF,
figure_padding = (56, 56, 44, 40))
axk = Axis(figc[1, 1]; backgroundcolor = SURF)
hidespines!(axk); hidedecorations!(axk)
xlims!(axk, 4.0, 53.0); ylims!(axk, 0, 1)
boxplot_h!(axk, s, 0.40, 0.115)
for (v, lab) in ((s.q1, "Q1"), (s.q2, "mediana"), (s.q3, "Q3"))
guia!(axk, v, 0.53, 0.585)
text!(axk, v, 0.60; text = lab, align = (:center, :bottom), fontsize = 21,
color = v == s.q2 ? BLUE_D : INK2, font = v == s.q2 ? :bold : :regular)
end
seta_dupla!(axk, s.q1, s.q3, 0.20; ms = 12)
text!(axk, (s.q1 + s.q3) / 2, 0.17; text = "IQR", align = (:center, :top),
fontsize = 20, color = INK2)
text!(axk, mean(s.outliers), 0.46; text = "outliers", align = (:center, :bottom),
fontsize = 19, color = RED)
for v in (s.adj_inf, s.adj_sup)
text!(axk, v, 0.47; text = "bigode", align = (:center, :bottom), fontsize = 17,
color = MUTED)
end
text!(axk, 4.0, 0.99; text = "Anatomia de um boxplot", align = (:left, :top),
fontsize = 36, font = :bold, color = INK)
text!(axk, 4.0, 0.845;
text = "Q1, mediana, Q3, IQR, bigodes e outliers — o que cada elemento significa",
align = (:left, :top), fontsize = 19, color = INK2)
save(joinpath(OUT, "boxplot-capa.png"), figc)
as figuras são desenhadas com primitivas (
poly!, lines!, scatter!) em vez da receita pronta de boxplot. Dá mais trabalho na primeira vez, mas é o que permite anotar cada elemento, controlar a altura de cada caixa e implementar variantes que a receita padrão não oferece.
12. Conclusão
O boxplot é um resumo de cinco números desenhado de forma resistente a valores extremos. Cada elemento tem um significado preciso:
- a caixa vai de Q1 a Q3 e contém os 50% centrais;
- a linha interna é a mediana, não a média;
- o IQR é a largura da caixa e mede dispersão sem se deixar levar por um único valor absurdo;
- as cercas são limiares calculados a 1,5 · IQR dos quartis;
- os bigodes param no último dado antes da cerca — não no mínimo, não no máximo, não em um desvio-padrão;
- os pontos soltos são observações incomuns em relação ao IQR desta amostra, e nada mais que isso.
E, como todo resumo, ele tem um preço: quatro distribuições radicalmente diferentes podem produzir a mesma caixa. Sobrepor os dados custa uma linha de código e devolve o que o resumo tirou.
O boxplot responde "onde estão os dados típicos e quão espalhados eles são" com resistência a extremos — mas não responde "que forma a distribuição tem", e é por isso que ele quase sempre pede companhia.
Referências
-
Tukey, J. W. — Exploratory Data Analysis. Addison-Wesley, 1977. (Obra que introduz o schematic plot, os hinges e a regra de 1,5 · IQR.)
-
McGill, R.; Tukey, J. W.; Larsen, W. A. — Variations of Box Plots. The American Statistician, 32(1), 12–16, 1978. DOI: 10.1080/00031305.1978.10479236.
https://www.tandfonline.com/doi/abs/10.1080/00031305.1978.10479236 -
Frigge, M.; Hoaglin, D. C.; Iglewicz, B. — Some Implementations of the Boxplot. The American Statistician, 43(1), 50–54, 1989. DOI: 10.1080/00031305.1989.10475612.
https://www.tandfonline.com/doi/abs/10.1080/00031305.1989.10475612 -
Hyndman, R. J.; Fan, Y. — Sample Quantiles in Statistical Packages. The American Statistician, 50(4), 361–365, 1996. DOI: 10.1080/00031305.1996.10473566.
https://robjhyndman.com/papers/sample_quantiles.pdf -
Hintze, J. L.; Nelson, R. D. — Violin Plots: A Box Plot-Density Trace Synergism. The American Statistician, 52(2), 181–184, 1998. DOI: 10.1080/00031305.1998.10480559.
https://www.tandfonline.com/doi/abs/10.1080/00031305.1998.10480559 -
Wickham, H.; Stryjewski, L. — 40 Years of Boxplots. Relatório técnico, 2011. (História do gráfico, incluindo o range bar de Mary Eleanor Spear, de 1952, e um catálogo de variantes.)
https://vita.had.co.nz/papers/boxplots.pdf -
Chambers, J. M.; Cleveland, W. S.; Kleiner, B.; Tukey, P. A. — Graphical Methods for Data Analysis. Wadsworth, 1983. (A constante do notch aparece na p. 62; é a base da implementação em
boxplot.statsdo R.)
https://stat.ethz.ch/R-manual/R-patched/library/grDevices/html/boxplot.stats.html -
Krzywinski, M.; Altman, N. — Visualizing samples with box plots. Nature Methods, 11(2), 119–120, 2014. DOI: 10.1038/nmeth.2813.
https://www.nature.com/articles/nmeth.2813 -
Hofmann, H.; Wickham, H.; Kafadar, K. — Letter-Value Plots: Boxplots for Large Data. Journal of Computational and Graphical Statistics, 26(3), 469–477, 2017. DOI: 10.1080/10618600.2017.1305277.
https://vita.had.co.nz/papers/letter-value-plot.pdf -
Anscombe, F. J. — Graphs in Statistical Analysis. The American Statistician, 27(1), 17–21, 1973. DOI: 10.1080/00031305.1973.10478966.
-
Julia — Statistics Standard Library: documentação de
quantile,mediane demais funções usadas no código.
https://docs.julialang.org/en/v1/stdlib/Statistics/ -
Makie.jl — CairoMakie: documentação oficial do backend usado para gerar as figuras.
https://docs.makie.org/stable/explanations/backends/cairomakie
Comentários