Regressão Logística

Em muitas situações na Ciência de Dados, o objetivo não é prever um número contínuo, como o preço de um produto ou a temperatura amanhã. Em vez disso, queremos prever uma classificação, como:

Nestes casos, usamos a Regressão Logística.

Apesar do nome, a regressão logística não estima valores contínuos, e sim a probabilidade de uma observação pertencer a uma classe. É especialmente útil quando a variável resposta é binária (0 ou 1, sim ou não, positivo ou negativo).

Animação mostrando como a função logística ajusta uma curva S aos dados

Na animação acima, vemos como a regressão logística encontra a melhor curva sigmoide (formato de S) que separa duas classes. Diferentemente da regressão linear que traça uma linha reta, a regressão logística usa uma função matemática especial que sempre produz valores entre 0 e 1 - perfeita para representar probabilidades!

Conceito

A regressão logística é um modelo estatístico que estima a probabilidade de uma observação pertencer à classe 1 (ou seja, de ter um evento positivo).

A equação geral é:

\[ P(Y = 1) = \frac{1}{1 + e^{-(\beta_0 + \beta_1 X_1 + \dots + \beta_n X_n)}} \]

  • A saída do modelo é uma probabilidade entre 0 e 1.
  • Essa probabilidade pode ser convertida em uma classe, com base em um limite (threshold) — por padrão, 0.5.

Diferença da Regressão Linear

Regressão Linear Regressão Logística
Saída é numérica Saída é probabilidade
Pode prever qualquer valor Prevê entre 0 e 1
Regressão sobre Y contínuo Classificação binária

Pressupostos da Regressão Logística

Embora mais flexível que a regressão linear, a regressão logística também tem pressupostos importantes. Vamos mostrar como verificar cada um deles na prática mais adiante no material.

Pressuposto Descrição Como Verificar
1. Variável resposta binária A variável Y deve ter dois resultados possíveis (0/1, sim/não) Verificação direta dos dados
2. Independência Observações independentes entre si Análise do design do estudo/coleta
3. Linearidade no logit Relação linear entre variáveis e log-odds da probabilidade Teste Box-Tidwell ou análise residual
4. Ausência de multicolinearidade Variáveis explicativas não altamente correlacionadas VIF (Variance Inflation Factor)
Nota📊 Diferença importante da regressão linear

Note que não é necessário que haja linearidade entre X e Y (como na regressão linear). O pressuposto é de linearidade entre as variáveis e o logit (log das chances) da probabilidade.

Exemplo Prático com Python

Vamos usar o dataset dos pinguins de Palmer para classificar espécies. Focaremos em um problema de classificação binária interessante: distinguir entre Adelie e Chinstrap, duas espécies que frequentemente se confundem devido às suas características físicas similares.

Nota🐧 Por que excluir a espécie Gentoo?

A espécie Gentoo é relativamente fácil de distinguir das outras duas devido ao seu tamanho corporal maior e características bem distintas. Para tornar nosso problema mais desafiador e realístico, vamos focar nas duas espécies que realmente se sobrepõem: Adelie e Chinstrap. Esta é uma situação comum na prática - quando temos um problema de classificação difícil entre grupos similares.

Vamos prever se um pinguim é da espécie Adelie (0) ou Chinstrap (1) com base em suas características físicas:

  • Comprimento do bico (bill_length_mm)
  • Profundidade do bico (bill_depth_mm)
  • Comprimento da nadadeira (flipper_length_mm)
  • Massa corporal (body_mass_g)
import pandas as pd
import altair as alt
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import classification_report, confusion_matrix, roc_auc_score, roc_curve

# Carregar dataset Palmer Penguins
penguins = pd.read_csv('https://raw.githubusercontent.com/allisonhorst/palmerpenguins/master/inst/extdata/penguins.csv')
penguins = penguins.dropna()

# Filtrar apenas Adelie e Chinstrap (excluir Gentoo)
penguins_bin = penguins[penguins['species'].isin(['Adelie', 'Chinstrap'])].copy()

# Criar variável binária: 1 para Chinstrap, 0 para Adelie
penguins_bin['species_binary'] = (penguins_bin['species'] == 'Chinstrap').astype(int)

