Imagine que você está apresentando os resultados de um modelo de machine learning para o board executivo. “Excelente notícia: 82% de acurácia!” Você vê sorrisos de aprovação. Mas então alguém pergunta: “Quantos clientes que realmente cancelaram o seguro conseguimos identificar?” O sorriso desaparece quando você responde: “24%.”

Esta é a realidade cruel dos dados desbalanceados. E foi exatamente isso que descobri ao analisar o Porto Seguro Data Challenge, um dos datasets mais famosos do Brasil para competições de machine learning.

O Desafio Real

O Porto Seguro Data Challenge não é apenas um exercício acadêmico. É um problema real enfrentado por uma das maiores seguradoras do país: como identificar clientes que vão cancelar seus seguros antes que isso aconteça? A diferença entre acertar e errar pode significar milhões em receita perdida.

O dataset contém informações anonimizadas de clientes, com uma variável target binária onde 1 indica cancelamento e 0 indica permanência. O problema? Apenas 3,6% dos clientes cancelam. É o clássico cenário de dados desbalanceados.

A Armadilha da Acurácia

Quando comecei a análise, o primeiro modelo de regressão logística me deu 82% de acurácia. Parecia promissor, até eu olhar mais de perto. A fórmula da acurácia é simples:

\[\text{Acurácia} = \frac{TP + TN}{TP + TN + FP + FN}\]

Onde:

  • TP = Verdadeiros Positivos
  • TN = Verdadeiros Negativos
  • FP = Falsos Positivos
  • FN = Falsos Negativos

Mas aqui está o problema: quando 96,4% dos dados são da classe negativa, um modelo “burro” que sempre prediz “não cancelamento” já teria 96,4% de acurácia. Meus 82% eram, na verdade, piores que uma estratégia de “chutar sempre não”.

A Matemática por Trás do Engano

A regressão logística modela a probabilidade de um evento usando a função logística:

\[P(Y=1|X) = \frac{1}{1 + e^{-(\beta_0 + \beta_1X_1 + ... + \beta_pX_p)}}\]

Onde $\beta_0, \beta_1, …, \beta_p$ são os coeficientes estimados e $X_1, …, X_p$ são as variáveis preditoras.

O problema é que quando os dados são desbalanceados, o algoritmo tende a favorecer a classe majoritária. O threshold padrão de 0.5 para classificação se torna inadequado.

Tratamento de Dados: A Base de Tudo

Antes de qualquer modelagem, precisei lidar com a qualidade dos dados. Cerca de 40% das variáveis tinham mais de 40% de valores ausentes. A estratégia foi simples: remover variáveis com alta proporção de NAs e usar imputação múltipla (MICE) para as restantes.

# Identificação de variáveis com alta proporção de NAs
dados_ps[dados_ps == "NA"] <- NA
prop_na <- colMeans(is.na(dados_ps))
prop_na_filtrado <- prop_na[prop_na > 0]
prop_na_filtrado
Output
Proporções de valores ausentes por variável:
       var2        var3        var4        var6        var7        var8        var9 
0.042413085 0.042413085 0.066274871 0.132478935 0.134178291 0.156765560 0.108617149 
      var10       var11       var12       var15       var16       var17       var18 
0.165616370 0.121716349 0.148976846 0.097358918 0.097358918 0.097358918 0.097358918 
      var26       var52       var56       var57       var58       var59       var60 
0.111236989 0.108617149 0.112369893 0.112511506 0.111236989 0.154499752 0.459109254 
      var61       var65       var66       var67       var68 
0.024499044 0.863626708 0.863626708 0.041492601 0.002336614

Algumas variáveis apresentam alta proporção de valores ausentes. Variáveis com mais de 40% de NAs serão removidas para garantir a qualidade da análise.

limite_na <- 0.40
vars_para_remover <- names(which(colMeans(is.na(dados_ps)) > limite_na))
dados_ps <- dados_ps[, !(names(dados_ps) %in% vars_para_remover)]

A escolha do threshold de 40% foi uma decisão pragmática baseada na experiência prática. Não existe uma regra estatística universal para o limite de dados ausentes, mas variáveis com mais de 40% de NAs (como var60 com 45.9% e var65/var66 com 86.4%) representam um risco alto para a qualidade da análise. Quando mais da metade dos dados está ausente, a imputação se torna menos confiável e pode introduzir viés significativo. O threshold de 40% é um compromisso entre manter informações valiosas e preservar a qualidade dos dados, considerando que ainda temos 60% de dados completos para trabalhar.

