Fase 3 — Visualização e comunicação | Pré-requisitos: Aulas 0 a 15 | Duração estimada: 85 minutos


Introdução

Análise univariada descreve variáveis individualmente. Análise bivariada investiga relações entre pares de variáveis — se quando uma aumenta a outra também tende a aumentar, se grupos diferentes têm distribuições diferentes, se categorias se associam de formas específicas.

A natureza da relação que você pode investigar depende dos tipos das duas variáveis: numérica com numérica, numérica com categórica, ou categórica com categórica. Cada combinação tem ferramentas apropriadas.

Esta aula cobre as três combinações com as visualizações e métricas correspondentes.


Numérica com numérica: scatter plot

O scatter plot é a ferramenta fundamental para investigar relações entre duas variáveis numéricas. Cada ponto representa uma observação, com coordenadas x e y determinadas pelos valores das duas variáveis.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import seaborn as sns
from scipy import stats

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.alpha': 0.3,
    'axes.grid.linewidth': 0.5,
    'font.size': 11
})

np.random.seed(42)
n = 800

df = pd.DataFrame({
    'salario': np.random.lognormal(mean=8.5, sigma=0.5, size=n),
    'idade': np.random.normal(38, 10, n).clip(18, 70).astype(int),
    'anos_experiencia': np.random.exponential(8, n).clip(0, 40).astype(int),
    'departamento': np.random.choice(
        ['Vendas', 'TI', 'RH', 'Financeiro', 'Operações'],
        n, p=[0.30, 0.25, 0.15, 0.20, 0.10]
    ),
    'nivel': np.random.choice(
        ['Júnior', 'Pleno', 'Sênior', 'Gerente'],
        n, p=[0.35, 0.30, 0.25, 0.10]
    ),
    'satisfacao': np.random.choice(range(1, 6), n, p=[0.05, 0.10, 0.25, 0.40, 0.20]),
    'horas_extras': np.random.exponential(4, n).clip(0, 30).astype(int)
})

# Adicionar correlação entre salario e experiencia
df['salario'] = df['salario'] + df['anos_experiencia'] * 800

Scatter plot básico

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Scatter básico
axes[0].scatter(df['anos_experiencia'], df['salario'],
                alpha=0.4, s=20, color='steelblue', edgecolors='none')
axes[0].set_xlabel('Anos de experiência')
axes[0].set_ylabel('Salário (R$)')
axes[0].set_title('Salário vs. anos de experiência')
axes[0].yaxis.set_major_formatter(ticker.FuncFormatter(
    lambda x, p: f'R$ {x/1000:.0f}k'
))

# Com linha de tendência (regressão linear)
x = df['anos_experiencia']
y = df['salario']
slope, intercept, r_value, p_value, std_err = stats.linregress(x, y)
x_line = np.linspace(x.min(), x.max(), 100)
y_line = slope * x_line + intercept

axes[1].scatter(x, y, alpha=0.4, s=20, color='steelblue', edgecolors='none')
axes[1].plot(x_line, y_line, color='red', linewidth=2,
             label=f'Regressão linear\nr = {r_value:.2f}, p < 0.001')
axes[1].set_xlabel('Anos de experiência')
axes[1].set_ylabel('Salário (R$)')
axes[1].set_title('Com linha de tendência')
axes[1].legend(fontsize=9)
axes[1].yaxis.set_major_formatter(ticker.FuncFormatter(
    lambda x, p: f'R$ {x/1000:.0f}k'
))

plt.tight_layout()
plt.show()

Problema de overplotting

Com muitos pontos, o scatter plot sofre de overplotting: pontos se sobrepõem e a densidade real da distribuição fica invisível. Três soluções:

fig, axes = plt.subplots(1, 3, figsize=(15, 4))

# Solução 1: transparência
axes[0].scatter(df['idade'], df['salario'],
                alpha=0.1, s=15, color='steelblue', edgecolors='none')
axes[0].set_title('Transparência (alpha=0.1)')
axes[0].set_xlabel('Idade')
axes[0].set_ylabel('Salário (R$)')

# Solução 2: hexbin — divide o plano em hexágonos e conta pontos
hb = axes[1].hexbin(df['idade'], df['salario'],
                     gridsize=25, cmap='Blues', mincnt=1)
plt.colorbar(hb, ax=axes[1], label='Contagem')
axes[1].set_title('Hexbin (densidade)')
axes[1].set_xlabel('Idade')
axes[1].set_ylabel('Salário (R$)')

# Solução 3: KDE 2D
sns.kdeplot(data=df, x='idade', y='salario', ax=axes[2],
            fill=True, cmap='Blues', levels=8)
axes[2].set_title('KDE 2D (densidade)')
axes[2].set_xlabel('Idade')
axes[2].set_ylabel('Salário (R$)')

for ax in axes:
    ax.yaxis.set_major_formatter(ticker.FuncFormatter(
        lambda x, p: f'R$ {x/1000:.0f}k'
    ))

plt.tight_layout()
plt.show()

Scatter com contexto adicional

Adicionar uma terceira variável ao scatter como cor ou tamanho dos pontos cria uma visualização trivariada (tecnicamente bivariada com uma variável adicional codificada):

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Cor como terceira variável categórica
niveis = df['nivel'].unique()
cores = dict(zip(niveis, sns.color_palette('Set2', len(niveis))))
for nivel in niveis:
    mask = df['nivel'] == nivel
    axes[0].scatter(df.loc[mask, 'anos_experiencia'],
                    df.loc[mask, 'salario'],
                    alpha=0.5, s=20, label=nivel,
                    color=cores[nivel], edgecolors='none')
