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

  1. 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.

  2. 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()
    
  3. Escreva o código para criar um histograma da variável anos_experiencia do 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()
    
  4. 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.

  5. 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) ou np.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.