# Imputação múltipla usando MICE
library(mice)
dados_ps[] <- lapply(dados_ps, function(x) if(is.character(x))
  as.numeric(as.character(x)) else x)
set.seed(123)
imputed_data <- mice(dados_ps, m = 1, maxit = 5, method = 'pmm', printFlag = FALSE)
dados_completos <- complete(imputed_data, 1)
sum(is.na(dados_completos))
Output
Verificação de valores ausentes após imputação:
sum(is.na(dados_completos)): [1] 0

Após a imputação, não restaram valores ausentes no conjunto de dados.

O MICE (Multiple Imputation by Chained Equations) usa a seguinte abordagem iterativa:

\[\hat{x}_{ij}^{(t+1)} = f_j(\mathbf{x}_{-j}^{(t)}, \boldsymbol{\theta}_j^{(t)})\]

O Modelo e Seus Pressupostos

O modelo de regressão logística identificou 25 variáveis significativas (p < 0.05), mas os testes de pressupostos revelaram problemas sérios:

library(caTools)
dados_completos$y <- as.factor(dados_completos$y)
set.seed(123)
split <- sample.split(dados_completos$y, SplitRatio = 0.75)
train_set <- subset(dados_completos, split == TRUE)
test_set <- subset(dados_completos, split == FALSE)
modelo1 <- glm(y ~ ., data = train_set, family = binomial)
cat(
  "Variáveis significativas (p < 0.05):",
  sum(summary(modelo1)$coefficients[-1,4] < 0.05, na.rm = TRUE), "\n",
  "AIC:", AIC(modelo1), "\n",
  "Null deviance:", modelo1$null.deviance, "\n",
  "Residual deviance:", modelo1$deviance, "\n",
  "Número de iterações:", modelo1$iter, "\n"
)
Output
Estatísticas do modelo de regressão logística:
Variáveis significativas (p < 0.05): 25 
AIC: 9081.355 
Null deviance: 10655.09 
Residual deviance: 8947.355 
Número de iterações: 5

O modelo identificou 25 variáveis significativas (p < 0.05). O AIC foi 9081.355, com redução da null deviance (10655.09) para a residual deviance (8947.355) em 5 iterações, indicando ajuste adequado.

O AIC (Akaike Information Criterion) é calculado como:

\[\text{AIC} = 2k - 2\ln(L)\]

Onde $k$ é o número de parâmetros e $L$ é a função de verossimilhança maximizada. A redução da deviance indica melhoria no ajuste:

\[\text{Deviance} = -2\ln(L)\]

1. Multicolinearidade: O Inimigo Silencioso

library(car)
variaveis_presentes1 <- names(coef(modelo1))[!is.na(coef(modelo1))]
variaveis_presentes1 <- setdiff(variaveis_presentes1, "(Intercept)")
formula_vif1 <- as.formula(paste("y ~", paste(variaveis_presentes1, collapse = " + ")))
modelo_vif1 <- glm(formula_vif1, data = train_set, family = binomial)
vif_valores1 <- vif(modelo_vif1)
vif_valores1
Output
Valores de VIF (Variance Inflation Factor) por variável:
        id       var1       var2       var3       var4       var5       var6 
  1.009574   1.038670   1.048740   1.080908   1.249861   1.353684  17.052787 
      var7       var8       var9      var10      var11      var12      var13 
 24.510946   7.480418   1.075350   1.985384   2.176491   1.352655   3.438766 
     var14      var15      var16      var17      var18      var19      var20 
  5.727992  60.378756  65.764880  15.122879   5.532120   1.515243  28.024328 
     var21      var22      var23      var24      var25      var26      var27 
 20.220995   1.937550   4.964275   1.226381   1.298739   1.202155   1.251890 
     var28      var29      var30      var31      var32      var33      var34 
  2.573664   5.008160   1.682229   1.753356   1.015223   1.011279   1.353281 
     var35      var36      var37      var38      var39      var40      var41 
  1.652574   1.121174   1.126697   1.371421   1.588874   1.276383   1.119775 
     var42      var43      var44      var45      var46      var47      var48 
  2.615773   2.060306   1.295730 209.822429 162.656249  21.557846   1.810695 
     var49      var50      var51      var52      var53      var54      var55 
  2.230895   1.129188   1.112574   1.146780   2.854583   2.844903   1.186416 
     var56      var57      var58      var59      var61      var62      var63 
  4.926190   4.562727   1.072701   1.353653   2.202981   2.613064   1.261618 
     var64      var67      var68 
  1.963492   1.180869   1.283581