axes[0].set_xlabel('Anos de experiência')
axes[0].set_ylabel('Salário (R$)')
axes[0].set_title('Salário vs. experiência por nível')
axes[0].legend(title='Nível', fontsize=9, markerscale=1.5)
axes[0].yaxis.set_major_formatter(ticker.FuncFormatter(
    lambda x, p: f'R$ {x/1000:.0f}k'
))

# Usando seaborn scatterplot (mais simples para múltiplos atributos)
sns.scatterplot(data=df, x='anos_experiencia', y='salario',
                hue='nivel', style='nivel', alpha=0.6, s=30, ax=axes[1],
                palette='Set2')
axes[1].set_xlabel('Anos de experiência')
axes[1].set_ylabel('Salário (R$)')
axes[1].set_title('Com seaborn scatterplot')
axes[1].yaxis.set_major_formatter(ticker.FuncFormatter(
    lambda x, p: f'R$ {x/1000:.0f}k'
))

plt.tight_layout()
plt.show()

Correlação: quantificando a relação

O scatter plot mostra visualmente a relação. A correlação quantifica a força e direção dessa relação.

# Correlação de Pearson — para relações lineares
r_pearson, p_pearson = stats.pearsonr(
    df['anos_experiencia'], df['salario']
)

# Correlação de Spearman — para relações monotônicas, robusta a outliers
r_spearman, p_spearman = stats.spearmanr(
    df['anos_experiencia'], df['salario']
)

print(f"Pearson:  r = {r_pearson:.3f}, p = {p_pearson:.2e}")
print(f"Spearman: r = {r_spearman:.3f}, p = {p_spearman:.2e}")

# Matriz de correlação entre múltiplas variáveis
variaveis_numericas = ['salario', 'idade', 'anos_experiencia', 'satisfacao', 'horas_extras']
matriz_corr = df[variaveis_numericas].corr()
print("\nMatriz de correlação:")
print(matriz_corr.round(3))

Heatmap de correlação

fig, ax = plt.subplots(figsize=(8, 6))

mask = np.triu(np.ones_like(matriz_corr, dtype=bool), k=1)

sns.heatmap(
    matriz_corr,
    ax=ax,
    annot=True,
    fmt='.2f',
    cmap='RdBu_r',
    vmin=-1, vmax=1,
    center=0,
    square=True,
    linewidths=0.5,
    cbar_kws={'shrink': 0.8, 'label': 'Correlação de Pearson'}
)

ax.set_title('Matriz de correlação', fontsize=13, pad=15)
plt.tight_layout()
plt.show()

Interpretação do heatmap: vermelho escuro indica correlação positiva forte, azul escuro indica correlação negativa forte, branco ou próximo de branco indica correlação fraca ou inexistente.


Numérica com categórica

Quando você quer comparar a distribuição de uma variável numérica entre grupos definidos por uma variável categórica, as ferramentas são boxplot, violin plot, strip plot e barplot de médias.

Boxplot comparativo

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Ordenar por mediana para facilitar comparação
ordem_dept = (df.groupby('departamento')['salario']
              .median()
              .sort_values(ascending=False)
              .index)

# Boxplot horizontal com pontos sobrepostos
sns.boxplot(data=df, y='departamento', x='salario',
            order=ordem_dept, ax=axes[0], palette='muted',
            flierprops=dict(marker='o', markersize=2, alpha=0.3))
axes[0].set_title('Salários por departamento')
axes[0].set_xlabel('Salário (R$)')
axes[0].set_ylabel('')
axes[0].xaxis.set_major_formatter(ticker.FuncFormatter(
    lambda x, p: f'R$ {x/1000:.0f}k'
))

# Adicionar médias como pontos
medias = df.groupby('departamento')['salario'].mean()
for i, dept in enumerate(ordem_dept):
    axes[0].scatter(medias[dept], i, marker='D', color='red',
                   s=40, zorder=5, label='Média' if i == 0 else '')
axes[0].legend(fontsize=9)

# Strip plot com jitter para ver todos os pontos
ordem_nivel = ['Júnior', 'Pleno', 'Sênior', 'Gerente']
sns.stripplot(data=df, x='nivel', y='salario', order=ordem_nivel,
              ax=axes[1], alpha=0.3, size=3, palette='muted', jitter=True)
sns.pointplot(data=df, x='nivel', y='salario', order=ordem_nivel,
              ax=axes[1], color='red', markers='D', linestyles='-',
              errorbar='ci', capsize=0.1)
axes[1].set_title('Salários por nível (com IC 95%)')
axes[1].set_xlabel('')
axes[1].set_ylabel('Salário (R$)')
axes[1].yaxis.set_major_formatter(ticker.FuncFormatter(
    lambda x, p: f'R$ {x/1000:.0f}k'
))

plt.tight_layout()
plt.show()

Barplot com intervalos de confiança

O barplot de médias com intervalos de confiança é muito comum em relatórios corporativos, mas exige interpretação cuidadosa:

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Barplot com IC — seaborn calcula automaticamente
sns.barplot(data=df, x='departamento', y='salario',
            order=ordem_dept, ax=axes[0],
            palette='muted', capsize=0.1,
            errorbar='ci')