print(f"Total de pinguins: {len(penguins_bin)}")
print(f"Adelie (0): {sum(penguins_bin['species_binary'] == 0)}")
print(f"Chinstrap (1): {sum(penguins_bin['species_binary'] == 1)}")
Total de pinguins: 214
Adelie (0): 146
Chinstrap (1): 68
# Visualizar distribuição das espécies por características
alt.Chart(penguins_bin).mark_circle(size=80, opacity=0.7).encode(
    x=alt.X('bill_length_mm:Q', 
            title='Comprimento do Bico (mm)',
            scale=alt.Scale(domain=[30, 60])),
    y=alt.Y('bill_depth_mm:Q', 
            title='Profundidade do Bico (mm)',
            scale=alt.Scale(domain=[15, 22])),
    color=alt.Color('species:N', 
                   scale=alt.Scale(scheme='category10'),
                   title='Espécie'),
    tooltip=['species:N', 'bill_length_mm:Q', 'bill_depth_mm:Q', 'flipper_length_mm:Q', 'body_mass_g:Q']
).properties(
    title='Adelie vs Chinstrap: Observe a sobreposição!',
    width=600,
    height=400
)

Podemos ver que há uma sobreposição entre Adelie e Chinstrap - isso torna nosso problema de classificação desafiador e interessante! Agora vamos preparar os dados e treinar nosso modelo:

# Selecionar variáveis explicativas
X = penguins_bin[['bill_length_mm', 'bill_depth_mm', 'flipper_length_mm', 'body_mass_g']]
y = penguins_bin['species_binary']

# Separar treino (70%) e teste (30%)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42, stratify=y)

# Padronizar os dados (importante para regressão logística)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
Dica🎯 Por que padronizar?

A regressão logística é sensível à escala das variáveis. Como temos variáveis em unidades diferentes (milímetros vs gramas), a padronização garante que todas tenham a mesma importância inicial no modelo.

Agora vamos treinar nosso modelo de regressão logística:

# Ajustar modelo
modelo_log = LogisticRegression(random_state=42)
modelo_log.fit(X_train_scaled, y_train)

# Mostrar coeficientes
coef_df = pd.DataFrame({
    'Variável': X.columns,
    'Coeficiente': modelo_log.coef_[0],
    'Interpretação': [
        'Impacto do comprimento do bico',
        'Impacto da profundidade do bico', 
        'Impacto do comprimento da nadadeira',
        'Impacto da massa corporal'
    ]
})

print("Coeficientes do Modelo de Regressão Logística:")
coef_df.round(3)
Coeficientes do Modelo de Regressão Logística:
Variável Coeficiente Interpretação
0 bill_length_mm 3.774 Impacto do comprimento do bico
1 bill_depth_mm -0.621 Impacto da profundidade do bico
2 flipper_length_mm 0.138 Impacto do comprimento da nadadeira
3 body_mass_g -0.749 Impacto da massa corporal

Os coeficientes estimados indicam o impacto de cada variável nos log-odds da resposta ser 1 (ou seja, do pinguim ser Chinstrap).

Nota📊 Interpretando os coeficientes
  • Coeficientes positivos: aumentam a probabilidade de ser Chinstrap
  • Coeficientes negativos: diminuem a probabilidade de ser Chinstrap (favorecem Adelie)
  • Magnitude: quanto maior o valor absoluto, maior o impacto da variável

Fazendo Predições

Agora que nosso modelo está treinado, vamos usá-lo para classificar os pinguins do conjunto de teste:

# Fazer predições no conjunto de teste
y_pred = modelo_log.predict(X_test_scaled)
y_proba = modelo_log.predict_proba(X_test_scaled)[:, 1]

# Criar DataFrame para visualização (resetar índices para alinhar)
test_results = pd.DataFrame({
    'bill_length_mm': X_test['bill_length_mm'].values,
    'bill_depth_mm': X_test['bill_depth_mm'].values,
    'species_true': pd.Series(y_test.values).map({0: 'Adelie', 1: 'Chinstrap'}),
    'species_pred': pd.Series(y_pred).map({0: 'Adelie', 1: 'Chinstrap'}),
    'probability_chinstrap': y_proba,
    'correct': y_test.values == y_pred
})

print(f"Acurácia no conjunto de teste: {(y_test == y_pred).mean():.3f}")
Acurácia no conjunto de teste: 1.000
Aviso⚠️ Acurácia perfeita: motivo de preocupação!

Uma acurácia de 100% pode parecer excelente, mas na prática é um sinal de alerta! Estamos trabalhando com um dataset bastante controlado (pinguins bem estudados em laboratório), mas mesmo assim, acurácia perfeita geralmente indica:

  • Sobreajuste (overfitting): O modelo decorou os dados ao invés de aprender padrões gerais
  • Vazamento de dados: Informação do futuro “vazou” para o treinamento
  • Dataset muito simples: Problema pode ser mais fácil do que a realidade

💡 Por isso é fundamental: Conhecer profundamente nosso problema e o modelo utilizado. Na prática, prefira modelos com boa generalização (85-95% de acurácia) do que aparente “perfeição” que não funciona no mundo real.

