Objetivo

Aprender a criar visualizações de dados profissionais com Matplotlib e Seaborn. Dominar os gráficos mais usados em projetos de ML para exploração de dados, comunicação de resultados e diagnóstico de modelos.


1. Por que Visualização é Essencial em IA?

Antes de treinar qualquer modelo, você precisa entender seus dados. Visualizações revelam padrões, outliers, distribuições e relações que números brutos escondem.

Em projetos de ML, gráficos aparecem em:

  • EDA (Análise Exploratória de Dados): entender distribuições e correlações antes de modelar
  • Diagnóstico de qualidade: detectar outliers, desbalanceamento de classes, valores ausentes
  • Curvas de aprendizado: monitorar treino vs validação ao longo das épocas
  • Avaliação de modelos: matriz de confusão, curva ROC, importância de features
  • Comunicação de resultados: apresentar achados para stakeholders não técnicos

2. Matplotlib vs Seaborn

Matplotlib                          Seaborn
─────────────────────────────────────────────────────────
Biblioteca base, baixo nível        Construída sobre Matplotlib
Controle total sobre cada detalhe   API de alto nível, menos código
Mais verbosa                        Mais concisa para gráficos estatísticos
Qualquer tipo de gráfico            Foco em visualizações estatísticas
Customização máxima                 Estilos bonitos por padrão

Na prática: use Seaborn para gráficos estatísticos rápidos e bonitos, e Matplotlib quando precisar de controle fino ou tipos de gráficos que o Seaborn não oferece.


3. Configuração Inicial

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib as mpl
import seaborn as sns

# Configurações globais de estilo
plt.style.use("seaborn-v0_8-whitegrid")   # estilo limpo
sns.set_theme(style="whitegrid", palette="muted")

# Tamanho padrão das figuras
plt.rcParams["figure.figsize"] = (10, 6)
plt.rcParams["font.size"] = 12
plt.rcParams["axes.titlesize"] = 14
plt.rcParams["axes.labelsize"] = 12

# No Jupyter Notebook — renderizar inline
# %matplotlib inline

# Seed para reprodutibilidade
np.random.seed(42)

print("Matplotlib:", mpl.__version__)
print("Seaborn:", sns.__version__)

4. Estrutura Básica do Matplotlib

4.1 Figure e Axes

A hierarquia do Matplotlib:

Figure (a janela/página inteira)
  └── Axes (área de um gráfico individual)
        ├── Title
        ├── X axis (label, ticks, limits)
        ├── Y axis (label, ticks, limits)
        └── Plot elements (linhas, barras, pontos...)
# Interface orientada a objetos (recomendada)
fig, ax = plt.subplots(figsize=(8, 5))

x = np.linspace(0, 10, 100)
y = np.sin(x)

ax.plot(x, y, color="steelblue", linewidth=2, label="sen(x)")
ax.set_title("Função Seno")
ax.set_xlabel("x")
ax.set_ylabel("sen(x)")
ax.legend()
ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig("seno.png", dpi=150, bbox_inches="tight")
plt.show()
# Interface procedural (mais simples para gráficos rápidos)
plt.figure(figsize=(8, 5))
plt.plot(x, y)
plt.title("Função Seno")
plt.xlabel("x")
plt.show()

Use a interface orientada a objetos (fig, ax) quando criar múltiplos subplots ou precisar de controle fino. Use a procedural para rascunhos rápidos.


5. Gráfico de Linha

Ideal para séries temporais, curvas de aprendizado e funções contínuas.

# Simulando curvas de aprendizado de um modelo
epocas = np.arange(1, 51)
train_loss = 1.0 * np.exp(-0.08 * epocas) + np.random.normal(0, 0.02, 50)
val_loss   = 1.0 * np.exp(-0.06 * epocas) + np.random.normal(0, 0.03, 50) + 0.05

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

# Gráfico de perda
axes[0].plot(epocas, train_loss, label="Treino",
             color="steelblue", linewidth=2)
axes[0].plot(epocas, val_loss, label="Validação",
             color="coral", linewidth=2, linestyle="--")
axes[0].set_title("Curva de Aprendizado — Loss")
axes[0].set_xlabel("Época")
axes[0].set_ylabel("Loss")
axes[0].legend()
axes[0].set_ylim(0, 1.1)

# Gráfico de acurácia
train_acc = 1 - train_loss * 0.9
val_acc   = 1 - val_loss   * 0.9

axes[1].plot(epocas, train_acc, label="Treino",
             color="steelblue", linewidth=2)
axes[1].plot(epocas, val_acc, label="Validação",
             color="coral", linewidth=2, linestyle="--")