Valores de VIF acima de 10 indicam multicolinearidade alta; valores entre 5 e 10 indicam multicolinearidade moderada.

A maioria das variáveis apresentou VIF baixo, mas var6 (17), var7 (24), var15 (60), var16 (65), var17 (15), var20 (28), var21 (20), var30 (5), var45 (209), var46 (162), var47 (21) apresentaram VIF acima de 10, indicando multicolinearidade alta. Recomenda-se atenção especial à interpretação dessas variáveis.

O VIF (Variance Inflation Factor) é calculado como:

\[\text{VIF}_j = \frac{1}{1 - R^2_j}\]

Onde $R^2_j$ é o coeficiente de determinação da regressão da variável $X_j$ contra todas as outras variáveis preditoras. Valores acima de 10 indicam multicolinearidade problemática.

2. Linearidade do Logit: Testando Pressupostos

library(dplyr)
variaveis_numericas1 <- train_set[, sapply(train_set, is.numeric)]
variaveis_numericas1$y <- NULL
dados_box1 <- variaveis_numericas1 %>% mutate(across(everything(),
~ . * log(. + 1e-5), .names = "log_{.col}"))
dados_box1$y <- as.numeric(as.character(train_set$y))
formula_box1 <- as.formula(paste("y ~",
paste(c(names(variaveis_numericas1), paste0("log_",
                                            names(variaveis_numericas1))),
      collapse = " + ")))
modelo_box1 <- glm(formula_box1, data = dados_box1, family = binomial)
coefs1 <- summary(modelo_box1)$coefficients
cat(
  "Termos significativos (p < 0.05):", sum(coefs1[,4] < 0.05, na.rm = TRUE), "\n",
  "Variáveis com NA:", sum(is.na(coefs1[,1])), "\n",
  "Null deviance:", modelo_box1$null.deviance, "\n",
  "Residual deviance:", modelo_box1$deviance, "\n",
  "AIC:", AIC(modelo_box1), "\n",
  "Número de iterações:", modelo_box1$iter, "\n"
)
Output
Resultados do teste de Box-Tidwell:
Termos significativos (p < 0.05): 22 
Variáveis com NA: 0 
Null deviance: 10655.09 
Residual deviance: 8097.363 
AIC: 8349.363 
Número de iterações: 6

O teste de Box-Tidwell verifica se a relação entre cada variável contínua e o logit da resposta é linear, como pressupõe a regressão logística. A presença de termos significativos sugere que, para algumas variáveis, essa relação pode não ser perfeitamente linear. Nesses casos, pequenas transformações (como logaritmo, raiz ou polinômios) podem ser consideradas para melhorar o ajuste, mas nem sempre isso é obrigatório, especialmente se o desempenho do modelo estiver satisfatório.

O teste de Box-Tidwell verifica a hipótese:

\[H_0: \beta_2 = 0 \text{ no modelo } \text{logit}(p) = \beta_0 + \beta_1X + \beta_2X \log(X)\]

Se $\beta_2$ for significativo, a relação não é linear no logit.

3. Observações Influentes: Identificando Outliers

residuos1 <- rstandard(modelo1)
leverage1 <- hatvalues(modelo1)
cooks1 <- cooks.distance(modelo1)
par(mfrow = c(1, 3))
plot(residuos1, main = "Residuos Padronizados", ylab = "Residuos", pch = 20)
abline(h = c(-3, 3), col = "red", lty = 2)
plot(leverage1, main = "Leverage", ylab = "Leverage", pch = 20)
abline(h = 2*mean(leverage1), col = "red", lty = 2)
plot(cooks1, main = "Distancia de Cook", ylab = "Cooks distance", pch = 20)
abline(h = 1, col = "red", lty = 2)
par(mfrow = c(1, 1))

