Fase 3 — Visualização e comunicação | Pré-requisitos: Aulas 0 a 14 | Duração estimada: 80 minutos
Introdução
Análise univariada é o estudo de uma variável por vez. Antes de investigar relações entre variáveis, você precisa entender cada uma individualmente: qual é sua distribuição, onde está o centro, quão dispersos são os valores, há outliers, a distribuição é simétrica ou assimétrica.
A Aula 6 introduziu visualizações básicas com pandas. Esta aula aprofunda com matplotlib e seaborn, que oferecem muito mais controle sobre aparência e mais tipos de gráficos. A diferença entre as duas bibliotecas é de nível de abstração: matplotlib é de baixo nível — você controla cada elemento explicitamente. seaborn é de alto nível — você descreve o gráfico em termos estatísticos e ele cuida da aparência.
Na prática os dois trabalham juntos: seaborn para criar o gráfico rapidamente, matplotlib para ajustar detalhes.
Configuração
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import seaborn as sns
# Estilo base — remover elementos desnecessários por padrão
sns.set_theme(style='ticks', palette='muted')
plt.rcParams.update({
'figure.dpi': 120,
'axes.spines.top': False,
'axes.spines.right': False,
'axes.grid': True,
'axes.grid.axis': 'y',
'grid.alpha': 0.3,
'grid.linewidth': 0.5,
'font.size': 11
})
# Dataset de exemplo — usaremos ao longo da aula
np.random.seed(42)
n = 1000
df = pd.DataFrame({
'salario': np.random.lognormal(mean=8.5, sigma=0.6, size=n),
'idade': np.random.normal(loc=38, scale=10, size=n).clip(18, 70).astype(int),
'anos_experiencia': np.random.exponential(scale=8, size=n).clip(0, 40).astype(int),
'departamento': np.random.choice(
['Vendas', 'TI', 'RH', 'Financeiro', 'Operações'],
size=n, p=[0.30, 0.25, 0.15, 0.20, 0.10]
),
'nivel': np.random.choice(
['Júnior', 'Pleno', 'Sênior', 'Gerente'],
size=n, p=[0.35, 0.30, 0.25, 0.10]
),
'satisfacao': np.random.choice(range(1, 6), size=n, p=[0.05, 0.10, 0.25, 0.40, 0.20])
})
Histograma
O histograma é o ponto de partida para qualquer variável numérica contínua. Ele mostra a distribuição de frequência dividindo o intervalo de valores em bins e contando quantas observações caem em cada bin.
fig, axes = plt.subplots(1, 2, figsize=(13, 4))
# Histograma básico com seaborn
sns.histplot(data=df, x='salario', bins=40, ax=axes[0],
color='steelblue', edgecolor='white', linewidth=0.5)
axes[0].set_title('Distribuição de salários')
axes[0].set_xlabel('Salário (R$)')
axes[0].set_ylabel('Frequência')
axes[0].xaxis.set_major_formatter(ticker.FuncFormatter(
lambda x, p: f'R$ {x/1000:.0f}k'
))
# Com curva de densidade sobreposta (KDE)
sns.histplot(data=df, x='salario', bins=40, ax=axes[1],
color='steelblue', edgecolor='white', linewidth=0.5,
kde=True, kde_kws={'linewidth': 2})
axes[1].set_title('Distribuição de salários com KDE')
axes[1].set_xlabel('Salário (R$)')
axes[1].set_ylabel('Densidade')
axes[1].xaxis.set_major_formatter(ticker.FuncFormatter(
lambda x, p: f'R$ {x/1000:.0f}k'
))
plt.tight_layout()
plt.show()
Adicionando linhas de referência
Linhas de referência — média, mediana, percentis — tornam o histograma muito mais informativo:
fig, ax = plt.subplots(figsize=(10, 5))
sns.histplot(data=df, x='salario', bins=40, ax=ax,
color='steelblue', edgecolor='white', linewidth=0.5, alpha=0.7)
media = df['salario'].mean()
mediana = df['salario'].median()
p25 = df['salario'].quantile(0.25)
p75 = df['salario'].quantile(0.75)
ax.axvline(media, color='red', linestyle='--', linewidth=1.5,
label=f'Média: R$ {media:,.0f}')
ax.axvline(mediana, color='green', linestyle='--', linewidth=1.5,
label=f'Mediana: R$ {mediana:,.0f}')
ax.axvline(p25, color='orange', linestyle=':', linewidth=1,
label=f'P25: R$ {p25:,.0f}')
ax.axvline(p75, color='orange', linestyle=':', linewidth=1,
label=f'P75: R$ {p75:,.0f}')
ax.legend(fontsize=9)
ax.set_title('Distribuição de salários — média vs. mediana')
ax.set_xlabel('Salário (R$)')
ax.set_ylabel('Frequência')
ax.xaxis.set_major_formatter(ticker.FuncFormatter(
lambda x, p: f'R$ {x/1000:.0f}k'
))
# Anotação da assimetria
skew = df['salario'].skew()
ax.text(0.98, 0.95, f'Assimetria: {skew:.2f}',
transform=ax.transAxes, ha='right', va='top',
fontsize=9, color='gray')
plt.tight_layout()
plt.show()
Escolhendo o número de bins
A escolha do número de bins afeta o que você vê. Regras automáticas existem mas nenhuma é universalmente boa:
fig, axes = plt.subplots(1, 4, figsize=(16, 3))
opcoes_bins = [10, 30, 60, 100]
for ax, bins in zip(axes, opcoes_bins):
sns.histplot(data=df, x='salario', bins=bins, ax=ax,
color='steelblue', edgecolor='white', linewidth=0.3)
ax.set_title(f'{bins} bins')
ax.set_xlabel('')
ax.set_ylabel('')
ax.xaxis.set_major_formatter(ticker.FuncFormatter(
lambda x, p: f'{x/1000:.0f}k'
))
axes[0].set_ylabel('Frequência')
fig.suptitle('Efeito do número de bins na leitura da distribuição', y=1.02)
plt.tight_layout()
plt.show()
Density plot (KDE)
O KDE (Kernel Density Estimate) é uma alternativa ao histograma que mostra uma curva de densidade suavizada em vez de barras. É especialmente útil para comparar distribuições de múltiplos grupos:
fig, axes = plt.subplots(1, 2, figsize=(13, 4))
# KDE simples
sns.kdeplot(data=df, x='salario', ax=axes[0],
fill=True, color='steelblue', alpha=0.4, linewidth=2)
axes[0].set_title('Densidade de salários')
axes[0].set_xlabel('Salário (R$)')
axes[0].set_ylabel('Densidade')
# KDE por grupo — comparação de distribuições
sns.kdeplot(data=df, x='salario', hue='nivel', ax=axes[1],
fill=True, alpha=0.25, linewidth=1.5,
palette='Set2')
axes[1].set_title('Distribuição de salários por nível')
axes[1].set_xlabel('Salário (R$)')
axes[1].set_ylabel('Densidade')
plt.tight_layout()
plt.show()
Boxplot
O boxplot representa graficamente o resumo de cinco números (mínimo, Q1, mediana, Q3, máximo) e marca pontos além de 1.5×IQR como outliers potenciais.
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
# Boxplot simples
sns.boxplot(data=df, y='salario', ax=axes[0],
color='steelblue', width=0.4,
flierprops=dict(marker='o', markersize=3, alpha=0.4))
axes[0].set_title('Boxplot de salários')
axes[0].set_ylabel('Salário (R$)')
axes[0].yaxis.set_major_formatter(ticker.FuncFormatter(
lambda x, p: f'R$ {x/1000:.0f}k'
))
# Boxplot por grupo (horizontal — facilita leitura de rótulos)
ordem = df.groupby('departamento')['salario'].median().sort_values(ascending=False).index
sns.boxplot(data=df, y='departamento', x='salario', ax=axes[1],
order=ordem, palette='muted', width=0.5,
flierprops=dict(marker='o', markersize=2, alpha=0.3))
axes[1].set_title('Salários por departamento')
axes[1].set_xlabel('Salário (R$)')
axes[1].set_ylabel('')
axes[1].xaxis.set_major_formatter(ticker.FuncFormatter(
lambda x, p: f'R$ {x/1000:.0f}k'
))
# Boxplot com pontos sobrepostos (strip plot)
sns.boxplot(data=df[df['nivel'].isin(['Júnior', 'Sênior'])],
y='nivel', x='salario', ax=axes[2],
palette='muted', width=0.4,
flierprops=dict(alpha=0))
sns.stripplot(data=df[df['nivel'].isin(['Júnior', 'Sênior'])],
y='nivel', x='salario', ax=axes[2],
alpha=0.3, size=3, color='gray', jitter=True)
axes[2].set_title('Júnior vs. Sênior com pontos')
axes[2].set_xlabel('Salário (R$)')
axes[2].set_ylabel('')
plt.tight_layout()
plt.show()
Violin plot
O violin plot combina boxplot com KDE, mostrando a distribuição completa enquanto mantém as marcações de quartis:
fig, ax = plt.subplots(figsize=(10, 5))
ordem = df.groupby('departamento')['salario'].median().sort_values(ascending=False).index
sns.violinplot(data=df, y='departamento', x='salario', ax=ax,
order=ordem, palette='muted',
inner='quartile', # mostra quartis dentro do violin
cut=0) # não extrapola além dos dados
ax.set_title('Distribuição de salários por departamento')
ax.set_xlabel('Salário (R$)')
ax.set_ylabel('')
ax.xaxis.set_major_formatter(ticker.FuncFormatter(
lambda x, p: f'R$ {x/1000:.0f}k'
))
plt.tight_layout()
plt.show()
O violin plot é especialmente útil quando você quer ver se uma distribuição é unimodal, bimodal ou tem forma incomum — informação que o boxplot não mostra.
Variáveis categóricas: gráfico de barras e countplot
Para variáveis categóricas, o gráfico de barras de contagem é o equivalente ao histograma:
fig, axes = plt.subplots(1, 2, figsize=(13, 4))
# Countplot simples — frequência absoluta
ordem_dept = df['departamento'].value_counts().index
sns.countplot(data=df, x='departamento', ax=axes[0],
order=ordem_dept, palette='muted',
edgecolor='white', linewidth=0.5)
axes[0].set_title('Funcionários por departamento')
axes[0].set_xlabel('')
axes[0].set_ylabel('Número de funcionários')
# Adicionar rótulos nas barras
for container in axes[0].containers:
axes[0].bar_label(container, fontsize=9)
# Frequência relativa com barplot
freq_rel = df['satisfacao'].value_counts(normalize=True).sort_index() * 100
ax = axes[1]
barras = ax.bar(freq_rel.index, freq_rel.values, color='steelblue',
edgecolor='white', linewidth=0.5)
ax.set_title('Distribuição de satisfação dos funcionários')
ax.set_xlabel('Nota de satisfação')
ax.set_ylabel('Porcentagem (%)')
ax.set_xticks(freq_rel.index)
ax.set_xticklabels(['1\n(Muito insatisfeito)', '2', '3', '4', '5\n(Muito satisfeito)'])
# Rótulos nas barras
for barra in barras:
altura = barra.get_height()
ax.text(barra.get_x() + barra.get_width()/2, altura + 0.3,
f'{altura:.1f}%', ha='center', va='bottom', fontsize=9)
plt.tight_layout()
plt.show()
ECDF — Função de distribuição acumulada empírica
O ECDF é uma alternativa ao histograma que não depende da escolha de bins. Para cada valor x, mostra a proporção de observações menores ou iguais a x. É especialmente útil para comparar distribuições:
fig, ax = plt.subplots(figsize=(10, 5))
# ECDF por nível
niveis = ['Júnior', 'Pleno', 'Sênior', 'Gerente']
cores = sns.color_palette('Set2', len(niveis))
for nivel, cor in zip(niveis, cores):
dados = df[df['nivel'] == nivel]['salario'].sort_values()
ecdf = np.arange(1, len(dados) + 1) / len(dados)
ax.plot(dados, ecdf, linewidth=2, label=nivel, color=cor)
ax.set_title('Distribuição acumulada de salários por nível')
ax.set_xlabel('Salário (R$)')
ax.set_ylabel('Proporção acumulada')
ax.xaxis.set_major_formatter(ticker.FuncFormatter(
lambda x, p: f'R$ {x/1000:.0f}k'
))
# Linhas de referência em percentis importantes
for pct in [0.25, 0.50, 0.75]:
ax.axhline(pct, color='gray', linestyle=':', linewidth=0.8, alpha=0.5)
ax.text(ax.get_xlim()[1] * 0.98, pct + 0.01,
f'P{int(pct*100)}', va='bottom', ha='right', fontsize=8, color='gray')
ax.legend(title='Nível', fontsize=9)
plt.tight_layout()
plt.show()
Tabela de estatísticas descritivas formatada
Uma visualização univariada completa inclui não apenas o gráfico mas também uma tabela de estatísticas descritivas legível:
def estatisticas_descritivas(df, col, nome_coluna=None):
"""
Gera tabela formatada de estatísticas descritivas para uma coluna.
"""
s = df[col].dropna()
nome = nome_coluna or col
stats = {
'n': len(s),
'ausentes': df[col].isnull().sum(),
'média': s.mean(),
'mediana': s.median(),
'desvio padrão': s.std(),
'mínimo': s.min(),
'P25': s.quantile(0.25),
'P75': s.quantile(0.75),
'máximo': s.max(),
'assimetria': s.skew(),
'curtose': s.kurt()
}
print(f"=== Estatísticas descritivas: {nome} ===")
for stat, valor in stats.items():
if stat in ['n', 'ausentes']:
print(f" {stat:20s}: {valor:,.0f}")
elif stat in ['assimetria', 'curtose']:
print(f" {stat:20s}: {valor:.3f}")
else:
print(f" {stat:20s}: R$ {valor:,.2f}" if 'salario' in col else
f" {stat:20s}: {valor:.2f}")
estatisticas_descritivas(df, 'salario', 'Salário')
Painel de exploração univariada completo
Juntando tudo num painel que combina histograma, boxplot horizontal e estatísticas:
def explorar_variavel_numerica(df, col, titulo=None, unidade='', n_bins=40):
"""
Gera um painel completo de exploração univariada para uma variável numérica.
"""
s = df[col].dropna()
titulo = titulo or col
fig = plt.figure(figsize=(14, 5))
gs = fig.add_gridspec(1, 3, width_ratios=[3, 1, 1.2])
ax_hist = fig.add_subplot(gs[0])
ax_box = fig.add_subplot(gs[1])
ax_stats = fig.add_subplot(gs[2])
# Histograma com KDE
sns.histplot(s, bins=n_bins, ax=ax_hist,
color='steelblue', edgecolor='white', linewidth=0.3,
kde=True, kde_kws={'linewidth': 2, 'color': 'navy'})
ax_hist.axvline(s.mean(), color='red', linestyle='--', linewidth=1.5,
label=f'Média: {s.mean():,.1f}{unidade}')
ax_hist.axvline(s.median(), color='green', linestyle='--', linewidth=1.5,
label=f'Mediana: {s.median():,.1f}{unidade}')
ax_hist.legend(fontsize=9)
ax_hist.set_title(f'Distribuição de {titulo}', fontsize=12)
ax_hist.set_xlabel(f'{titulo} ({unidade})' if unidade else titulo)
ax_hist.set_ylabel('Frequência')
# Boxplot
sns.boxplot(y=s, ax=ax_box, color='steelblue', width=0.4,
flierprops=dict(marker='o', markersize=3, alpha=0.4))
ax_box.set_ylabel('')
ax_box.set_xticks([])
ax_box.set_title('Boxplot')
# Tabela de estatísticas
ax_stats.axis('off')
stats_texto = [
('n', f'{len(s):,}'),
('Média', f'{s.mean():,.1f}'),
('Mediana', f'{s.median():,.1f}'),
('Std', f'{s.std():,.1f}'),
('Mín', f'{s.min():,.1f}'),
('P25', f'{s.quantile(0.25):,.1f}'),
('P75', f'{s.quantile(0.75):,.1f}'),
('Máx', f'{s.max():,.1f}'),
('Assimetria', f'{s.skew():.2f}'),
('Curtose', f'{s.kurt():.2f}'),
]
for i, (label, valor) in enumerate(stats_texto):
y_pos = 1 - i * 0.1
ax_stats.text(0.05, y_pos, label, transform=ax_stats.transAxes,
fontsize=9, color='gray', va='top')
ax_stats.text(0.95, y_pos, valor, transform=ax_stats.transAxes,
fontsize=9, ha='right', va='top', fontweight='bold')
ax_stats.set_title('Estatísticas', fontsize=10)
plt.tight_layout()
plt.show()
# Usar a função
explorar_variavel_numerica(df, 'salario', titulo='Salário', unidade='R$')
explorar_variavel_numerica(df, 'idade', titulo='Idade', unidade='anos')
Explorando variáveis categóricas em profundidade
Para variáveis categóricas, a exploração vai além de contar frequências:
def explorar_variavel_categorica(df, col, titulo=None, top_n=15):
"""
Gera painel de exploração para variável categórica.
"""
s = df[col].dropna()
titulo = titulo or col
n_unicos = s.nunique()
fig, axes = plt.subplots(1, 2, figsize=(13, max(4, min(n_unicos, top_n) * 0.35 + 2)))
# Frequência absoluta (barras horizontais, ordenadas)
freq = s.value_counts().head(top_n)
cores = ['steelblue'] * len(freq)
axes[0].barh(freq.index[::-1], freq.values[::-1], color=cores[::-1],
edgecolor='white', linewidth=0.5)
axes[0].set_title(f'{titulo} — frequência absoluta')
axes[0].set_xlabel('Contagem')
for i, (idx, v) in enumerate(zip(freq.index[::-1], freq.values[::-1])):
axes[0].text(v + freq.max() * 0.01, i, f'{v:,}',
va='center', fontsize=9)
# Frequência relativa (pie ou barras normalizadas)
freq_rel = (s.value_counts(normalize=True) * 100).head(top_n)
axes[1].barh(freq_rel.index[::-1], freq_rel.values[::-1],
color='steelblue', alpha=0.8, edgecolor='white', linewidth=0.5)
axes[1].set_title(f'{titulo} — frequência relativa')
axes[1].set_xlabel('Porcentagem (%)')
axes[1].set_xlim(0, freq_rel.max() * 1.15)
for i, (idx, v) in enumerate(zip(freq_rel.index[::-1], freq_rel.values[::-1])):
axes[1].text(v + freq_rel.max() * 0.01, i, f'{v:.1f}%',
va='center', fontsize=9)
# Nota sobre total e ausentes
n_ausentes = df[col].isnull().sum()
fig.text(0.5, -0.02,
f'Total: {len(s):,} | Categorias únicas: {n_unicos} | '
f'Ausentes: {n_ausentes} ({n_ausentes/len(df)*100:.1f}%)',
ha='center', fontsize=9, color='gray')
plt.tight_layout()
plt.show()
explorar_variavel_categorica(df, 'departamento', titulo='Departamento')
explorar_variavel_categorica(df, 'nivel', titulo='Nível')
Comparando distribuições de múltiplos grupos
Quando você quer comparar a mesma variável entre grupos, há quatro opções principais:
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
# 1. Histogramas sobrepostos com transparência
for nivel in df['nivel'].unique():
dados = df[df['nivel'] == nivel]['salario']
axes[0, 0].hist(dados, bins=30, alpha=0.4, label=nivel, edgecolor='none')
axes[0, 0].set_title('Histogramas sobrepostos')
axes[0, 0].set_xlabel('Salário (R$)')
axes[0, 0].set_ylabel('Frequência')
axes[0, 0].legend(fontsize=9)
# 2. KDE por grupo (mais limpo que histogramas sobrepostos)
sns.kdeplot(data=df, x='salario', hue='nivel', ax=axes[0, 1],
fill=True, alpha=0.2, linewidth=1.5, palette='Set2')
axes[0, 1].set_title('KDE por grupo')
axes[0, 1].set_xlabel('Salário (R$)')
axes[0, 1].set_ylabel('Densidade')
# 3. Boxplot por grupo
ordem = df.groupby('nivel')['salario'].median().sort_values().index
sns.boxplot(data=df, x='nivel', y='salario', ax=axes[1, 0],
order=ordem, palette='Set2',
flierprops=dict(marker='o', markersize=2, alpha=0.3))
axes[1, 0].set_title('Boxplot por grupo')
axes[1, 0].set_xlabel('')
axes[1, 0].set_ylabel('Salário (R$)')
# 4. Violin plot por grupo
sns.violinplot(data=df, x='nivel', y='salario', ax=axes[1, 1],
order=ordem, palette='Set2', inner='quartile', cut=0)
axes[1, 1].set_title('Violin plot por grupo')
axes[1, 1].set_xlabel('')
axes[1, 1].set_ylabel('Salário (R$)')
for ax in axes.flat:
ax.yaxis.set_major_formatter(ticker.FuncFormatter(
lambda x, p: f'R$ {x/1000:.0f}k'
))
plt.tight_layout()
plt.show()
Resumo
Análise univariada estuda uma variável por vez antes de investigar relações. Para variáveis numéricas, o histograma revela distribuição e forma, o KDE suaviza sem dependência de bins, o boxplot resume quartis e outliers, o violin plot combina os dois, e o ECDF é robusto à escolha de parâmetros. Para variáveis categóricas, o gráfico de barras de contagem com frequência absoluta e relativa é a ferramenta central. Linhas de referência de média e mediana no histograma imediatamente revelam assimetria. Para comparar grupos, KDE sobreposto ou violin plot são geralmente melhores que histogramas sobrepostos. seaborn facilita a criação de gráficos estatísticos com menos código; matplotlib fornece controle fino sobre cada elemento.
Exercícios
-
Qual a diferença entre histograma e KDE para representar a distribuição de uma variável numérica? Em quais situações cada um é preferível?
✓ Resposta:O histograma divide o intervalo de valores em bins de largura fixa e conta observações em cada bin. O resultado é um gráfico de barras que representa frequência ou densidade por intervalo. Sua principal limitação é que a aparência depende da escolha do número e posição dos bins — escolhas diferentes podem revelar ou esconder estrutura.
O KDE (Kernel Density Estimate) posiciona uma curva suave em torno de cada ponto de dado e soma essas curvas para criar uma estimativa contínua da densidade. Não depende de bins, mas tem um parâmetro de suavização (bandwidth) que controla quanta estrutura é visível — bandwidth muito pequeno cria muitos picos, bandwidth muito grande suaviza demais.
O histograma é preferível quando: você quer mostrar frequências absolutas (contagem de observações por intervalo), o público está menos familiarizado com densidade probabilística, ou você tem amostras pequenas onde o KDE pode criar a impressão de suavidade que não existe nos dados.
O KDE é preferível quando: você quer comparar distribuições de múltiplos grupos no mesmo gráfico (linhas KDE sobrepostas são mais limpas que histogramas sobrepostos), você quer mostrar a forma geral da distribuição sem a granularidade de bins, ou você está trabalhando com amostras grandes onde o histograma tem muitas barras de altura similar.
-
Por que o violin plot pode ser mais informativo que o boxplot em certas situações? Dê um exemplo de caso em que o boxplot seria enganoso e o violin plot revelaria a realidade.
✓ Resposta:O boxplot resume uma distribuição em cinco números e marca outliers, mas não mostra a forma da distribuição entre esses pontos. Duas distribuições muito diferentes podem ter boxplots quase idênticos se compartilharem os mesmos quartis.
Exemplo concreto de boxplot enganoso: imagine uma variável de renda com distribuição bimodal — a maioria das pessoas ganha entre R$ 2.000 e R$ 4.000, mas há um segundo grupo que ganha entre R$ 15.000 e R$ 20.000. O boxplot vai mostrar uma mediana baixa, uma caixa estreita cobrindo os percentis 25-75 do grupo principal, e uma série de pontos marcados como "outliers" representando o grupo de alta renda. A leitura visual é "distribuição típica com alguns outliers extremos". O violin plot mostraria dois picos claramente distintos, revelando que há dois grupos estruturalmente diferentes — não uma distribuição normal com outliers.
Outro caso clássico: distribuições com formato de U (bimodais nos extremos) ou distribuições uniformes têm boxplots que se parecem com distribuições normais, porque os quartis não capturam a forma.
# Demonstração: dois datasets com boxplot similar mas distribuições diferentes np.random.seed(42) normal = np.random.normal(50, 10, 500) bimodal = np.concatenate([np.random.normal(35, 5, 250), np.random.normal(65, 5, 250)]) fig, axes = plt.subplots(1, 2, figsize=(12, 4)) # Boxplots — parecem diferentes mas não revelam a bimodalidade claramente dados_box = pd.DataFrame({'normal': normal, 'bimodal': bimodal}) dados_box.boxplot(ax=axes[0]) axes[0].set_title('Boxplot — bimodalidade invisível') # Violin plots — revelam a diferença estrutural axes[1].violinplot([normal, bimodal], positions=[1, 2], showmedians=True) axes[1].set_xticks([1, 2]) axes[1].set_xticklabels(['normal', 'bimodal']) axes[1].set_title('Violin plot — bimodalidade visível') plt.tight_layout() plt.show() -
Escreva o código para criar um histograma da variável
anos_experienciado dataset da aula com as seguintes características: 20 bins, cor cinza-azulada, bordas brancas, linha vertical vermelha tracejada na mediana com rótulo, linha vertical verde tracejada na média com rótulo, título descritivo e rótulos de eixo com unidade.✓ Resposta:import matplotlib.pyplot as plt import seaborn as sns fig, ax = plt.subplots(figsize=(10, 5)) sns.histplot(data=df, x='anos_experiencia', bins=20, ax=ax, color='#5B8DB8', edgecolor='white', linewidth=0.8) mediana = df['anos_experiencia'].median() media = df['anos_experiencia'].mean() ax.axvline(mediana, color='red', linestyle='--', linewidth=2, label=f'Mediana: {mediana:.1f} anos') ax.axvline(media, color='green', linestyle='--', linewidth=2, label=f'Média: {media:.1f} anos') ax.set_title('Distribuição de anos de experiência dos funcionários', fontsize=13, pad=12) ax.set_xlabel('Anos de experiência', fontsize=11) ax.set_ylabel('Número de funcionários', fontsize=11) ax.legend(fontsize=10) ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False) ax.grid(axis='y', alpha=0.3, linewidth=0.5) plt.tight_layout() plt.show() -
Qual é o ECDF e por que ele é considerado robusto à escolha de parâmetros? Em que situação você preferiria um ECDF a um histograma?
✓ Resposta:O ECDF (Empirical Cumulative Distribution Function) para cada valor x no eixo horizontal mostra a proporção de observações que são menores ou iguais a x. Formalmente, para n observações ordenadas x₁ ≤ x₂ ≤ ... ≤ xₙ, o ECDF em xᵢ é i/n.
Ele é robusto à escolha de parâmetros porque não tem parâmetros. O histograma depende do número de bins e da posição de início dos bins — duas escolhas que afetam o que você vê. O KDE depende do bandwidth. O ECDF não tem nenhuma dessas escolhas: para cada ponto de dado existe exatamente um degrau na curva, de tamanho 1/n. O resultado é determinístico e não depende de decisões de suavização.
Situações em que preferiria o ECDF ao histograma:
Quando você quer responder perguntas precisas sobre percentis. "Qual porcentagem dos funcionários ganha menos de R$ 5.000?" é lida diretamente do ECDF — você encontra R$ 5.000 no eixo x e lê o valor no eixo y. No histograma, você teria que estimar somando barras.
Quando você está comparando duas ou mais distribuições e quer ver diferenças em toda a faixa de valores, não apenas nos picos. Duas curvas ECDF que divergem muito nas caudas revelam diferenças nas extremidades que podem ser invisíveis em histogramas sobrepostos.
Quando a amostra é pequena. Com 30 ou 50 pontos, um histograma é muito sensível à escolha de bins. O ECDF mostra exatamente os dados sem suavização artificial.
-
Você tem um dataset de preços de imóveis com forte assimetria à direita — a maioria dos imóveis custa entre R$ 200.000 e R$ 800.000, mas há alguns imóveis de alto padrão que chegam a R$ 15.000.000. Descreva como você criaria um painel de exploração univariada para essa variável, quais transformações consideraria e por que o histograma com escala linear seria problemático.
✓ Resposta:O histograma com escala linear seria problemático porque os imóveis caros — poucos em número mas com valores até 20 vezes maiores que a maioria — comprimiriam toda a distribuição da maioria dos imóveis em poucos bins à esquerda, com a maior parte do eixo x vazio. A distribuição dos imóveis típicos, que é a parte mais relevante para a maioria das análises, ficaria invisível como um amontoado de barras altas à esquerda.
Para criar um painel útil, consideraria as seguintes abordagens.
Transformação logarítmica: aplicar
np.log10(preco)ounp.log1p(preco)antes de plotar transforma a distribuição lognormal em algo próximo de uma normal, tornando a distribuição legível numa escala linear. O eixo x teria rótulos como "100k", "1M", "10M" em escala log.fig, axes = plt.subplots(1, 3, figsize=(15, 4)) # Escala linear — problemática sns.histplot(df['preco'], bins=50, ax=axes[0], color='steelblue') axes[0].set_title('Escala linear (problemática)') # Escala logarítmica no eixo x sns.histplot(df['preco'], bins=50, ax=axes[1], color='steelblue', log_scale=True) axes[1].set_title('Eixo X em escala log') # Transformação log dos valores sns.histplot(np.log10(df['preco']), bins=40, ax=axes[2], color='steelblue') axes[2].set_title('log₁₀(preço)') axes[2].set_xlabel('log₁₀(preço em R$)') # Converter ticks de volta para escala original ticks = [5, 5.5, 6, 6.5, 7, 7.5] axes[2].set_xticks(ticks) axes[2].set_xticklabels([f'R$ {10**t/1e6:.1f}M' if 10**t >= 1e6 else f'R$ {10**t/1e3:.0f}k' for t in ticks]) plt.tight_layout() plt.show()O painel completo incluiria: histograma em escala log para a distribuição geral, boxplot para visualizar outliers extremos, e uma nota sobre a assimetria com os valores de média, mediana e percentil 95 — que revelariam o quanto os imóveis de luxo puxam a média para cima em relação à mediana.
Referências
- Seaborn documentation: seaborn.pydata.org. Referência completa de todos os tipos de gráficos estatísticos com exemplos e parâmetros.
- Matplotlib documentation: matplotlib.org/stable/index.html Referência do sistema de baixo nível, incluindo controle de eixos, formatadores de escala e anotações.
- Wilke, Claus O. Fundamentals of Data Visualization. O'Reilly, 2019. Capítulos 7 a 9 cobrem visualizações de distribuição em profundidade, com discussão de quando usar cada tipo. Disponível em clauswilke.com/dataviz
- Tukey, John W. Exploratory Data Analysis. Addison-Wesley, 1977. O livro que criou o boxplot e estabeleceu a EDA como disciplina. Os capítulos sobre distribuições de uma variável continuam relevantes.
- VanderPlas, Jake. Python Data Science Handbook. O'Reilly, 2016. Capítulo 4 cobre matplotlib com profundidade, incluindo o sistema de figura e eixos. Disponível em jakevdp.github.io/PythonDataScienceHandbook
- Waskom, Michael. "Seaborn: statistical data visualization". Journal of Open Source Software, 2021. O artigo de referência do seaborn, descrevendo a filosofia de design da biblioteca.