axes[1].set_title("Curva de Aprendizado — Acurácia")
axes[1].set_xlabel("Época")
axes[1].set_ylabel("Acurácia")
axes[1].legend()
axes[1].set_ylim(0, 1.05)

plt.suptitle("Monitoramento do Treinamento", fontsize=16, y=1.02)
plt.tight_layout()
plt.show()

6. Gráfico de Barras

Ideal para comparar categorias, distribuição de classes e importância de features.

# Importância de features de um modelo
features = ["idade", "renda", "tempo_emprego",
            "valor_emprestimo", "num_produtos",
            "satisfacao", "historico_credito"]
importancias = np.array([0.18, 0.25, 0.12, 0.22, 0.08, 0.09, 0.06])
indices = np.argsort(importancias)[::-1]   # ordenar decrescente

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

# Barras verticais
axes[0].bar(range(len(features)),
            importancias[indices],
            color="steelblue",
            alpha=0.8,
            edgecolor="white")
axes[0].set_xticks(range(len(features)))
axes[0].set_xticklabels([features[i] for i in indices], rotation=45, ha="right")
axes[0].set_title("Importância das Features (vertical)")
axes[0].set_ylabel("Importância")

# Barras horizontais — mais legível para muitas features
axes[1].barh([features[i] for i in indices],
             importancias[indices],
             color="coral",
             alpha=0.8,
             edgecolor="white")
axes[1].set_title("Importância das Features (horizontal)")
axes[1].set_xlabel("Importância")
axes[1].invert_yaxis()   # maior no topo

plt.tight_layout()
plt.show()
# Barras agrupadas — comparar múltiplos grupos
categorias = ["Grupo A", "Grupo B", "Grupo C"]
metrica_1  = [0.85, 0.78, 0.92]
metrica_2  = [0.80, 0.82, 0.88]

x = np.arange(len(categorias))
largura = 0.35

fig, ax = plt.subplots(figsize=(8, 5))
barras1 = ax.bar(x - largura/2, metrica_1, largura,
                  label="Modelo A", color="steelblue", alpha=0.8)
barras2 = ax.bar(x + largura/2, metrica_2, largura,
                  label="Modelo B", color="coral", alpha=0.8)

# Adicionar valores sobre as barras
for barra in barras1:
    ax.text(barra.get_x() + barra.get_width()/2,
            barra.get_height() + 0.01,
            f"{barra.get_height():.2f}",
            ha="center", va="bottom", fontsize=10)

for barra in barras2:
    ax.text(barra.get_x() + barra.get_width()/2,
            barra.get_height() + 0.01,
            f"{barra.get_height():.2f}",
            ha="center", va="bottom", fontsize=10)

ax.set_xticks(x)
ax.set_xticklabels(categorias)
ax.set_ylim(0, 1.1)
ax.set_title("Comparação de Modelos por Grupo")
ax.set_ylabel("Acurácia")
ax.legend()
plt.tight_layout()
plt.show()

7. Histograma

Ideal para visualizar a distribuição de uma variável contínua.

np.random.seed(42)

# Dados simulados: idades de clientes
idades_classe0 = np.random.normal(40, 10, 500)
idades_classe1 = np.random.normal(35, 8, 200)

fig, axes = plt.subplots(1, 3, figsize=(16, 5))

# Histograma simples
axes[0].hist(idades_classe0, bins=30,
             color="steelblue", alpha=0.7, edgecolor="white")
axes[0].set_title("Distribuição de Idades")
axes[0].set_xlabel("Idade")
axes[0].set_ylabel("Frequência")

# Histograma sobreposto (comparar classes)
axes[1].hist(idades_classe0, bins=30, alpha=0.6,
             color="steelblue", label="Classe 0", edgecolor="white")
axes[1].hist(idades_classe1, bins=30, alpha=0.6,
             color="coral", label="Classe 1", edgecolor="white")
axes[1].set_title("Distribuição por Classe")
axes[1].set_xlabel("Idade")
axes[1].legend()

# Histograma com densidade (KDE)
axes[2].hist(idades_classe0, bins=30, alpha=0.6,
             color="steelblue", density=True,
             label="Histograma", edgecolor="white")

# KDE manual com Seaborn
sns.kdeplot(idades_classe0, ax=axes[2],
            color="darkblue", linewidth=2, label="KDE")
axes[2].set_title("Histograma com KDE")
axes[2].set_xlabel("Idade")
axes[2].legend()

plt.tight_layout()
plt.show()

8. Scatter Plot (Diagrama de Dispersão)

