8  Estudo de Caso Completo

Neste capítulo, vamos organizar um fluxo de trabalho mais próximo do que acontece em um projeto real. O problema escolhido será previsão de churn, isto é, estimar se um cliente tende a cancelar um serviço.

O foco aqui não é apenas treinar um modelo, mas estruturar o pensamento:

8.1 Contexto do problema

Em churn, geralmente queremos identificar clientes com maior risco de cancelamento para agir antes da perda acontecer. Uma árvore de decisão é particularmente útil nesse contexto porque transforma o problema em regras compreensíveis para equipes de negócio.

8.2 Passo 1: gerando uma base sintética plausível

import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score, f1_score, classification_report

np.random.seed(42)
n = 1000

df = pd.DataFrame({
    "idade": np.random.randint(18, 70, n),
    "tempo_cliente": np.random.randint(1, 120, n),
    "gasto_mensal": np.random.normal(120, 40, n).clip(20, 400),
    "suporte_chamados": np.random.poisson(2, n),
    "atrasos_pagamento": np.random.poisson(1, n),
    "usa_app": np.random.binomial(1, 0.7, n),
})

logit = (
    -2.2
    + 0.012 * df["idade"]
    - 0.022 * df["tempo_cliente"]
    + 0.010 * df["gasto_mensal"]
    + 0.35 * df["suporte_chamados"]
    + 0.45 * df["atrasos_pagamento"]
    - 0.60 * df["usa_app"]
)

prob = 1 / (1 + np.exp(-logit))
df["churn"] = (np.random.rand(n) < prob).astype(int)

print(df.head())
print(df["churn"].mean())
   idade  tempo_cliente  gasto_mensal  suporte_chamados  atrasos_pagamento  \
0     56             99    121.149793                 5                  0   
1     69            115    171.138075                 1                  0   
2     46             15    127.643963                 1                  1   
3     32             64    121.857462                 4                  0   
4     60             89     65.605754                 1                  0   

   usa_app  churn  
0        1      0  
1        1      1  
2        1      0  
3        1      0  
4        0      0  
0.307

8.3 Passo 2: separando atributos e alvo

X = df.drop(columns=["churn"])
y = df["churn"]

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.25,
    random_state=42,
    stratify=y
)

print(X_train.shape, X_test.shape)
(750, 6) (250, 6)

8.4 Passo 3: treinando uma árvore interpretável

clf = DecisionTreeClassifier(
    max_depth=5,
    min_samples_leaf=15,
    random_state=42
)
clf.fit(X_train, y_train)

pred = clf.predict(X_test)
print("Accuracy:", accuracy_score(y_test, pred))
print("F1:", f1_score(y_test, pred))
print(classification_report(y_test, pred))
Accuracy: 0.74
F1: 0.4036697247706422
              precision    recall  f1-score   support

           0       0.75      0.94      0.83       173
           1       0.69      0.29      0.40        77

    accuracy                           0.74       250
   macro avg       0.72      0.61      0.62       250
weighted avg       0.73      0.74      0.70       250

Esses hiperparâmetros foram escolhidos para equilibrar clareza e desempenho. Em problemas de negócio, uma árvore um pouco mais compacta pode ser mais útil do que uma estrutura enorme com pequenas vantagens marginais.

8.5 Passo 4: lendo a importância das variáveis

importances = pd.Series(clf.feature_importances_, index=X.columns)
print(importances.sort_values(ascending=False))
tempo_cliente        0.528235
suporte_chamados     0.186245
gasto_mensal         0.180747
idade                0.049257
atrasos_pagamento    0.041453
usa_app              0.014064
dtype: float64

8.5.1 Interpretação

Se suporte_chamados e atrasos_pagamento aparecem no topo, isso sugere que experiência ruim e atrito operacional estão associados ao cancelamento. Se tempo_cliente tiver importância alta, o modelo pode estar distinguindo clientes ainda pouco fidelizados.

8.6 Passo 5: visualizando a árvore