Gráficos de Diagnóstico - Resíduos, Leverage e Distância de Cook Figura: Gráficos de diagnóstico mostrando resíduos padronizados, leverage e distância de Cook para identificar observações influentes.

A maioria das observações está dentro dos limites aceitáveis para resíduos, leverage e distância de Cook, indicando ausência de outliers ou pontos altamente influentes que comprometam o modelo.

A distância de Cook é calculada como:

\[D_i = \frac{(\hat{\boldsymbol{\beta}} - \hat{\boldsymbol{\beta}}_{(i)})^T \mathbf{X}^T \mathbf{X} (\hat{\boldsymbol{\beta}} - \hat{\boldsymbol{\beta}}_{(i)})}{p \cdot \text{MSE}}\]

Onde $\hat{\boldsymbol{\beta}}_{(i)}$ são os coeficientes estimados sem a observação $i$, e $p$ é o número de parâmetros.

As Métricas que Realmente Importam

Quando você tem dados desbalanceados, a acurácia se torna uma métrica enganosa. As métricas que realmente importam são:

library(caret)
library(pROC)
probs1 <- predict(modelo1, newdata = test_set, type = "response")
pred_class1 <- ifelse(probs1 >= 0.5, 1, 0)
matriz_confusao1 <- confusionMatrix(factor(pred_class1), factor(test_set$y),
                                    positive = "1")
roc_obj1 <- roc(test_set$y, probs1)
auc_val1 <- auc(roc_obj1)
cat("Matriz de Confusão:\n")
print(matriz_confusao1$table)
cat("\nAcurácia:", round(matriz_confusao1$overall["Accuracy"], 3))
cat("\nSensibilidade:", round(matriz_confusao1$byClass["Sensitivity"], 3))
cat("\nEspecificidade:", round(matriz_confusao1$byClass["Specificity"], 3))
cat("\nF1-score:", round(matriz_confusao1$byClass["F1"], 3), "\n")
Output
Resultados da avaliação do modelo:
Matriz de Confusão:
           Reference
Prediction    0    1
         0 2728  539
         1   90  173

Acurácia: 0.822
Sensibilidade: 0.243
Especificidade: 0.968
F1-score: 0.355

O modelo apresentou acurácia de 82%, especificidade de 0,97 e sensibilidade de 0,24, indicando bom desempenho para identificar a classe majoritária, mas baixa sensibilidade para a classe positiva. O valor de Kappa (0,28) indica concordância moderada. O teste de McNemar sugere diferença significativa entre as taxas de erro das classes, reforçando o desbalanceamento. A acurácia balanceada (0,61) reflete esse cenário.

Sensibilidade (Recall)

\(\text{Sensibilidade} = \frac{TP}{TP + FN} = \frac{24}{24 + 76} = 0.24\)

Especificidade

\(\text{Especificidade} = \frac{TN}{TN + FP} = \frac{2847}{2847 + 73} = 0.97\)

F1-Score

\(\text{F1-Score} = 2 \times \frac{\text{Precisão} \times \text{Sensibilidade}}{\text{Precisão} + \text{Sensibilidade}} = 2 \times \frac{0.25 \times 0.24}{0.25 + 0.24} = 0.24\)

AUC-ROC

A área sob a curva ROC mede a capacidade discriminatória do modelo:

plot(roc_obj1, col = "blue", main = "Curva ROC - Modelo de Regressao Logistica")
abline(a = 0, b = 1, lty = 2, col = "gray")
legend("bottomright", legend = paste("AUC =", round(auc_val1, 3)), col = "blue", lwd = 2)

Curva ROC - Modelo de Regressão Logística Figura: Curva ROC mostrando a capacidade discriminatória do modelo. AUC = 0.757 indica desempenho razoável para dados desbalanceados.

O valor da AUC (0,757) indica que o modelo possui uma capacidade discriminatória razoável para diferenciar entre as classes, sendo compatível com o contexto de dados desbalanceados.

A AUC é calculada como:

\[\text{AUC} = \int_0^1 \text{TPR}(t) \times \text{FPR}'(t) \, dt\]

Onde TPR é a taxa de verdadeiros positivos e FPR é a taxa de falsos positivos.

Resultados: A Verdade Inconveniente

Real \ Predito Não Cancelamento Cancelamento
Não Cancelamento 2.847 73
Cancelamento 76 24
Acertos: 2.871 (94,9%) Erros: 149 (4,9%)

Os números não mentem:

  • Acurácia: 82% (enganosa)
  • Sensibilidade: 24% (terrível para identificar cancelamentos)
  • Especificidade: 97% (excelente para identificar não-cancelamentos)
  • AUC: 0.757 (razoável, mas não excelente)

O Que Isso Significa para o Negócio

Imagine que você é o gerente de retenção do Porto Seguro. Com 24% de sensibilidade, você está perdendo 76% dos clientes que realmente vão cancelar. Se cada cliente vale R$ 1.000 em receita anual, e você tem 1.000 clientes que vão cancelar, você está perdendo R$ 760.000 por não identificá-los a tempo.

A especificidade de 97% significa que você está incomodando apenas 3% dos clientes que não vão cancelar com ofertas de retenção. Isso é bom para a experiência do cliente, mas não resolve o problema principal.

Lições Aprendidas

Acurácia é enganosa em dados desbalanceados e essa é talvez a lição mais importante. Sempre olhe para sensibilidade, especificidade e F1-score quando trabalhar com dados desbalanceados. A acurácia balanceada pode ser uma métrica muito mais justa para avaliar o desempenho real do modelo. No nosso caso, a acurácia balanceada foi de apenas 60,5%, muito diferente dos 82% de acurácia tradicional que poderiam nos enganar. A fórmula da acurácia balanceada é simples: ela é a média entre sensibilidade e especificidade, dando peso igual para ambas as classes, independentemente de quantas observações cada uma possui.

\[\text{Acurácia Balanceada} = \frac{\text{Sensibilidade} + \text{Especificidade}}{2} = \frac{0.24 + 0.97}{2} = 0.605\]

Threshold otimização é crucial para modelos com dados desbalanceados. O threshold padrão de 0.5 pode não ser o ideal quando você tem classes desbalanceadas. Use a curva ROC para encontrar o ponto ótimo que equilibra sensibilidade e especificidade de acordo com suas necessidades de negócio. Às vezes, vale a pena aceitar mais falsos positivos para capturar mais verdadeiros positivos, especialmente quando o custo de perder um cliente é alto. A otimização do threshold deve sempre considerar o contexto do problema e os custos associados a cada tipo de erro.

Técnicas de balanceamento podem ajudar significativamente a melhorar a sensibilidade do modelo. SMOTE (Synthetic Minority Oversampling Technique), undersampling da classe majoritária, ou class weights podem melhorar a capacidade do modelo de identificar a classe minoritária. No entanto, sempre valide essas técnicas no conjunto de teste para garantir que a melhoria não seja apenas um artefato do overfitting. É importante lembrar que essas técnicas não criam informação nova, apenas redistribuem o peso das classes durante o treinamento, o que pode ser suficiente para melhorar o desempenho em problemas específicos.

Contexto de negócio importa mais do que qualquer métrica técnica isolada. Se o custo de um falso negativo (perder um cliente que realmente vai cancelar) é muito maior que o custo de um falso positivo (oferecer retenção desnecessária para um cliente que não vai cancelar), você deve otimizar para sensibilidade, mesmo que isso reduza a especificidade. No caso do Porto Seguro, perder um cliente pode significar milhares de reais em receita perdida, enquanto oferecer retenção desnecessária tem um custo muito menor. Portanto, vale a pena “incomodar” alguns clientes que não vão cancelar para não perder aqueles que realmente vão cancelar.

Conclusão

O Porto Seguro Data Challenge me ensinou que 82% de acurácia pode ser um fracasso completo quando o contexto importa. Em problemas de dados desbalanceados, as métricas tradicionais podem nos enganar, levando a decisões de negócio catastróficas.

A próxima vez que alguém te apresentar um modelo com alta acurácia, pergunte: “E a sensibilidade? E o custo de cada tipo de erro?” A resposta pode surpreender você.

No mundo real, identificar corretamente 24% dos clientes que vão cancelar pode ser melhor que não identificar nenhum. Mas também pode ser um sinal de que precisamos repensar nossa abordagem, nossos dados, ou até mesmo nossa pergunta de negócio.

Afinal, às vezes o problema não está no modelo. Está na pergunta que estamos fazendo.