Ideal para ver relações entre duas variáveis numéricas e separação de classes.

np.random.seed(42)
n = 200

# Duas classes com sobreposição
classe0_x = np.random.normal(2, 1, n)
classe0_y = np.random.normal(2, 1, n)
classe1_x = np.random.normal(4, 1, n)
classe1_y = np.random.normal(4, 1, n)

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

# Scatter simples por classe
axes[0].scatter(classe0_x, classe0_y,
                c="steelblue", alpha=0.6, label="Classe 0", s=30)
axes[0].scatter(classe1_x, classe1_y,
                c="coral", alpha=0.6, label="Classe 1", s=30)
axes[0].set_title("Separação entre Classes")
axes[0].set_xlabel("Feature 1")
axes[0].set_ylabel("Feature 2")
axes[0].legend()

# Scatter com terceira dimensão via cor (colormap)
x = np.random.uniform(0, 10, 200)
y = np.random.uniform(0, 10, 200)
z = np.sin(x) + np.cos(y)   # valor contínuo como cor

scatter = axes[1].scatter(x, y, c=z,
                           cmap="coolwarm",
                           alpha=0.7, s=40)
plt.colorbar(scatter, ax=axes[1], label="Valor")
axes[1].set_title("Scatter com Colormap")
axes[1].set_xlabel("x")
axes[1].set_ylabel("y")

plt.tight_layout()
plt.show()

9. Boxplot

Ideal para ver distribuição, mediana, quartis e outliers de uma variável por grupo.

np.random.seed(42)

df = pd.DataFrame({
    "salario": np.concatenate([
        np.random.normal(5000, 1000, 100),
        np.random.normal(8000, 1500, 100),
        np.random.normal(6000, 800,  100)
    ]),
    "departamento": ["TI"] * 100 + ["Gestão"] * 100 + ["Vendas"] * 100
})

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

# Boxplot com Matplotlib
departamentos = df["departamento"].unique()
dados_por_depto = [df[df["departamento"] == d]["salario"].values
                   for d in departamentos]

bp = axes[0].boxplot(dados_por_depto,
                     labels=departamentos,
                     patch_artist=True,
                     medianprops=dict(color="white", linewidth=2))

cores = ["steelblue", "coral", "mediumseagreen"]
for patch, cor in zip(bp["boxes"], cores):
    patch.set_facecolor(cor)
    patch.set_alpha(0.7)

axes[0].set_title("Distribuição Salarial por Departamento (Matplotlib)")
axes[0].set_ylabel("Salário (R$)")

# Boxplot com Seaborn — bem mais simples
sns.boxplot(data=df, x="departamento", y="salario",
            palette=["steelblue", "coral", "mediumseagreen"],
            ax=axes[1])
axes[1].set_title("Distribuição Salarial por Departamento (Seaborn)")
axes[1].set_ylabel("Salário (R$)")
axes[1].set_xlabel("Departamento")

plt.tight_layout()
plt.show()

10. Heatmap

Ideal para matrizes de correlação e matrizes de confusão.

np.random.seed(42)

# Matriz de correlação
df_features = pd.DataFrame(
    np.random.randn(200, 6),
    columns=["renda", "idade", "divida",
             "tempo_emprego", "num_produtos", "satisfacao"]
)

# Adicionar correlações artificiais
df_features["divida"] = df_features["renda"] * 0.6 + np.random.randn(200) * 0.5
df_features["satisfacao"] = -df_features["divida"] * 0.4 + np.random.randn(200) * 0.7

corr_matrix = df_features.corr()

fig, axes = plt.subplots(1, 2, figsize=(16, 6))

# Heatmap de correlação
sns.heatmap(corr_matrix,
            annot=True,
            fmt=".2f",
            cmap="coolwarm",
            center=0,
            vmin=-1, vmax=1,
            square=True,
            linewidths=0.5,
            ax=axes[0])
axes[0].set_title("Matriz de Correlação")

# Matriz de confusão simulada
conf_matrix = np.array([[85, 10,  5],
                         [ 8, 78, 14],
                         [ 3, 12, 85]])
classes = ["Classe A", "Classe B", "Classe C"]

sns.heatmap(conf_matrix,
            annot=True,
            fmt="d",
            cmap="Blues",
            xticklabels=classes,
            yticklabels=classes,
            linewidths=0.5,
            ax=axes[1])
axes[1].set_title("Matriz de Confusão")
axes[1].set_ylabel("Real")
axes[1].set_xlabel("Predito")

plt.tight_layout()
plt.show()

11. Gráficos Seaborn Específicos