axes[0].set_title('Salário médio por departamento\n(com IC 95%)')
axes[0].set_xlabel('')
axes[0].set_ylabel('Salário médio (R$)')
axes[0].tick_params(axis='x', rotation=15)
axes[0].yaxis.set_major_formatter(ticker.FuncFormatter(
    lambda x, p: f'R$ {x/1000:.0f}k'
))

# Alternativa: calcular manualmente e plotar com barras de erro
stats_dept = df.groupby('departamento')['salario'].agg(['mean', 'std', 'count'])
stats_dept['se'] = stats_dept['std'] / np.sqrt(stats_dept['count'])
stats_dept['ic95'] = stats_dept['se'] * 1.96
stats_dept = stats_dept.sort_values('mean', ascending=False)

axes[1].bar(range(len(stats_dept)), stats_dept['mean'],
            yerr=stats_dept['ic95'],
            color='steelblue', alpha=0.7, capsize=5,
            error_kw={'linewidth': 1.5})
axes[1].set_xticks(range(len(stats_dept)))
axes[1].set_xticklabels(stats_dept.index, rotation=15)
axes[1].set_title('Cálculo manual do IC 95%')
axes[1].set_ylabel('Salário médio (R$)')
axes[1].yaxis.set_major_formatter(ticker.FuncFormatter(
    lambda x, p: f'R$ {x/1000:.0f}k'
))

plt.tight_layout()
plt.show()

Teste estatístico de diferença entre grupos

Visualizar a diferença é o primeiro passo. O segundo é quantificar se a diferença é estatisticamente significativa:

from scipy.stats import kruskal, mannwhitneyu

# Teste de Kruskal-Wallis — versão não-paramétrica de ANOVA
# Testa se as medianas de três ou mais grupos são iguais
grupos = [df[df['departamento'] == d]['salario'].values
          for d in df['departamento'].unique()]

stat, p_value = kruskal(*grupos)
print(f"Teste de Kruskal-Wallis: H = {stat:.2f}, p = {p_value:.4f}")
if p_value < 0.05:
    print("Há diferença significativa entre pelo menos dois departamentos (p < 0.05)")

# Comparação entre dois grupos específicos — Mann-Whitney U
ti = df[df['departamento'] == 'TI']['salario']
rh = df[df['departamento'] == 'RH']['salario']
stat_mw, p_mw = mannwhitneyu(ti, rh, alternative='two-sided')
print(f"\nTI vs. RH — Mann-Whitney U: p = {p_mw:.4f}")

Categórica com categórica

Quando as duas variáveis são categóricas, você está investigando se há associação entre elas — se as categorias de uma tendem a co-ocorrer com categorias específicas da outra.

Tabela de contingência e heatmap

# Tabela de contingência — frequências absolutas
tabela = pd.crosstab(df['departamento'], df['nivel'])
print("Tabela de contingência (frequências absolutas):")
print(tabela)

# Frequências relativas por linha (proporção dentro de cada departamento)
tabela_rel = pd.crosstab(df['departamento'], df['nivel'], normalize='index') * 100
print("\nFrequências relativas por departamento (%):")
print(tabela_rel.round(1))
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Heatmap de frequências absolutas
sns.heatmap(tabela, annot=True, fmt='d', cmap='Blues',
            linewidths=0.5, ax=axes[0], cbar_kws={'label': 'Contagem'})
axes[0].set_title('Contagem: departamento × nível')
axes[0].set_xlabel('Nível')
axes[0].set_ylabel('Departamento')

# Heatmap de frequências relativas por linha
ordem_niveis = ['Júnior', 'Pleno', 'Sênior', 'Gerente']
tabela_rel_ord = tabela_rel[ordem_niveis]
sns.heatmap(tabela_rel_ord, annot=True, fmt='.1f', cmap='Blues',
            linewidths=0.5, ax=axes[1], cbar_kws={'label': '% por departamento'})
axes[1].set_title('Proporção por departamento (%)')
axes[1].set_xlabel('Nível')
axes[1].set_ylabel('')

plt.tight_layout()
plt.show()

Barras empilhadas e agrupadas

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Barras empilhadas normalizadas — mostram proporção
tabela_rel_ord.plot(kind='bar', stacked=True, ax=axes[0],
                    colormap='Set2', edgecolor='white', linewidth=0.5)
axes[0].set_title('Composição por nível em cada departamento')
axes[0].set_xlabel('')
axes[0].set_ylabel('Proporção (%)')
axes[0].tick_params(axis='x', rotation=30)
axes[0].legend(title='Nível', bbox_to_anchor=(1.01, 1),
               loc='upper left', fontsize=9)

# Barras agrupadas — facilitam comparação entre grupos
tabela_rel_ord.plot(kind='bar', stacked=False, ax=axes[1],
                    colormap='Set2', edgecolor='white', linewidth=0.5, width=0.7)
axes[1].set_title('Distribuição de nível por departamento')
axes[1].set_xlabel('')
axes[1].set_ylabel('Proporção (%)')
axes[1].tick_params(axis='x', rotation=30)
axes[1].legend(title='Nível', bbox_to_anchor=(1.01, 1),
               loc='upper left', fontsize=9)

plt.tight_layout()
plt.show()

Teste qui-quadrado de independência

from scipy.stats import chi2_contingency