import matplotlib.pyplot as plt
from sklearn.tree import plot_tree

plt.figure(figsize=(18, 10))
plot_tree(
    clf,
    feature_names=X.columns,
    class_names=["permanece", "churn"],
    filled=True,
    rounded=True,
    fontsize=9
)
plt.show()

Ao olhar a árvore, tente responder:

  • qual variável foi para a raiz?
  • quais pontos de corte apareceram?
  • existem folhas muito pequenas?
  • as regras parecem coerentes com o domínio?

8.7 Passo 6: extraindo regras textuais

from sklearn.tree import export_text

print(export_text(clf, feature_names=list(X.columns)))
|--- tempo_cliente <= 58.50
|   |--- suporte_chamados <= 1.50
|   |   |--- gasto_mensal <= 113.12
|   |   |   |--- idade <= 54.00
|   |   |   |   |--- gasto_mensal <= 74.36
|   |   |   |   |   |--- class: 0
|   |   |   |   |--- gasto_mensal >  74.36
|   |   |   |   |   |--- class: 0
|   |   |   |--- idade >  54.00
|   |   |   |   |--- class: 0
|   |   |--- gasto_mensal >  113.12
|   |   |   |--- tempo_cliente <= 50.00
|   |   |   |   |--- tempo_cliente <= 35.50
|   |   |   |   |   |--- class: 0
|   |   |   |   |--- tempo_cliente >  35.50
|   |   |   |   |   |--- class: 0
|   |   |   |--- tempo_cliente >  50.00
|   |   |   |   |--- class: 1
|   |--- suporte_chamados >  1.50
|   |   |--- gasto_mensal <= 107.16
|   |   |   |--- tempo_cliente <= 24.50
|   |   |   |   |--- idade <= 48.50
|   |   |   |   |   |--- class: 0
|   |   |   |   |--- idade >  48.50
|   |   |   |   |   |--- class: 1
|   |   |   |--- tempo_cliente >  24.50
|   |   |   |   |--- gasto_mensal <= 94.44
|   |   |   |   |   |--- class: 0
|   |   |   |   |--- gasto_mensal >  94.44
|   |   |   |   |   |--- class: 0
|   |   |--- gasto_mensal >  107.16
|   |   |   |--- tempo_cliente <= 36.50
|   |   |   |   |--- atrasos_pagamento <= 0.50
|   |   |   |   |   |--- class: 0
|   |   |   |   |--- atrasos_pagamento >  0.50
|   |   |   |   |   |--- class: 1
|   |   |   |--- tempo_cliente >  36.50
|   |   |   |   |--- suporte_chamados <= 2.50
|   |   |   |   |   |--- class: 0
|   |   |   |   |--- suporte_chamados >  2.50
|   |   |   |   |   |--- class: 1
|--- tempo_cliente >  58.50
|   |--- gasto_mensal <= 132.95
|   |   |--- idade <= 42.50
|   |   |   |--- idade <= 26.50
|   |   |   |   |--- atrasos_pagamento <= 0.50
|   |   |   |   |   |--- class: 0
|   |   |   |   |--- atrasos_pagamento >  0.50
|   |   |   |   |   |--- class: 0
|   |   |   |--- idade >  26.50
|   |   |   |   |--- gasto_mensal <= 79.51
|   |   |   |   |   |--- class: 0
|   |   |   |   |--- gasto_mensal >  79.51
|   |   |   |   |   |--- class: 0
|   |   |--- idade >  42.50
|   |   |   |--- suporte_chamados <= 1.50
|   |   |   |   |--- gasto_mensal <= 96.07
|   |   |   |   |   |--- class: 0
|   |   |   |   |--- gasto_mensal >  96.07
|   |   |   |   |   |--- class: 0
|   |   |   |--- suporte_chamados >  1.50
|   |   |   |   |--- gasto_mensal <= 118.71
|   |   |   |   |   |--- class: 0
|   |   |   |   |--- gasto_mensal >  118.71
|   |   |   |   |   |--- class: 0
|   |--- gasto_mensal >  132.95
|   |   |--- suporte_chamados <= 0.50
|   |   |   |--- class: 0
|   |   |--- suporte_chamados >  0.50
|   |   |   |--- usa_app <= 0.50
|   |   |   |   |--- gasto_mensal <= 155.19
|   |   |   |   |   |--- class: 0
|   |   |   |   |--- gasto_mensal >  155.19
|   |   |   |   |   |--- class: 0
|   |   |   |--- usa_app >  0.50
|   |   |   |   |--- gasto_mensal <= 160.22
|   |   |   |   |   |--- class: 0
|   |   |   |   |--- gasto_mensal >  160.22
|   |   |   |   |   |--- class: 0