11.1 pairplot — relações entre todas as features

np.random.seed(42)

df_iris = pd.DataFrame({
    "comprimento_sepala": np.concatenate([
        np.random.normal(5.0, 0.4, 50),
        np.random.normal(6.0, 0.5, 50),
        np.random.normal(6.5, 0.6, 50)
    ]),
    "largura_sepala": np.concatenate([
        np.random.normal(3.4, 0.4, 50),
        np.random.normal(2.8, 0.3, 50),
        np.random.normal(3.0, 0.3, 50)
    ]),
    "comprimento_petala": np.concatenate([
        np.random.normal(1.5, 0.2, 50),
        np.random.normal(4.3, 0.5, 50),
        np.random.normal(5.5, 0.6, 50)
    ]),
    "especie": ["setosa"] * 50 + ["versicolor"] * 50 + ["virginica"] * 50
})

# pairplot mostra scatter entre cada par de features
# e histograma na diagonal
g = sns.pairplot(df_iris,
                 hue="especie",
                 palette=["steelblue", "coral", "mediumseagreen"],
                 diag_kind="kde",
                 plot_kws={"alpha": 0.6, "s": 30})
g.fig.suptitle("Pairplot — Dataset Iris Simulado", y=1.02)
plt.show()

11.2 violinplot — distribuição + boxplot combinados

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

sns.violinplot(data=df, x="departamento", y="salario",
               palette=["steelblue", "coral", "mediumseagreen"],
               ax=axes[0])
axes[0].set_title("Violinplot de Salários")

# Com split para comparar dois grupos
df_genero = pd.DataFrame({
    "salario":      np.concatenate([
        np.random.normal(6000, 1500, 150),
        np.random.normal(5500, 1200, 150)
    ]),
    "departamento": (["TI"] * 50 + ["Gestão"] * 50 + ["Vendas"] * 50) * 2,
    "genero":       ["M"] * 150 + ["F"] * 150
})

sns.violinplot(data=df_genero, x="departamento", y="salario",
               hue="genero", split=True,
               palette=["steelblue", "coral"],
               ax=axes[1])
axes[1].set_title("Violinplot com Split por Gênero")

plt.tight_layout()
plt.show()

11.3 countplot — frequência de categorias

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

df_churn = pd.DataFrame({
    "churn":    np.random.choice([0, 1], 300, p=[0.75, 0.25]),
    "plano":    np.random.choice(["Basic", "Standard", "Premium"], 300),
    "regiao":   np.random.choice(["Sul", "Norte", "Sudeste", "Nordeste"], 300)
})

# Distribuição de uma variável
sns.countplot(data=df_churn, x="plano",
              palette="muted",
              order=["Basic", "Standard", "Premium"],
              ax=axes[0])
axes[0].set_title("Distribuição por Plano")
axes[0].set_ylabel("Contagem")

# Empilhado por target (hue)
sns.countplot(data=df_churn, x="plano",
              hue="churn",
              palette=["steelblue", "coral"],
              order=["Basic", "Standard", "Premium"],
              ax=axes[1])
axes[1].set_title("Churn por Plano")
axes[1].set_ylabel("Contagem")
axes[1].legend(title="Churn", labels=["Não", "Sim"])

plt.tight_layout()
plt.show()

12. Subplots — Múltiplos Gráficos

fig, axes = plt.subplots(2, 3, figsize=(16, 10))

x = np.linspace(0, 2 * np.pi, 100)

# Linha 0
axes[0, 0].plot(x, np.sin(x), color="steelblue")
axes[0, 0].set_title("Seno")

axes[0, 1].plot(x, np.cos(x), color="coral")
axes[0, 1].set_title("Cosseno")

axes[0, 2].plot(x, np.tan(x), color="mediumseagreen")
axes[0, 2].set_ylim(-5, 5)
axes[0, 2].set_title("Tangente")

# Linha 1
dados = np.random.randn(1000)
axes[1, 0].hist(dados, bins=30, color="steelblue", alpha=0.7)
axes[1, 0].set_title("Histograma Normal")

axes[1, 1].scatter(np.random.randn(100), np.random.randn(100),
                   alpha=0.6, color="coral")
axes[1, 1].set_title("Scatter Aleatório")

axes[1, 2].boxplot([np.random.normal(i, 1, 100) for i in range(4)],
                   labels=["G1", "G2", "G3", "G4"],
                   patch_artist=True)
axes[1, 2].set_title("Boxplot por Grupo")

plt.suptitle("Painel de Visualizações", fontsize=16, y=1.02)
plt.tight_layout()
plt.show()