chi2, p, dof, expected = chi2_contingency(tabela)
print(f"Teste qui-quadrado de independência:")
print(f"  χ² = {chi2:.2f}")
print(f"  p-valor = {p:.4f}")
print(f"  Graus de liberdade = {dof}")

if p < 0.05:
    print("  Há associação significativa entre departamento e nível (p < 0.05)")
else:
    print("  Não há evidência de associação entre departamento e nível (p >= 0.05)")

# Força da associação: V de Cramér
n = tabela.sum().sum()
cramers_v = np.sqrt(chi2 / (n * (min(tabela.shape) - 1)))
print(f"\n  V de Cramér = {cramers_v:.3f}")
print("  (0 = sem associação, 1 = associação perfeita)")

Numérica discreta com categórica

Variáveis numéricas discretas com poucos valores únicos (como satisfação de 1 a 5) são melhor tratadas como categóricas ordinais para visualização:

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Distribuição de satisfação por departamento
ordem_depts = df.groupby('departamento')['satisfacao'].mean().sort_values(ascending=False).index

sns.boxplot(data=df, y='departamento', x='satisfacao',
            order=ordem_depts, ax=axes[0], palette='muted',
            width=0.5)
axes[0].set_title('Satisfação por departamento')
axes[0].set_xlabel('Satisfação (1-5)')
axes[0].set_ylabel('')
axes[0].set_xlim(0.5, 5.5)
axes[0].set_xticks([1, 2, 3, 4, 5])

# Distribuição de notas por departamento — stacked bar
prop_satisf = (df.groupby(['departamento', 'satisfacao'])
               .size()
               .unstack(fill_value=0)
               .apply(lambda x: x / x.sum() * 100, axis=1))
prop_satisf = prop_satisf.loc[ordem_depts]

cores_satisfacao = ['#d73027', '#fc8d59', '#fee08b', '#91cf60', '#1a9850']
prop_satisf.plot(kind='bar', stacked=True, ax=axes[1],
                 color=cores_satisfacao, edgecolor='white', linewidth=0.3,
                 width=0.7)
axes[1].set_title('Distribuição de satisfação por departamento')
axes[1].set_xlabel('')
axes[1].set_ylabel('Porcentagem (%)')
axes[1].tick_params(axis='x', rotation=30)
axes[1].legend(title='Satisfação', labels=['1', '2', '3', '4', '5'],
               bbox_to_anchor=(1.01, 1), loc='upper left', fontsize=9)

plt.tight_layout()
plt.show()

Jointplot: distribuição bivariada completa

O jointplot do seaborn combina scatter central com distribuições marginais nas bordas — uma visão completa da relação e das distribuições individuais num único gráfico:

# jointplot básico
g = sns.jointplot(data=df, x='anos_experiencia', y='salario',
                  kind='scatter', alpha=0.3, s=15,
                  marginal_kws={'bins': 30, 'fill': True})
g.set_axis_labels('Anos de experiência', 'Salário (R$)')
g.figure.suptitle('Relação entre experiência e salário', y=1.02)

# Adicionar correlação no gráfico
r, p = stats.pearsonr(df['anos_experiencia'], df['salario'])
g.ax_joint.text(0.05, 0.95, f'r = {r:.2f}, p < 0.001',
               transform=g.ax_joint.transAxes, fontsize=10,
               va='top', color='red')

plt.tight_layout()
plt.show()

# Variação com hexbin (melhor para overplotting)
g2 = sns.jointplot(data=df, x='idade', y='salario',
                   kind='hex', gridsize=20, cmap='Blues')
g2.set_axis_labels('Idade', 'Salário (R$)')
plt.tight_layout()
plt.show()

Lollipop chart para comparação de categorias

O lollipop chart é uma alternativa elegante ao gráfico de barras para comparações bivariadas onde você quer destacar um valor específico:

fig, ax = plt.subplots(figsize=(10, 5))

medias = (df.groupby('departamento')['salario']
          .mean()
          .sort_values(ascending=True))

media_geral = df['salario'].mean()

# Desenhar os "cabos" dos lollipops
for i, (dept, valor) in enumerate(medias.items()):
    cor = '#D73027' if valor < media_geral else '#4DAC26'
    ax.plot([media_geral, valor], [i, i],
            color=cor, linewidth=2, alpha=0.7, zorder=1)
    ax.scatter(valor, i, color=cor, s=100, zorder=2)
    ax.text(valor + 500, i, f'R$ {valor/1000:.1f}k',
            va='center', fontsize=9)

# Linha da média geral
ax.axvline(media_geral, color='gray', linestyle='--', linewidth=1,
           label=f'Média geral: R$ {media_geral/1000:.1f}k')

ax.set_yticks(range(len(medias)))
ax.set_yticklabels(medias.index)
ax.set_xlabel('Salário médio (R$)')
ax.set_title('Salário médio por departamento vs. média geral')
ax.xaxis.set_major_formatter(ticker.FuncFormatter(
    lambda x, p: f'R$ {x/1000:.0f}k'
))
ax.legend(fontsize=9)

plt.tight_layout()
plt.show()

Slopegraph para comparação antes/depois ou entre grupos

O slopegraph é ideal para mostrar como valores mudam entre dois pontos — dois períodos, dois grupos, duas condições:

fig, ax = plt.subplots(figsize=(8, 6))