✅ No nosso caso específico: A acurácia alta faz sentido! Como vimos nos gráficos anteriores, existe uma boa separação natural entre Adelie e Chinstrap quando usamos múltiplas variáveis físicas. O modelo conseguiu capturar essa separação real dos dados.

⚠️ Mas atenção: Mesmo com boa separação atual, novos pinguins que apareçam exatamente na “divisa” entre as espécies podem confundir nosso modelo. A alta acurácia reflete nosso conjunto de teste específico, não uma garantia universal.

Vamos visualizar os resultados das predições:

# Criar gráfico simples com predições
prediction_chart = alt.Chart(test_results).mark_circle(size=80).encode(
    x=alt.X('bill_length_mm:Q', 
            scale=alt.Scale(domain=[30, 60]),
            title='Comprimento do Bico (mm)'),
    y=alt.Y('bill_depth_mm:Q',
            scale=alt.Scale(domain=[15, 22]),
            title='Profundidade do Bico (mm)'), 
    color=alt.Color('probability_chinstrap:Q',
                   scale=alt.Scale(scheme='redblue'),
                   title='Probabilidade Chinstrap')
).properties(
    title='Predições do Modelo de Regressão Logística',
    width=500,
    height=400
)

prediction_chart
Nota🎨 Interpretando a visualização
  • Cores: Gradiente de cores mostra a probabilidade de ser Chinstrap (cores mais escuras = maior probabilidade)
  • Posição: Cada ponto representa um pinguim do conjunto de teste com suas medidas reais do bico

Esse gráfico simples mostra como nosso modelo atribui probabilidades aos diferentes pinguins com base em suas características físicas.

O y_pred nos dá a classe (0 ou 1) para cada pinguim, enquanto y_proba nos mostra a probabilidade estimada de ser Chinstrap.

Diagnóstico do Modelo

Vamos verificar se nosso modelo atende aos pressupostos da regressão logística. Como vimos anteriormente, precisamos checar principalmente a linearidade no logit e a ausência de multicolinearidade.

Verificação da Linearidade no Logit

Nota📊 Pressuposto de Linearidade

A regressão logística assume uma relação linear entre as variáveis explicativas e o logit (log das chances). Na prática, com dados bem comportados como os nossos pinguins e com padronização adequada, esse pressuposto é geralmente satisfeito razoavelmente.

Para verificação mais rigorosa, técnicas como o teste Box-Tidwell podem ser aplicadas, mas requerem cuidados especiais com estabilidade numérica. Em casos de violação grave, considere transformações das variáveis ou modelos não-lineares.

Verificação de Multicolinearidade

Vamos usar o VIF (Variance Inflation Factor) para detectar correlações altas entre as variáveis:

from statsmodels.stats.outliers_influence import variance_inflation_factor

# Calcular VIF
vif_data = pd.DataFrame()
vif_data['Variável'] = X.columns
vif_data['VIF'] = [variance_inflation_factor(X_train_scaled, i) for i in range(X_train_scaled.shape[1])]

print("Fatores de Inflação da Variância (VIF):")
print(vif_data.round(2))

print("\n🔍 Interpretação dos valores de VIF:")
print("• VIF ≤ 5: Correlação baixa a moderada (aceitável)")
print("• 5 < VIF ≤ 10: Correlação elevada (atenção necessária)")  
print("• VIF > 10: Multicolinearidade crítica (problema sério)")
Fatores de Inflação da Variância (VIF):
            Variável   VIF
0     bill_length_mm  1.31
1      bill_depth_mm  1.56
2  flipper_length_mm  1.55
3        body_mass_g  1.72

🔍 Interpretação dos valores de VIF:
• VIF ≤ 5: Correlação baixa a moderada (aceitável)
• 5 < VIF ≤ 10: Correlação elevada (atenção necessária)
• VIF > 10: Multicolinearidade crítica (problema sério)
Aviso⚠️ Lidando com Violações dos Pressupostos

Se encontrarmos violações significativas dos pressupostos, algumas alternativas incluem:

  • Multicolinearidade: Remover variáveis correlacionadas ou usar regularização (Ridge/Lasso)
  • Não-linearidade: Transformações das variáveis ou modelos não-lineares
  • Problemas menores: Ainda podemos usar o modelo, mas com cautela na interpretação

Avaliação do Desempenho

Vamos avaliar como nosso modelo está performando usando várias métricas importantes:

Matriz de Confusão

# Calcular e mostrar matriz de confusão
cm = confusion_matrix(y_test, y_pred)
print("Matriz de Confusão:")
print(f"                 Predito")
print(f"Real      Adelie  Chinstrap")
print(f"Adelie       {cm[0,0]:2d}       {cm[0,1]:2d}")
print(f"Chinstrap    {cm[1,0]:2d}       {cm[1,1]:2d}")