13. Salvando Figuras

fig, ax = plt.subplots(figsize=(10, 6))
ax.plot([1, 2, 3], [1, 4, 9])
ax.set_title("Exemplo")

# PNG — para relatórios e apresentações
fig.savefig("grafico.png", dpi=150, bbox_inches="tight")

# PDF — vetorial, ideal para artigos
fig.savefig("grafico.pdf", bbox_inches="tight")

# SVG — vetorial, ideal para web
fig.savefig("grafico.svg", bbox_inches="tight")

# JPEG — menor tamanho, alguma perda de qualidade
fig.savefig("grafico.jpg", dpi=150, quality=95, bbox_inches="tight")

plt.close(fig)   # liberar memória

14. Exemplo Completo: Dashboard de EDA para ML

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

np.random.seed(42)
sns.set_theme(style="whitegrid")

# Dataset simulado
n = 500
df = pd.DataFrame({
    "idade":          np.random.normal(40, 12, n).clip(18, 75).astype(int),
    "renda":          np.random.exponential(5000, n).clip(1000, 30000).round(2),
    "divida":         np.random.exponential(2000, n).clip(0, 15000).round(2),
    "satisfacao":     np.random.randint(1, 6, n),
    "tempo_cliente":  np.random.randint(1, 120, n),
    "plano":          np.random.choice(["Basic", "Standard", "Premium"],
                                        n, p=[0.5, 0.35, 0.15]),
    "churn":          np.random.choice([0, 1], n, p=[0.75, 0.25])
})

# Taxa de churn um pouco maior para satisfação baixa
mask_baixa_sat = df["satisfacao"] <= 2
df.loc[mask_baixa_sat, "churn"] = np.random.choice(
    [0, 1], mask_baixa_sat.sum(), p=[0.5, 0.5]
)

fig = plt.figure(figsize=(18, 14))
fig.suptitle("Dashboard EDA — Dataset de Churn", fontsize=18, y=1.01)

# ── Gráfico 1: Distribuição do Target ──────────────
ax1 = fig.add_subplot(3, 3, 1)
contagem = df["churn"].value_counts()
cores_target = ["steelblue", "coral"]
ax1.pie(contagem.values,
        labels=["Não Churn", "Churn"],
        colors=cores_target,
        autopct="%1.1f%%",
        startangle=90,
        wedgeprops=dict(edgecolor="white", linewidth=2))
ax1.set_title("Distribuição do Target")

# ── Gráfico 2: Distribuição de Idades ──────────────
ax2 = fig.add_subplot(3, 3, 2)
ax2.hist(df[df["churn"] == 0]["idade"], bins=25,
         alpha=0.6, color="steelblue", label="Não Churn", edgecolor="white")
ax2.hist(df[df["churn"] == 1]["idade"], bins=25,
         alpha=0.6, color="coral", label="Churn", edgecolor="white")
ax2.set_title("Distribuição de Idades por Churn")
ax2.set_xlabel("Idade")
ax2.set_ylabel("Frequência")
ax2.legend()

# ── Gráfico 3: Renda por Churn ──────────────────────
ax3 = fig.add_subplot(3, 3, 3)
sns.boxplot(data=df, x="churn", y="renda",
            palette=["steelblue", "coral"],
            ax=ax3)
ax3.set_title("Renda por Churn")
ax3.set_xlabel("Churn (0=Não, 1=Sim)")
ax3.set_ylabel("Renda (R$)")

# ── Gráfico 4: Churn por Plano ──────────────────────
ax4 = fig.add_subplot(3, 3, 4)
churn_por_plano = (df.groupby("plano")["churn"]
                     .mean()
                     .reindex(["Basic", "Standard", "Premium"]))
barras = ax4.bar(churn_por_plano.index,
                 churn_por_plano.values * 100,
                 color=["#5B9BD5", "#70AD47", "#ED7D31"],
                 alpha=0.85,
                 edgecolor="white")
for b in barras:
    ax4.text(b.get_x() + b.get_width() / 2,
             b.get_height() + 0.5,
             f"{b.get_height():.1f}%",
             ha="center", fontsize=10)
ax4.set_title("Taxa de Churn por Plano")
ax4.set_ylabel("Taxa de Churn (%)")
ax4.set_ylim(0, 50)

# ── Gráfico 5: Satisfação vs Churn ──────────────────
ax5 = fig.add_subplot(3, 3, 5)
churn_por_sat = df.groupby("satisfacao")["churn"].mean() * 100
ax5.plot(churn_por_sat.index, churn_por_sat.values,
         marker="o", color="coral", linewidth=2, markersize=8)