# Médias de satisfação por departamento em dois anos hipotéticos
np.random.seed(10)
satisfacao_2023 = df.groupby('departamento')['satisfacao'].mean()
satisfacao_2024 = satisfacao_2023 + np.random.uniform(-0.3, 0.5, len(satisfacao_2023))

departamentos = satisfacao_2023.index

for dept in departamentos:
    v2023 = satisfacao_2023[dept]
    v2024 = satisfacao_2024[dept]
    cor = '#4DAC26' if v2024 > v2023 else '#D73027'

    ax.plot([0, 1], [v2023, v2024], color=cor, linewidth=2, alpha=0.7)
    ax.scatter([0, 1], [v2023, v2024], color=cor, s=60, zorder=5)

    # Rótulos à esquerda e à direita
    ax.text(-0.05, v2023, f'{dept}\n{v2023:.2f}',
            ha='right', va='center', fontsize=9)
    ax.text(1.05, v2024, f'{v2024:.2f}',
            ha='left', va='center', fontsize=9)

ax.set_xlim(-0.3, 1.3)
ax.set_xticks([0, 1])
ax.set_xticklabels(['2023', '2024'], fontsize=12)
ax.set_ylabel('Satisfação média')
ax.set_title('Evolução da satisfação por departamento\n2023 → 2024')
ax.yaxis.grid(True, alpha=0.3, linewidth=0.5)

plt.tight_layout()
plt.show()

Painel bivariado completo

Para uma análise bivariada formal, combine o visual com a métrica:

def analisar_relacao_numerica(df, x, y, titulo=None):
    """
    Análise bivariada completa para duas variáveis numéricas.
    """
    dados_limpos = df[[x, y]].dropna()

    fig, axes = plt.subplots(1, 3, figsize=(15, 4))
    titulo = titulo or f'{y} vs. {x}'

    # 1. Scatter com regressão
    slope, intercept, r, p, se = stats.linregress(dados_limpos[x], dados_limpos[y])
    x_range = np.linspace(dados_limpos[x].min(), dados_limpos[x].max(), 100)

    axes[0].scatter(dados_limpos[x], dados_limpos[y],
                    alpha=0.3, s=15, color='steelblue', edgecolors='none')
    axes[0].plot(x_range, slope * x_range + intercept,
                 color='red', linewidth=2)
    axes[0].set_xlabel(x)
    axes[0].set_ylabel(y)
    axes[0].set_title(f'Scatter (r = {r:.2f})')

    # 2. Residuos da regressão
    y_pred = slope * dados_limpos[x] + intercept
    residuos = dados_limpos[y] - y_pred

    axes[1].scatter(y_pred, residuos,
                    alpha=0.3, s=15, color='coral', edgecolors='none')
    axes[1].axhline(0, color='black', linewidth=1, linestyle='--')
    axes[1].set_xlabel('Valores preditos')
    axes[1].set_ylabel('Resíduos')
    axes[1].set_title('Gráfico de resíduos')

    # 3. Sumário estatístico
    r_sp, p_sp = stats.spearmanr(dados_limpos[x], dados_limpos[y])

    axes[2].axis('off')
    metricas = [
        ('r de Pearson', f'{r:.3f}'),
        ('p-valor (Pearson)', f'{p:.2e}'),
        ('r de Spearman', f'{r_sp:.3f}'),
        ('p-valor (Spearman)', f'{p_sp:.2e}'),
        ('Inclinação', f'{slope:.4f}'),
        ('Intercepto', f'{intercept:.2f}'),
        ('R²', f'{r**2:.3f}'),
        ('n', f'{len(dados_limpos):,}'),
    ]

    for i, (label, valor) in enumerate(metricas):
        y_pos = 0.95 - i * 0.11
        axes[2].text(0.05, y_pos, label, transform=axes[2].transAxes,
                    fontsize=9, color='gray', va='top')
        axes[2].text(0.95, y_pos, valor, transform=axes[2].transAxes,
                    fontsize=9, ha='right', va='top', fontweight='bold')

    axes[2].set_title('Métricas', fontsize=10)

    fig.suptitle(titulo, fontsize=13, y=1.02)
    plt.tight_layout()
    plt.show()

analisar_relacao_numerica(df, 'anos_experiencia', 'salario',
                          'Relação entre experiência e salário')

Resumo

Análise bivariada estuda relações entre pares de variáveis. Para numérica com numérica: scatter plot com linha de tendência, hexbin ou KDE 2D para overplotting, correlação de Pearson para relações lineares e Spearman para relações monotônicas ou com outliers, jointplot para visão completa. Para numérica com categórica: boxplot ou violin plot comparativos, barplot de médias com IC, strip plot para ver distribuição completa, teste de Kruskal-Wallis ou Mann-Whitney para significância estatística. Para categórica com categórica: tabela de contingência, heatmap de proporções, barras empilhadas normalizadas, teste qui-quadrado para associação e V de Cramér para força da associação. Sempre visualize antes de calcular métricas — a visualização revela padrões que um único número não captura.


Resumo do curso — onde estamos e o que falta


O plano completo

O curso tem 20 aulas numeradas de 0 a 19, organizadas em três fases, mais três projetos práticos. O total é aproximadamente 43 horas de estudo ativo.


O que já foi coberto

Aula 0 — Planejamento

Por que estudar análise de dados, estrutura completa do curso, ferramentas necessárias, pré-requisitos e como estudar de forma eficaz.

Fase 1 — Fundamentos do processo de análise (Aulas 1 a 7)