print("\n📊 Interpretação:")
print(f"• Verdadeiros Negativos (VN): {cm[0,0]} - Adelie classificado como Adelie")
print(f"• Falsos Positivos (FP): {cm[0,1]} - Adelie classificado como Chinstrap")
print(f"• Falsos Negativos (FN): {cm[1,0]} - Chinstrap classificado como Adelie")
print(f"• Verdadeiros Positivos (VP): {cm[1,1]} - Chinstrap classificado como Chinstrap")
Matriz de Confusão:
                 Predito
Real      Adelie  Chinstrap
Adelie       44        0
Chinstrap     0       21

📊 Interpretação:
• Verdadeiros Negativos (VN): 44 - Adelie classificado como Adelie
• Falsos Positivos (FP): 0 - Adelie classificado como Chinstrap
• Falsos Negativos (FN): 0 - Chinstrap classificado como Adelie
• Verdadeiros Positivos (VP): 21 - Chinstrap classificado como Chinstrap

Métricas de Classificação

# Relatório completo de classificação
print("Relatório de Classificação:")
print(classification_report(y_test, y_pred, target_names=['Adelie', 'Chinstrap']))

# Calcular AUC-ROC
auc_score = roc_auc_score(y_test, y_proba)
print(f"\nAUC-ROC Score: {auc_score:.3f}")
Relatório de Classificação:
              precision    recall  f1-score   support

      Adelie       1.00      1.00      1.00        44
   Chinstrap       1.00      1.00      1.00        21

    accuracy                           1.00        65
   macro avg       1.00      1.00      1.00        65
weighted avg       1.00      1.00      1.00        65


AUC-ROC Score: 1.000
Dica📈 Interpretando as Métricas
  • Acurácia: Proporção total de predições corretas
  • Precisão: Das predições positivas, quantas estavam certas? (confiabilidade)
  • Recall (Sensibilidade): Dos casos positivos reais, quantos foram encontrados? (cobertura)
  • F1-Score: Média harmônica entre precisão e recall
  • AUC-ROC: Área sob a curva ROC - mede capacidade de discriminação (1.0 = perfeito, 0.5 = aleatório)

Curva ROC

A curva ROC nos mostra como o modelo discrimina entre as duas classes em diferentes thresholds:

# Calcular curva ROC
fpr, tpr, thresholds = roc_curve(y_test, y_proba)
roc_data = pd.DataFrame({
    'fpr': fpr,
    'tpr': tpr,
    'threshold': thresholds
})

# Curva ROC
roc_curve_chart = alt.Chart(roc_data).mark_line(strokeWidth=3, color='blue').encode(
    x=alt.X('fpr:Q', title='Taxa de Falsos Positivos'),
    y=alt.Y('tpr:Q', title='Taxa de Verdadeiros Positivos')
)

# Linha diagonal (classificador aleatório)
diagonal = alt.Chart(pd.DataFrame({'x': [0, 1], 'y': [0, 1]})).mark_line(
    strokeDash=[5, 5], color='red'
).encode(x='x:Q', y='y:Q')

# Combinar gráficos
(roc_curve_chart + diagonal).properties(
    title=f'Curva ROC (AUC = {auc_score:.3f})',
    width=400,
    height=400
)

📋 Resumo do Desempenho:

Nosso modelo conseguiu uma boa separação entre as espécies Adelie e Chinstrap, considerando que elas são visualmente similares. A curva ROC acima da diagonal indica que o modelo é melhor que um classificador aleatório.

Conclusão

A regressão logística é uma ferramenta poderosa para problemas de classificação binária, transformando características numéricas em probabilidades que podem ser interpretadas de forma intuitiva. Através do estudo das espécies Adelie e Chinstrap, pudemos ver como o modelo consegue distinguir entre grupos similares usando a função sigmoide.

Os principais pontos que vimos foram:

  • A regressão logística usa a função sigmoide para mapear qualquer valor real em probabilidades entre 0 e 1
  • O diagnóstico de pressupostos é crucial, especialmente linearidade no logit e multicolinearidade
  • Visualizações ajudam muito a entender o comportamento do modelo e identificar padrões
  • Métricas como AUC-ROC e matriz de confusão são essenciais para avaliar o desempenho

A regressão logística pode ser aplicada em diferentes contextos, desde detecção de spam até diagnóstico médico, sempre fornecendo interpretações claras sobre quais características mais influenciam a classificação e apoiando a tomada de decisão baseada em evidências.

Com essa base, você está pronto para aplicar regressão logística em seus próprios projetos de ciência de dados!


Recursos Adicionais