ax5.fill_between(churn_por_sat.index, churn_por_sat.values,
                 alpha=0.2, color="coral")
ax5.set_title("Taxa de Churn por Satisfação")
ax5.set_xlabel("Nível de Satisfação (1=Baixo, 5=Alto)")
ax5.set_ylabel("Taxa de Churn (%)")
ax5.set_xticks([1, 2, 3, 4, 5])

# ── Gráfico 6: Scatter Renda vs Dívida ──────────────
ax6 = fig.add_subplot(3, 3, 6)
cores = df["churn"].map({0: "steelblue", 1: "coral"})
ax6.scatter(df["renda"], df["divida"],
            c=cores, alpha=0.4, s=15)
ax6.set_title("Renda vs Dívida")
ax6.set_xlabel("Renda (R$)")
ax6.set_ylabel("Dívida (R$)")

from matplotlib.patches import Patch
legenda = [Patch(color="steelblue", label="Não Churn"),
           Patch(color="coral",     label="Churn")]
ax6.legend(handles=legenda)

# ── Gráfico 7: Matriz de Correlação ─────────────────
ax7 = fig.add_subplot(3, 3, 7)
colunas_num = ["idade", "renda", "divida", "satisfacao", "tempo_cliente"]
corr = df[colunas_num].corr()
sns.heatmap(corr,
            annot=True, fmt=".2f",
            cmap="coolwarm", center=0,
            vmin=-1, vmax=1,
            linewidths=0.5,
            ax=ax7,
            cbar_kws={"shrink": 0.8})
ax7.set_title("Correlação entre Features")

# ── Gráfico 8: Distribuição do Tempo de Cliente ─────
ax8 = fig.add_subplot(3, 3, 8)
sns.kdeplot(data=df, x="tempo_cliente",
            hue="churn",
            palette=["steelblue", "coral"],
            fill=True, alpha=0.4,
            ax=ax8)
ax8.set_title("Tempo de Cliente por Churn")
ax8.set_xlabel("Tempo como Cliente (meses)")
ax8.legend(title="Churn", labels=["Não", "Sim"])

# ── Gráfico 9: Estatísticas Resumo ──────────────────
ax9 = fig.add_subplot(3, 3, 9)
ax9.axis("off")

resumo = df.groupby("churn")[colunas_num].mean().round(1)
tabela_dados = [["Métrica", "Não Churn", "Churn"]]
for col in colunas_num:
    tabela_dados.append([
        col,
        f"{resumo.loc[0, col]:.1f}",
        f"{resumo.loc[1, col]:.1f}"
    ])

tabela = ax9.table(
    cellText=tabela_dados[1:],
    colLabels=tabela_dados[0],
    cellLoc="center",
    loc="center",
    bbox=[0, 0, 1, 1]
)
tabela.auto_set_font_size(False)
tabela.set_fontsize(10)
for j in range(3):
    tabela[0, j].set_facecolor("#2C5F8A")
    tabela[0, j].set_text_props(color="white", fontweight="bold")
ax9.set_title("Médias por Grupo de Churn", pad=20)

plt.tight_layout()
plt.savefig("dashboard_eda.png", dpi=150, bbox_inches="tight")
plt.show()
print("Dashboard salvo em dashboard_eda.png")

Resumo da Aula

  • Matplotlib é a biblioteca base: controle total, mais verbosa
  • Seaborn é construída sobre Matplotlib: mais concisa, estatisticamente orientada
  • A hierarquia do Matplotlib é Figure → Axes → elementos visuais
  • fig, ax = plt.subplots() é a forma recomendada para criar gráficos
  • Gráfico de linha: séries temporais e curvas de aprendizado
  • Histograma: distribuição de variáveis contínuas
  • Scatter plot: relação entre duas variáveis e separação de classes
  • Boxplot: distribuição por grupo com quartis e outliers visíveis
  • Heatmap: matrizes de correlação e matrizes de confusão
  • pairplot do Seaborn mostra todas as relações entre features de uma vez
  • plt.subplots(m, n) cria grades de múltiplos gráficos
  • fig.savefig() exporta em PNG, PDF, SVG ou JPEG
  • Sempre defina título, rótulos dos eixos e legenda em gráficos para publicação