Aula 1 cobriu o processo de análise em cinco etapas: questionar, coletar, limpar, analisar e comunicar. Estabeleceu a filosofia que orienta todo o curso.

Aula 2 cobriu o Jupyter Notebook: modos de operação, atalhos essenciais, o problema do estado acumulado, boas práticas de organização e quando não usar Jupyter.

Aula 3 cobriu pandas fundamental: Series e DataFrame, carregamento de CSV com todos os parâmetros importantes, e o fluxo completo de inspeção inicial com shape, info, describe, isnull e value_counts.

Aula 4 cobriu manipulação de dados com pandas: filtragem booleana e query, criação e modificação de colunas, ordenação, agrupamento com groupby e agg, tratamento básico de ausentes, merge e concat, pivot e melt, operações de string e data.

Aula 5 cobriu NumPy: arrays homogêneos, indexação e fatiamento, a distinção crítica entre visão e cópia, operações vetorizadas, broadcasting, reshape e quando usar NumPy versus pandas.

Aula 6 cobriu estatísticas descritivas e visualizações básicas com pandas: medidas de tendência central e dispersão, correlação, histogramas, boxplots, barras, linhas, scatter e a distinção entre gráficos exploratórios e explicativos.

Aula 7 foi o Projeto 1: guia completo para investigação de um dataset escolhido pelo aluno, com estrutura do notebook, datasets sugeridos, critérios de qualidade e erros comuns.

Fase 2 — Wrangling avançado (Aulas 8 a 13)

Aula 8 cobriu a filosofia do data wrangling: as três fases (coleta, avaliação, limpeza), por que consome 60 a 80% do tempo, tipos de problemas de qualidade e estrutura, o princípio dos dados tidy e por que wrangling deve ser programático.

Aula 9 cobriu coleta de dados: CSV com parâmetros avançados e codificação, JSON aninhado com json_normalize, bancos de dados com SQLAlchemy, APIs REST com requests incluindo paginação e autenticação, e web scraping básico com BeautifulSoup.

Aula 10 cobriu avaliação sistemática de qualidade: o checklist completo de dimensões, ausentes, duplicatas, outliers, valores fora do domínio, inconsistências entre colunas, problemas de uniformidade e estrutura, documentação de problemas e avaliação de chaves antes de merge.

Aula 11 cobriu limpeza programática: correção de tipos com to_numeric e to_datetime, remoção de duplicatas, quatro estratégias de tratamento de ausentes (remoção, imputação simples, imputação por grupo e lógica de negócio), padronização de texto com mapeamento explícito, tratamento de outliers com winsorização, validação com assert, comparação de distribuições antes e depois, e um pipeline completo documentado.

Aula 12 cobriu limitações e vieses: viés de seleção, viés de sobrevivência, viés de confirmação, viés de medição, variáveis de confusão, paradoxo de Simpson, limitações de representatividade temporal e geográfica, como documentar limitações e a dimensão ética de omiti-las.

Aula 13 foi o Projeto 2: guia completo para wrangling de múltiplas fontes reais, com estrutura detalhada do notebook, combinações de fontes sugeridas, critérios de qualidade focados no rigor do processo e erros comuns.

Fase 3 — Visualização e comunicação (Aulas 14 a 16)

Aula 14 cobriu princípios de design: proporcionalidade visual e eixos truncados, hierarquia de canais visuais (posição é melhor que ângulo, que é melhor que área), uso correto de cor por tipo de dado, acessibilidade para daltonismo, relação sinal-ruído e data-ink ratio, escolha do tipo de gráfico pela pergunta, contexto com títulos e anotações, consistência em múltiplos gráficos e a regra dos cinco segundos.

Aula 15 cobriu exploração univariada com matplotlib e seaborn: histograma com linhas de referência e escolha de bins, KDE para distribuições suavizadas e comparação de grupos, boxplot com pontos sobrepostos, violin plot para distribuições bimodais, ECDF como alternativa robusta, countplot e barras de frequência para categóricas, e funções reutilizáveis de exploração.

Aula 16 cobriu exploração bivariada: scatter plot com linha de tendência e tratamento de overplotting via hexbin e KDE 2D, correlação de Pearson e Spearman com interpretação cuidadosa, heatmap de correlação, boxplot e violin comparativos entre grupos, testes de Kruskal-Wallis e Mann-Whitney, tabela de contingência e heatmap para duas categóricas, barras empilhadas normalizadas, teste qui-quadrado e V de Cramér, jointplot e slopegraph.


O que falta

Restam três aulas e um projeto — aproximadamente 15 a 20 horas de estudo.

Aula 17 — Exploração multivariada: três ou mais variáveis

Pair plot com sns.pairplot para ver todas as relações de uma vez. Facet grid para painéis condicionados a uma variável categórica. Gráficos com múltiplos canais codificados simultaneamente — cor, tamanho, forma. Análise de interações: "a relação entre X e Y muda dependendo de Z?" Heatmaps de pivot table para três variáveis. Parallel coordinates para variáveis de alta dimensão. Como evitar visualizações sobrecarregadas.

Aula 18 — Visualizações explicativas: do exploratório ao apresentável

A transformação central da Fase 3: como pegar gráficos exploratórios e refiná-los para comunicação. Hierarquia visual e destaque de um elemento central. Títulos que comunicam a conclusão em vez de descrever o gráfico. Anotações diretas para guiar a leitura. Remoção sistemática de ruído visual. Paletas de cor para publicação. Consistência de estilo em múltiplos gráficos de um mesmo relatório. Exportação em alta resolução para diferentes destinos.