Esse formato ajuda a comunicar o resultado para pessoas não técnicas.

8.8 Passo 7: comparando com uma árvore mais profunda

complex_clf = DecisionTreeClassifier(random_state=42)
complex_clf.fit(X_train, y_train)

pred_complex = complex_clf.predict(X_test)
print("Accuracy árvore livre:", accuracy_score(y_test, pred_complex))
print("F1 árvore livre:", f1_score(y_test, pred_complex))
print("Depth árvore livre:", complex_clf.get_depth())
print("Folhas árvore livre:", complex_clf.get_n_leaves())
Accuracy árvore livre: 0.688
F1 árvore livre: 0.4657534246575342
Depth árvore livre: 16
Folhas árvore livre: 176

A comparação evidencia um ponto importante do livro inteiro: nem sempre a árvore mais complexa é a mais interessante.

8.9 Passo 8: validação de negócio

Em projetos reais, score não basta. É preciso verificar se as regras são acionáveis.

Exemplos de perguntas úteis:

  • clientes com muitos chamados estão recebendo suporte insuficiente?
  • atrasos em pagamento refletem dificuldade financeira ou falha operacional?
  • usuários que não usam o app estão menos engajados com o serviço?
  • clientes novos precisam de onboarding melhor?

8.10 Passo 9: possíveis ações práticas

Se a árvore identificar grupos de alto risco, algumas ações podem ser desenhadas:

  • campanha de retenção para clientes com alto gasto e muitos chamados;
  • oferta de suporte proativo para clientes novos;
  • automação de cobrança e regularização para perfis com atrasos;
  • incentivo ao uso do aplicativo para aumentar engajamento.

8.11 Passo 10: próximos refinamentos

Em um projeto real, poderíamos ainda:

  • usar validação cruzada para ajustar hiperparâmetros;
  • avaliar desbalanceamento de classes;
  • testar poda com ccp_alpha;
  • comparar com Random Forest e Gradient Boosting;
  • calibrar probabilidades, se necessário.
NoteResumo
  • Um estudo de caso mostra a árvore como ferramenta de previsão e de descoberta de regras.
  • A interpretabilidade ajuda a ligar score a ação de negócio.
  • Comparar uma árvore compacta com uma árvore livre é uma forma concreta de estudar complexidade.
  • O valor prático da árvore aumenta quando as regras se tornam acionáveis.

Este estudo de caso mostrou que uma árvore de decisão pode servir ao mesmo tempo como modelo preditivo e ferramenta de descoberta de regras. Em contextos empresariais, essa combinação é poderosa porque permite transformar previsões em ações concretas. No último capítulo, vamos consolidar o aprendizado com exercícios práticos.

WarningErros comuns
  • focar apenas em score e ignorar a utilidade operacional das regras;
  • interpretar importância de atributo sem contexto de negócio;
  • construir uma árvore excessivamente detalhada para um público não técnico;
  • deixar de validar se a regra faz sentido fora da amostra sintética.
TipPerguntas de revisão
  1. Por que churn é um problema interessante para árvores de decisão?
  2. O que uma variável importante sugere, e o que ela não prova?
  3. Por que uma árvore mais simples pode ser mais útil para negócio?
  4. Que tipo de ação prática pode surgir da leitura das regras?