Exercícios

  1. Explique a diferença entre a interface procedural (plt.plot()) e a interface orientada a objetos (fig, ax = plt.subplots(); ax.plot()) do Matplotlib. Quando você usaria cada uma?

    ✓ Resposta:

    A interface procedural usa funções do módulo plt diretamente e mantém um estado global: sempre opera na figura e nos eixos "ativos" no momento. É mais concisa para gráficos rápidos e exploratórios de uma única figura.

    plt.figure(figsize=(8, 5))
    plt.plot([1, 2, 3], [1, 4, 9])
    plt.title("Rápido e simples")
    plt.show()
    

    A interface orientada a objetos cria explicitamente objetos Figure e Axes e opera sobre eles diretamente. É mais verbosa mas muito mais controlável, especialmente com múltiplos subplots.

    fig, axes = plt.subplots(1, 2, figsize=(12, 5))
    axes[0].plot([1, 2, 3], [1, 4, 9])
    axes[1].bar(["A", "B"], [3, 7])
    

    Use a procedural para rascunhos e exploração rápida em Jupyter. Use a orientada a objetos sempre que tiver múltiplos subplots, precisar passar os eixos para funções do Seaborn, ou criar gráficos para relatórios e publicações onde o controle fino importa.

  2. Dado o array de curvas de aprendizado abaixo, crie um gráfico de linha com as duas curvas (treino e validação), identificando visualmente o ponto de overfitting (onde a validação começa a piorar enquanto o treino continua melhorando). Adicione uma linha vertical nesse ponto.

    epocas     = np.arange(1, 31)
    train_loss = 1.0 / (1 + 0.15 * epocas) + np.random.normal(0, 0.01, 30)
    val_loss   = 1.0 / (1 + 0.10 * epocas) + np.random.normal(0, 0.02, 30)
    val_loss[15:] += np.linspace(0, 0.15, 15)   # overfitting após época 15
    

    ✓ Resposta:
    np.random.seed(42)
    epocas     = np.arange(1, 31)
    train_loss = 1.0 / (1 + 0.15 * epocas) + np.random.normal(0, 0.01, 30)
    val_loss   = 1.0 / (1 + 0.10 * epocas) + np.random.normal(0, 0.02, 30)
    val_loss[15:] += np.linspace(0, 0.15, 15)
    
    epoca_overfitting = 15
    
    fig, ax = plt.subplots(figsize=(10, 6))
    
    ax.plot(epocas, train_loss, label="Treino",
            color="steelblue", linewidth=2)
    ax.plot(epocas, val_loss, label="Validação",
            color="coral", linewidth=2)
    
    ax.axvline(x=epoca_overfitting, color="gray",
               linestyle="--", linewidth=1.5,
               label=f"Overfitting (época {epoca_overfitting})")
    
    ax.fill_betweenx([0, 1], epoca_overfitting, 30,
                     alpha=0.05, color="red")
    
    ax.annotate("Overfitting começa aqui",
                xy=(epoca_overfitting, val_loss[epoca_overfitting - 1]),
                xytext=(epoca_overfitting + 3, val_loss[epoca_overfitting - 1] + 0.05),
                arrowprops=dict(arrowstyle="->", color="gray"),
                fontsize=10, color="gray")
    
    ax.set_title("Curva de Aprendizado com Ponto de Overfitting")
    ax.set_xlabel("Época")
    ax.set_ylabel("Loss")
    ax.legend()
    ax.set_ylim(0, 0.8)
    plt.tight_layout()
    plt.show()
    
  3. Explique quando usar cada tipo de gráfico: histograma, boxplot e violinplot. Quais informações cada um mostra que os outros não mostram?

    ✓ Resposta:

    Histograma mostra a distribuição de frequências de uma variável contínua — quantos valores caem em cada intervalo. É ideal para ver se a distribuição é normal, assimétrica, bimodal ou uniforme. Não mostra informações por grupo facilmente.

    Boxplot mostra a mediana, os quartis (Q1 e Q3), os whiskers (geralmente 1.5×IQR) e os outliers como pontos individuais. É compacto e ideal para comparar distribuições entre múltiplos grupos lado a lado. Não revela se a distribuição é bimodal.

    Violinplot combina o boxplot com uma estimativa de densidade (KDE), mostrando a forma completa da distribuição além dos quartis. É mais informativo que o boxplot pois revela distribuições bimodais e assimetrias detalhadas, mas ocupa mais espaço e pode ser mais difícil de interpretar para não especialistas.

    Em resumo: use histograma para uma única variável em detalhe, boxplot para comparar muitos grupos de forma compacta, e violinplot quando a forma da distribuição é importante e você tem poucos grupos para comparar.

  4. Por que a matriz de correlação é uma visualização importante antes de treinar um modelo de ML? O que você faria se encontrasse duas features com correlação de 0.97?

    ✓ Resposta:

    A matriz de correlação revela relações lineares entre features. Antes de treinar é importante porque:

    Features altamente correlacionadas entre si (multicolinearidade) causam instabilidade em modelos lineares e tornam os coeficientes difíceis de interpretar. Features correlacionadas com o target são candidatas fortes para inclusão no modelo. Features com correlação próxima de zero com o target podem ser candidatas à remoção, reduzindo dimensionalidade.

    Se você encontrar duas features com correlação de 0.97, elas carregam essencialmente a mesma informação. As ações possíveis são:

    Remover uma delas: escolha manter a que tem maior correlação com o target ou a que é mais interpretável no contexto do negócio.

    Criar uma feature combinada: por exemplo, a média das duas, se fizer sentido semanticamente.

    Aplicar PCA: transformar as features correlacionadas em componentes ortogonais que capturam a variância máxima sem redundância.

    Para modelos baseados em árvores (Random Forest, XGBoost), multicolinearidade é menos problemática pois esses modelos não são sensíveis a ela da mesma forma que modelos lineares.

  5. Dado o DataFrame abaixo, crie um painel com dois subplots lado a lado: à esquerda um countplot mostrando a distribuição de cada categoria, e à direita um boxplot mostrando a distribuição do valor por categoria. Adicione títulos, rótulos e formate adequadamente.

    df = pd.DataFrame({
        "categoria": np.random.choice(["A", "B", "C", "D"], 200),
        "valor":     np.concatenate([
            np.random.normal(10, 2, 50),
            np.random.normal(15, 3, 50),
            np.random.normal(12, 2, 50),
            np.random.normal(18, 4, 50)
        ])
    })
    

    ✓ Resposta:
    np.random.seed(42)
    df = pd.DataFrame({
        "categoria": np.random.choice(["A", "B", "C", "D"], 200),
        "valor": np.concatenate([
            np.random.normal(10, 2, 50),
            np.random.normal(15, 3, 50),
            np.random.normal(12, 2, 50),
            np.random.normal(18, 4, 50)
        ])
    })
    
    palette = ["steelblue", "coral", "mediumseagreen", "mediumpurple"]
    ordem = ["A", "B", "C", "D"]
    
    fig, axes = plt.subplots(1, 2, figsize=(14, 5))
    
    # Countplot
    sns.countplot(data=df, x="categoria",
                  order=ordem, palette=palette, ax=axes[0])
    axes[0].set_title("Distribuição por Categoria")
    axes[0].set_xlabel("Categoria")
    axes[0].set_ylabel("Contagem")
    for p in axes[0].patches:
        axes[0].annotate(f"{int(p.get_height())}",
                         (p.get_x() + p.get_width() / 2, p.get_height() + 1),
                         ha="center", fontsize=11)
    
    # Boxplot
    sns.boxplot(data=df, x="categoria", y="valor",
                order=ordem, palette=palette, ax=axes[1])
    axes[1].set_title("Distribuição do Valor por Categoria")
    axes[1].set_xlabel("Categoria")
    axes[1].set_ylabel("Valor")
    
    plt.suptitle("Análise por Categoria", fontsize=14, y=1.02)
    plt.tight_layout()
    plt.show()
    
  6. Explique o que o pairplot do Seaborn mostra e por que ele é tão útil na fase de EDA de um projeto de ML. Quais padrões você procuraria nele antes de escolher um algoritmo?

    ✓ Resposta:

    O pairplot mostra todas as combinações de scatter plots entre pares de features numéricas, com as distribuições individuais na diagonal. Com o parâmetro hue, ele colore os pontos pela classe do target, revelando de uma vez:

    Separabilidade das classes: se os pontos de classes diferentes estão bem separados em um scatter, aquele par de features é poderoso para classificação. Se estão completamente sobrepostos, aquelas features sozinhas não distinguem as classes.

    Correlações entre features: se os pontos formam uma linha diagonal em um scatter, as duas features são correlacionadas — candidatas a multicolinearidade.

    Distribuições por classe na diagonal: se as distribuições de uma feature são bem separadas entre classes (pouco sobrepostas), aquela feature é individualmente informativa.

    Outliers e padrões não lineares: aglomerações, curvas ou pontos distantes ficam visíveis.

    Antes de escolher um algoritmo, você procuraria no pairplot: se as classes são linearmente separáveis (favorece regressão logística e SVM linear), se há separação não linear (favorece SVM com kernel RBF ou redes neurais), se as distribuições por classe são gaussianas e separadas (favorece Naive Bayes), e se há muitas features correlacionadas (sugere usar PCA antes de modelar).

Referências