Aula 19 — Projeto 3: comunicar descobertas

O projeto final consolida toda a Fase 3. O aluno escolhe um dataset, faz análise exploratória completa e produz um conjunto de quatro a seis visualizações explicativas com narrativa coerente — como se fosse um relatório real para um stakeholder. Critérios de qualidade focados na clareza da comunicação, na honestidade sobre limitações e na coerência entre análise e conclusões.


Visão geral do progresso

Das 20 aulas, 17 foram concluídas — 85% do curso. Das três fases, a Fase 1 e a Fase 2 estão completas. A Fase 3 está com três de cinco aulas concluídas. Dos três projetos práticos, o guia das aulas 7 e 13 foi entregue. O Projeto 3 (Aula 19) é o único que ainda não foi coberto.

O que foi feito cobre o núcleo técnico do curso: todo o processo analítico do início ao fim, coleta e wrangling em profundidade, e os fundamentos de visualização. O que falta é a camada de refinamento e comunicação — transformar análises corretas em análises comunicadas com eficácia.


Quando quiser continuar, é só pedir a Aula 17.

Exercícios

  1. Qual a diferença entre correlação de Pearson e correlação de Spearman? Em quais situações cada uma é mais adequada?

    ✓ Resposta:

    A correlação de Pearson mede a força e direção de uma relação linear entre duas variáveis numéricas. Ela calcula diretamente com os valores das variáveis e assume que a relação é aproximadamente linear e que os dados seguem distribuição aproximadamente normal, especialmente nas caudas.

    A correlação de Spearman mede a força e direção de uma relação monotônica — uma relação onde quando uma variável aumenta a outra tende a aumentar (ou diminuir) consistentemente, mas não necessariamente de forma linear. Ela é calculada sobre os rankings dos valores em vez dos valores em si, o que a torna robusta a outliers e a distribuições muito assimétricas.

    Use Pearson quando: a relação entre as variáveis é plausivamente linear (confirmado pelo scatter plot), as variáveis têm distribuição aproximadamente normal, e não há outliers extremos. Exemplo: relação entre altura e peso em adultos saudáveis.

    Use Spearman quando: a relação é monotônica mas não linear (confirmado pelo scatter plot com curvatura), há outliers que distorceriam Pearson, as variáveis são ordinais (como escalas de satisfação), ou a distribuição é muito assimétrica. Exemplo: relação entre anos de experiência e salário — a relação existe e é positiva, mas é claramente não-linear (os primeiros anos de experiência valem muito mais que os últimos).

    Na prática, é prudente calcular as duas e comparar: se Pearson e Spearman concordam, a relação é provavelmente linear. Se Spearman for muito maior que Pearson, a relação é monotônica mas não linear ou há outliers.

  2. Por que o hexbin é preferível ao scatter plot quando há muitos pontos? Quais informações o hexbin captura que o scatter com transparência não captura bem?

    ✓ Resposta:

    O scatter com transparência resolve parcialmente o overplotting, mas tem um problema: a percepção de densidade depende da sobreposição de pontos semi-transparentes. Em regiões muito densas, a sobreposição de centenas de pontos cria manchas escuras que parecem uniformes — você não consegue distinguir uma região com 50 pontos de uma com 500 pontos.

    O hexbin resolve esse problema dividindo o espaço em hexágonos e colorindo cada um pela contagem de pontos que caem dentro dele, usando uma escala de cor sequencial. Cada hexágono corresponde a um valor preciso na escala de cor, permitindo comparações de densidade muito mais acuradas.

    O hexbin captura melhor: a densidade absoluta em cada região (você sabe que a região mais escura tem X vezes mais pontos que a mais clara), gradientes de densidade suaves que parecem uniformes no scatter, e estrutura de distribuição bivariada como formas elípticas, caudas e outliers isolados.

    O scatter, mesmo com transparência, é melhor quando: o número de pontos é pequeno o suficiente para que os pontos individuais sejam distinguíveis, você quer identificar pontos específicos pelo contexto de outras variáveis codificadas como cor ou forma, ou a distribuição é muito esparsa e o hexbin criaria hexágonos vazios na maior parte do espaço.

  3. Escreva o código para criar um heatmap de correlação das variáveis numéricas do dataset da aula, mostrando apenas o triângulo inferior da matriz (sem a diagonal e sem o triângulo superior) e com anotações dos valores.

    ✓ Resposta:
    import seaborn as sns
    import matplotlib.pyplot as plt
    import numpy as np
    
    variaveis = ['salario', 'idade', 'anos_experiencia', 'satisfacao', 'horas_extras']
    matriz_corr = df[variaveis].corr()
    
    # Máscara para o triângulo superior e diagonal
    # np.triu com k=0 inclui a diagonal; k=1 exclui
    mask = np.triu(np.ones_like(matriz_corr, dtype=bool), k=0)
    
    fig, ax = plt.subplots(figsize=(8, 6))
    
    sns.heatmap(
        matriz_corr,
        ax=ax,
        mask=mask,
        annot=True,
        fmt='.2f',
        cmap='RdBu_r',
        vmin=-1, vmax=1,
        center=0,
        square=True,
        linewidths=0.5,
        annot_kws={'size': 10},
        cbar_kws={'shrink': 0.8, 'label': 'r de Pearson'}
    )
    
    ax.set_title('Matriz de correlação (triângulo inferior)', fontsize=13, pad=15)
    plt.tight_layout()
    plt.show()
    

    A máscara com np.triu(..., k=0) cobre a diagonal principal e tudo acima dela, deixando visível apenas o triângulo inferior. Mudar k=0 para k=1 mostraria também a diagonal, que sempre tem valor 1.00 e raramente acrescenta informação.

  4. Qual a diferença entre barras empilhadas e barras empilhadas normalizadas para 100%? Quando cada uma é mais adequada para análise bivariada categórica?

    ✓ Resposta:

    Barras empilhadas mostram os valores absolutos de cada subcategoria acumulados numa única barra. A altura total da barra representa o total de observações naquele grupo. Isso permite comparar tanto os totais entre grupos quanto a composição interna de cada grupo.

    Barras empilhadas normalizadas para 100% mostram a proporção de cada subcategoria dentro do grupo, independentemente do tamanho total do grupo. Todas as barras têm a mesma altura (100%) e a largura de cada segmento representa a proporção relativa.

    Use barras empilhadas absolutas quando o tamanho total do grupo é relevante para a análise. Exemplo: comparar o número absoluto de funcionários por nível em cada departamento — se TI tem muito mais funcionários que RH, isso é informação importante e deve aparecer na altura das barras.

    Use barras normalizadas para 100% quando você quer comparar a composição interna entre grupos que têm tamanhos muito diferentes, e o tamanho não é relevante para a pergunta. Exemplo: comparar a proporção de funcionários por nível em cada departamento, independentemente do tamanho do departamento — você quer saber "qual departamento tem proporcionalmente mais seniores?", não "qual departamento tem mais seniores em termos absolutos?".

    Um problema das barras normalizadas é que elas escondem diferenças de tamanho entre grupos. A solução quando ambas as informações importam é adicionar o total como rótulo em cada barra, ou apresentar os dois gráficos lado a lado.

  5. Você analisa a relação entre horas extras trabalhadas e satisfação dos funcionários e encontra uma correlação de Spearman de -0.42. Um colega conclui que "trabalhar mais horas extras causa redução na satisfação". Identifique os problemas nessa conclusão e como você descreveria o resultado de forma analiticamente correta.

    ✓ Resposta:

    Há três problemas na conclusão do colega.

    Problema 1 — correlação não implica causalidade. Uma correlação de -0.42 mostra que funcionários com mais horas extras tendem a ter menor satisfação — uma relação associativa. Mas ela não diz nada sobre direção causal. Poderia ser que horas extras reduzem satisfação (causalidade direta). Poderia ser que funcionários insatisfeitos são menos eficientes e precisam de mais horas para entregar o mesmo resultado (causalidade inversa). Poderia ser que um terceiro fator — como qualidade da gestão — causa simultaneamente mais horas extras e menor satisfação.

    Problema 2 — intensidade da correlação foi ignorada. Uma correlação de -0.42 é moderada — nem fraca nem forte. Isso significa que horas extras explicam apenas r² = 0.18 = 18% da variância na satisfação. Outros fatores explicam os 82% restantes. A relação existe mas está longe de ser determinística.

    Problema 3 — "causa" requer evidência causal. Para estabelecer causalidade seria necessário um experimento controlado ou uma estratégia de identificação causal como variáveis instrumentais ou diferenças em diferenças. Uma correlação observacional, por maior que seja, não é suficiente.

    A descrição analiticamente correta seria: "Observamos uma correlação negativa moderada entre horas extras e satisfação (r de Spearman = -0.42), indicando que funcionários com maior carga de horas extras tendem a reportar menor satisfação. Essa associação é consistente com a hipótese de que excesso de horas extras impacta negativamente o bem-estar, mas não estabelece causalidade — outros fatores, como qualidade da gestão e natureza do trabalho, podem explicar parte ou toda essa relação. Análises adicionais controlando por departamento, nível e tipo de projeto seriam necessárias para isolar o efeito das horas extras."

Referências

  • Cleveland, William S. Visualizing Data. Hobart Press, 1993. Extensão do trabalho seminal de Cleveland sobre percepção visual, com foco em scatter plots e representação de relações entre variáveis.
  • Wilke, Claus O. Fundamentals of Data Visualization. O'Reilly, 2019. Capítulos 12 a 14 cobrem visualizações bivariadas em profundidade. Disponível em clauswilke.com/dataviz
  • Seaborn — Statistical Data Visualization: seaborn.pydata.org/tutorial Os tutoriais oficiais do seaborn cobrem todos os tipos de gráficos relacionais e de distribuição com exemplos práticos.
  • Documentação do scipy.stats: docs.scipy.org/doc/scipy/reference/stats.html Referência para todos os testes estatísticos mencionados nesta aula, incluindo correlações, Mann-Whitney, Kruskal-Wallis e qui-quadrado.
  • Agresti, Alan. An Introduction to Categorical Data Analysis, 3ª edição. Wiley, 2018. Referência para análise de variáveis categóricas, tabelas de contingência e testes de associação.
  • McKinney, Wes. Python for Data Analysis, 3ª edição. O'Reilly, 2022. Capítulo 9 cobre visualizações com matplotlib e pandas, com bons exemplos de gráficos bivariados.