Objetivo
Entender como árvores de decisão funcionam internamente, seus critérios de divisão e limitações, e como o Random Forest resolve essas limitações através de ensemble learning. Implementar ambos com Scikit-learn e interpretar os resultados.
1. Por que Árvores de Decisão são Importantes?
Árvores de decisão são um dos algoritmos mais usados em produção por três razões principais:
- Interpretabilidade: você pode explicar cada predição como uma série de regras if/else
- Versatilidade: funcionam para classificação e regressão, com features numéricas e categóricas
- Base para algoritmos poderosos: Random Forest, Gradient Boosting e XGBoost são todos conjuntos de árvores
Em competições de ML com dados tabulares (Kaggle), modelos baseados em árvores vencem a maioria das competições.
2. Como Funciona uma Árvore de Decisão
Uma árvore de decisão divide recursivamente o espaço de features fazendo perguntas binárias do tipo "feature X > valor V?". Cada divisão cria dois galhos, até que os dados em cada folha sejam suficientemente puros.
Exemplo: prever se um cliente vai comprar (1) ou não (0)
[renda > 5000?]
/ \
Sim Não
/ \
[satisfacao >= 3?] prediz: Não (0)
/ \
Sim Não
| |
prediz:Sim(1) prediz:Não(0)
O modelo aprende quais perguntas fazer e quais valores usar como limiar.
3. Critérios de Divisão
Como o algoritmo decide qual feature e qual limiar usar em cada nó? Ele testa todas as combinações possíveis e escolhe a que resulta nos grupos mais "puros".
3.1 Impureza de Gini
O critério padrão para classificação no Scikit-learn:
Gini(nó) = 1 - Σ pᵢ²
onde pᵢ é a proporção de exemplos da classe i no nó.
Gini = 0: nó puro (todos da mesma classe)
Gini = 0.5: nó completamente impuro (50/50 entre duas classes)
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.tree import (DecisionTreeClassifier, DecisionTreeRegressor,
plot_tree, export_text)
from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import (accuracy_score, f1_score, r2_score,
classification_report, confusion_matrix,
mean_absolute_error)
import warnings
warnings.filterwarnings("ignore")
np.random.seed(42)
sns.set_theme(style="whitegrid")
def calcular_gini(proporcoes: list) -> float:
"""Calcula impureza de Gini dado lista de proporções."""
return 1 - sum(p**2 for p in proporcoes)
def calcular_entropia(proporcoes: list) -> float:
"""Calcula entropia dado lista de proporções."""
return -sum(p * np.log2(p + 1e-10) for p in proporcoes)
# Comparando os critérios
proporcao_classe1 = np.linspace(0.001, 0.999, 200)
proporcao_classe0 = 1 - proporcao_classe1
ginis = [calcular_gini([p0, p1])
for p0, p1 in zip(proporcao_classe0, proporcao_classe1)]
entropias = [calcular_entropia([p0, p1])
for p0, p1 in zip(proporcao_classe0, proporcao_classe1)]
fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(proporcao_classe1, ginis,
label="Gini", color="steelblue", linewidth=2.5)
ax.plot(proporcao_classe1, entropias,
label="Entropia (normalizada)", color="coral",
linewidth=2.5, linestyle="--")
ax.axvline(x=0.5, color="gray", linestyle=":", alpha=0.7)
ax.set_xlabel("Proporção da Classe 1")
ax.set_ylabel("Impureza")
ax.set_title("Gini vs Entropia — Critérios de Divisão")
ax.legend()
plt.tight_layout()
plt.show()
print("Exemplos de Gini:")
print(f" Nó puro (100% classe 0): {calcular_gini([1.0, 0.0]):.4f}")
print(f" Nó 50/50: {calcular_gini([0.5, 0.5]):.4f}")
print(f" Nó 80/20: {calcular_gini([0.8, 0.2]):.4f}")
print(f" Nó 90/10: {calcular_gini([0.9, 0.1]):.4f}")
Saída:
Exemplos de Gini:
Nó puro (100% classe 0): 0.0000
Nó 50/50: 0.5000
Nó 80/20: 0.3200
Nó 90/10: 0.1800
3.2 Ganho de Informação
O algoritmo calcula o ganho de impureza de cada divisão possível e escolhe a que maximiza o ganho:
Ganho = Impureza(pai) - [peso_esq × Impureza(esq) + peso_dir × Impureza(dir)]
onde peso = n_exemplos_filho / n_exemplos_pai
4. Primeira Árvore de Decisão
# Dataset: classificar risco de crédito
n = 800
df_cred = pd.DataFrame({
"renda": np.random.normal(5000, 2000, n).clip(500, 20000),
"divida": np.random.normal(2000, 1500, n).clip(0, 12000),
"historico": np.random.randint(0, 10, n).astype(float),
"tempo_emprego": np.random.randint(0, 30, n).astype(float),
"num_dependentes": np.random.randint(0, 5, n).astype(float),
})
# Regra de inadimplência
score = (df_cred["renda"] / 1000
- df_cred["divida"] / 1000
+ df_cred["historico"] * 0.5
+ df_cred["tempo_emprego"] * 0.1
- df_cred["num_dependentes"] * 0.3)
prob_inadim = 1 / (1 + np.exp(-(3 - score / 2)))
df_cred["inadimplente"] = (np.random.random(n) < prob_inadim).astype(int)
print(f"Dataset: {df_cred.shape}")
print(f"Taxa de inadimplência: {df_cred['inadimplente'].mean():.1%}")
features = ["renda", "divida", "historico",
"tempo_emprego", "num_dependentes"]
X = df_cred[features].values
y = df_cred["inadimplente"].values
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
# Árvore rasa para visualizar e interpretar
arvore_rasa = DecisionTreeClassifier(
max_depth=3,
criterion="gini",
random_state=42
)
arvore_rasa.fit(X_train, y_train)
y_pred_rasa = arvore_rasa.predict(X_test)
print(f"\nÁrvore com max_depth=3:")
print(f" Acurácia: {accuracy_score(y_test, y_pred_rasa):.4f}")
print(f" F1-Score: {f1_score(y_test, y_pred_rasa):.4f}")
print(f" Nós totais: {arvore_rasa.tree_.node_count}")
# Visualizar a árvore
fig, ax = plt.subplots(figsize=(18, 8))
plot_tree(arvore_rasa,
feature_names=features,
class_names=["Adimplente", "Inadimplente"],
filled=True,
rounded=True,
fontsize=9,
ax=ax)
ax.set_title("Árvore de Decisão — max_depth=3", fontsize=14)
plt.tight_layout()
plt.show()
# Versão em texto (mais fácil de ler em produção)
print("\nÁrvore em formato de texto:")
print(export_text(arvore_rasa, feature_names=features))
5. Overfitting em Árvores de Decisão
# Comparando diferentes profundidades
profundidades = [1, 2, 3, 5, 7, 10, 15, None] # None = sem limite
resultados_prof = []
for prof in profundidades:
arvore = DecisionTreeClassifier(
max_depth=prof, random_state=42
)
arvore.fit(X_train, y_train)
acc_treino = accuracy_score(y_train, arvore.predict(X_train))
acc_teste = accuracy_score(y_test, arvore.predict(X_test))
f1_teste = f1_score(y_test, arvore.predict(X_test))
n_nos = arvore.tree_.node_count
resultados_prof.append({
"profundidade": str(prof) if prof else "Sem limite",
"acc_treino": acc_treino,
"acc_teste": acc_teste,
"f1_teste": f1_teste,
"n_nos": n_nos
})
df_prof = pd.DataFrame(resultados_prof)
print("Efeito da profundidade na árvore:")
print(df_prof.to_string(index=False))
# Gráfico
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
profs_num = list(range(len(profundidades)))
labels = [str(p) if p else "∞" for p in profundidades]
axes[0].plot(profs_num, df_prof["acc_treino"],
label="Treino", color="steelblue",
linewidth=2, marker="o")
axes[0].plot(profs_num, df_prof["acc_teste"],
label="Teste", color="coral",
linewidth=2, marker="o")
axes[0].set_xticks(profs_num)
axes[0].set_xticklabels(labels)
axes[0].set_xlabel("Profundidade máxima")
axes[0].set_ylabel("Acurácia")
axes[0].set_title("Overfitting por Profundidade")
axes[0].legend()
axes[1].bar(profs_num, df_prof["n_nos"],
color="mediumseagreen", alpha=0.7)
axes[1].set_xticks(profs_num)
axes[1].set_xticklabels(labels)
axes[1].set_xlabel("Profundidade máxima")
axes[1].set_ylabel("Número de nós")
axes[1].set_title("Complexidade por Profundidade")
plt.tight_layout()
plt.show()
Saída:
Efeito da profundidade na árvore:
profundidade acc_treino acc_teste f1_teste n_nos
1 0.7719 0.7625 0.5854 3
2 0.7906 0.7875 0.6234 7
3 0.8250 0.8125 0.6897 15
5 0.8750 0.8250 0.7143 57
7 0.9344 0.8063 0.6809 133
10 0.9781 0.7875 0.6523 297
Sem limite 1.0000 0.7750 0.6418 547
A árvore sem limite atinge 100% no treino (memorizou cada exemplo) mas pior performance no teste — overfitting clássico.
6. Hiperparâmetros Importantes da Árvore
# Explorando os principais hiperparâmetros
print("Principais hiperparâmetros de DecisionTreeClassifier:")
print()
print("max_depth — profundidade máxima da árvore")
print(" None = sem limite (overfitting)")
print()
print("min_samples_split — mínimo de amostras para dividir um nó")
print(" valores maiores = árvore menor = menos overfitting")
print()
print("min_samples_leaf — mínimo de amostras em uma folha")
print(" valores maiores = mais regularização")
print()
print("max_features — número de features consideradas em cada split")
print(" 'sqrt' = raiz quadrada do total (padrão RF)")
print(" 'log2' = log base 2 do total")
print()
print("criterion — 'gini' (padrão) ou 'entropy'")
# Comparando hiperparâmetros
configs = [
{"max_depth": 5, "min_samples_split": 2, "min_samples_leaf": 1},
{"max_depth": 5, "min_samples_split": 20, "min_samples_leaf": 10},
{"max_depth": 7, "min_samples_split": 50, "min_samples_leaf": 25},
{"max_depth": 10,"min_samples_split": 5, "min_samples_leaf": 2},
]
print("\nComparação de configurações:")
print(f"{'max_depth':>10} {'min_split':>10} {'min_leaf':>10} "
f"{'Acc Treino':>12} {'Acc Teste':>12} {'F1 Teste':>10}")
print("-" * 68)
for cfg in configs:
arv = DecisionTreeClassifier(**cfg, random_state=42)
arv.fit(X_train, y_train)
at = accuracy_score(y_train, arv.predict(X_train))
ae = accuracy_score(y_test, arv.predict(X_test))
f1 = f1_score(y_test, arv.predict(X_test))
print(f"{cfg['max_depth']:>10} {cfg['min_samples_split']:>10} "
f"{cfg['min_samples_leaf']:>10} {at:>12.4f} {ae:>12.4f} {f1:>10.4f}")
7. Importância de Features
# Melhor configuração encontrada
arvore_final = DecisionTreeClassifier(
max_depth=5,
min_samples_split=20,
min_samples_leaf=10,
random_state=42
)
arvore_final.fit(X_train, y_train)
# Feature importance: redução média ponderada de impureza
importancias = arvore_final.feature_importances_
indices = np.argsort(importancias)[::-1]
fig, ax = plt.subplots(figsize=(9, 5))
cores = plt.cm.Blues(np.linspace(0.4, 0.9, len(features)))
barras = ax.barh(
[features[i] for i in indices],
importancias[indices],
color=cores[::-1],
edgecolor="white",
height=0.6
)
ax.set_title("Importância das Features — Árvore de Decisão")
ax.set_xlabel("Importância (redução média de Gini)")
ax.invert_yaxis()
for barra, val in zip(barras, importancias[indices]):
ax.text(val + 0.002, barra.get_y() + barra.get_height()/2,
f"{val:.4f}", va="center", fontsize=10)
plt.tight_layout()
plt.show()
print("\nImportância das features:")
for i in indices:
print(f" {features[i]:<20}: {importancias[i]:.4f}")
8. Limitações das Árvores de Decisão
Antes de introduzir o Random Forest, é importante entender por que árvores simples têm limitações:
Alta variância: pequenas mudanças nos dados de treino podem resultar em árvores completamente diferentes. Uma árvore treinada com 90% dos dados pode ser muito diferente de outra treinada com os outros 90%.
Fronteiras de decisão em ângulo reto: árvores só fazem divisões paralelas aos eixos. Para separar classes em diagonal, precisam de muitas divisões, tornando-se complexas.
Tendência ao overfitting: sem regularização adequada, árvores profundas memorizam o treino.
# Demonstrando a alta variância
acuracias_bootstrap = []
for i in range(50):
# Amostrar 80% dos dados de treino aleatoriamente
idx = np.random.choice(len(X_train),
size=int(0.8 * len(X_train)),
replace=False)
arv_boot = DecisionTreeClassifier(max_depth=10, random_state=i)
arv_boot.fit(X_train[idx], y_train[idx])
acuracias_bootstrap.append(
accuracy_score(y_test, arv_boot.predict(X_test))
)
print(f"Variação de acurácia com diferentes subsets de treino:")
print(f" Média: {np.mean(acuracias_bootstrap):.4f}")
print(f" Desvio: {np.std(acuracias_bootstrap):.4f}")
print(f" Mínimo: {np.min(acuracias_bootstrap):.4f}")
print(f" Máximo: {np.max(acuracias_bootstrap):.4f}")
Saída:
Variação de acurácia com diferentes subsets de treino:
Média: 0.7983
Desvio: 0.0261
Mínimo: 0.7375
Máximo: 0.8500
A acurácia varia muito dependendo de qual subconjunto de dados é usado — alta variância.
9. Random Forest — Ensemble de Árvores
Random Forest resolve as limitações das árvores através de dois mecanismos:
Bagging (Bootstrap Aggregating): treina múltiplas árvores, cada uma em uma amostra aleatória com reposição (bootstrap) dos dados de treino.
Aleatoriedade de features: em cada divisão de cada nó, considera apenas um subconjunto aleatório das features (tipicamente √p para classificação, p/3 para regressão).
A predição final é feita por votação majoritária (classificação) ou média (regressão).
Por que isso funciona: cada árvore comete erros diferentes e não correlacionados. Ao combinar muitas árvores com erros independentes, os erros se cancelam — o viés permanece similar, mas a variância cai dramaticamente.
# Visualizando a ideia do ensemble
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
# Uma árvore
arvore_1 = DecisionTreeClassifier(max_depth=10, random_state=1)
arvore_1.fit(X_train, y_train)
acc_1 = accuracy_score(y_test, arvore_1.predict(X_test))
# Ensemble manual de 3 árvores
preds_3 = np.column_stack([
DecisionTreeClassifier(max_depth=10, random_state=i).fit(
X_train, y_train).predict(X_test)
for i in range(3)
])
votos_3 = (preds_3.mean(axis=1) >= 0.5).astype(int)
acc_3 = accuracy_score(y_test, votos_3)
# Ensemble de 100 árvores
preds_100 = np.column_stack([
DecisionTreeClassifier(max_depth=10, random_state=i).fit(
X_train, y_train).predict(X_test)
for i in range(100)
])
votos_100 = (preds_100.mean(axis=1) >= 0.5).astype(int)
acc_100 = accuracy_score(y_test, votos_100)
titulos = [f"1 Árvore\nAcc={acc_1:.4f}",
f"3 Árvores\nAcc={acc_3:.4f}",
f"100 Árvores\nAcc={acc_100:.4f}"]
valores = [acc_1, acc_3, acc_100]
cores = ["#B0C4DE", "#6B8EB5", "#1E5799"]
for ax, titulo, val, cor in zip(axes, titulos, valores, cores):
ax.bar(["Acurácia"], [val], color=cor, alpha=0.9, width=0.4)
ax.set_ylim(0.7, 0.95)
ax.set_title(titulo)
ax.text(0, val + 0.003, f"{val:.4f}", ha="center", fontsize=12)
plt.suptitle("Efeito do Ensemble: Mais Árvores = Mais Estável", y=1.02)
plt.tight_layout()
plt.show()
10. Random Forest com Scikit-learn
# Treinando Random Forest
rf = RandomForestClassifier(
n_estimators=100, # número de árvores
max_depth=None, # None = sem limite por árvore
min_samples_split=2,
min_samples_leaf=1,
max_features="sqrt", # sqrt(n_features) por divisão
bootstrap=True, # usar bootstrap
n_jobs=-1, # usar todos os CPUs
random_state=42,
verbose=0
)
rf.fit(X_train, y_train)
y_pred_rf = rf.predict(X_test)
print("Random Forest (100 árvores):")
print(f" Acurácia: {accuracy_score(y_test, y_pred_rf):.4f}")
print(f" F1-Score: {f1_score(y_test, y_pred_rf):.4f}")
print(f"\n{classification_report(y_test, y_pred_rf, target_names=['Adimplente', 'Inadimplente'])}")
10.1 Efeito do número de árvores
n_trees_range = [1, 5, 10, 25, 50, 100, 200, 500]
accs_treino = []
accs_teste = []
f1s_teste = []
for n_trees in n_trees_range:
rf_n = RandomForestClassifier(
n_estimators=n_trees,
random_state=42,
n_jobs=-1
)
rf_n.fit(X_train, y_train)
accs_treino.append(accuracy_score(y_train, rf_n.predict(X_train)))
accs_teste.append(accuracy_score(y_test, rf_n.predict(X_test)))
f1s_teste.append(f1_score(y_test, rf_n.predict(X_test)))
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
axes[0].semilogx(n_trees_range, accs_treino,
label="Treino", color="steelblue",
linewidth=2, marker="o")
axes[0].semilogx(n_trees_range, accs_teste,
label="Teste", color="coral",
linewidth=2, marker="o")
axes[0].set_xlabel("Número de Árvores (escala log)")
axes[0].set_ylabel("Acurácia")
axes[0].set_title("Acurácia vs Número de Árvores")
axes[0].legend()
axes[1].semilogx(n_trees_range, f1s_teste,
color="mediumseagreen", linewidth=2, marker="o")
axes[1].set_xlabel("Número de Árvores (escala log)")
axes[1].set_ylabel("F1-Score")
axes[1].set_title("F1-Score vs Número de Árvores")
plt.tight_layout()
plt.show()
10.2 Out-of-Bag Error
Uma vantagem do Random Forest é o OOB score — uma estimativa gratuita de generalização, sem precisar de um conjunto de validação separado.
Em cada bootstrap, cerca de 37% dos exemplos não são incluídos na amostra (out-of-bag). Esses exemplos podem ser usados para avaliar cada árvore sem data leakage.
rf_oob = RandomForestClassifier(
n_estimators=200,
oob_score=True, # habilita OOB
random_state=42,
n_jobs=-1
)
rf_oob.fit(X_train, y_train)
print(f"OOB Score (estimativa sem usar conjunto de validação):")
print(f" OOB Accuracy: {rf_oob.oob_score_:.4f}")
print(f" Test Accuracy: {accuracy_score(y_test, rf_oob.predict(X_test)):.4f}")
print(f"\n Os dois devem ser próximos — OOB é uma boa estimativa!")
10.3 Feature Importance no Random Forest
importancias_rf = rf.feature_importances_
indices_rf = np.argsort(importancias_rf)[::-1]
std_importancias = np.std(
[tree.feature_importances_ for tree in rf.estimators_], axis=0
)
fig, ax = plt.subplots(figsize=(9, 5))
barras = ax.barh(
[features[i] for i in indices_rf],
importancias_rf[indices_rf],
xerr=std_importancias[indices_rf],
color="steelblue",
alpha=0.8,
edgecolor="white",
height=0.6,
capsize=4
)
ax.set_title("Importância das Features — Random Forest\n(com desvio padrão entre árvores)")
ax.set_xlabel("Importância Média (± desvio entre árvores)")
ax.invert_yaxis()
plt.tight_layout()
plt.show()
print("\nImportância das features (Random Forest):")
for i in indices_rf:
print(f" {features[i]:<20}: {importancias_rf[i]:.4f} "
f"(± {std_importancias[i]:.4f})")
11. Regressão com Árvore e Random Forest
Árvores também funcionam para regressão. Em vez de votar na classe majoritária, as folhas retornam a média dos valores de treino que chegaram ali.
# Dataset de regressão: prever preço de imóveis
np.random.seed(42)
n_reg = 600
X_reg = np.column_stack([
np.random.uniform(40, 300, n_reg), # area
np.random.randint(1, 6, n_reg), # quartos
np.random.uniform(1, 50, n_reg), # distancia
np.random.randint(0, 40, n_reg), # idade
])
y_reg = (1500 * X_reg[:, 0]
+ 20000 * X_reg[:, 1]
- 1000 * X_reg[:, 2]
- 300 * X_reg[:, 3]
+ 60000
+ np.random.normal(0, 20000, n_reg))
X_tr_r, X_te_r, y_tr_r, y_te_r = train_test_split(
X_reg, y_reg, test_size=0.2, random_state=42
)
# Árvore de regressão
arv_reg = DecisionTreeRegressor(max_depth=5, random_state=42)
arv_reg.fit(X_tr_r, y_tr_r)
y_pred_arv = arv_reg.predict(X_te_r)
# Random Forest para regressão
rf_reg = RandomForestRegressor(
n_estimators=100,
max_depth=None,
random_state=42,
n_jobs=-1
)
rf_reg.fit(X_tr_r, y_tr_r)
y_pred_rf_r = rf_reg.predict(X_te_r)
print("Regressão — Comparação de Modelos:")
print(f"\n{'Modelo':<25} {'R²':>8} {'MAE (R$)':>12} {'RMSE (R$)':>12}")
print("-" * 60)
from sklearn.metrics import mean_squared_error
for nome, pred in [("Árvore (depth=5)", y_pred_arv),
("Random Forest", y_pred_rf_r)]:
r2 = r2_score(y_te_r, pred)
mae = mean_absolute_error(y_te_r, pred)
rmse = np.sqrt(mean_squared_error(y_te_r, pred))
print(f"{nome:<25} {r2:>8.4f} {mae:>12,.0f} {rmse:>12,.0f}")
12. Comparação Final: Árvore vs Random Forest vs Regressão Logística
from sklearn.linear_model import LogisticRegression
modelos = {
"Regressão Logística": LogisticRegression(max_iter=1000, random_state=42),
"Árvore (depth=3)": DecisionTreeClassifier(max_depth=3, random_state=42),
"Árvore (depth=5)": DecisionTreeClassifier(max_depth=5, random_state=42),
"Árvore (sem limite)": DecisionTreeClassifier(random_state=42),
"Random Forest (50)": RandomForestClassifier(n_estimators=50,
random_state=42, n_jobs=-1),
"Random Forest (200)": RandomForestClassifier(n_estimators=200,
random_state=42, n_jobs=-1),
}
# Normalizar apenas para Regressão Logística
scaler_comp = StandardScaler()
X_train_s = scaler_comp.fit_transform(X_train)
X_test_s = scaler_comp.transform(X_test)
print("Comparação completa de modelos:")
print(f"\n{'Modelo':<25} {'Acc Treino':>12} {'Acc Teste':>12} "
f"{'F1 Teste':>10} {'CV Score':>10}")
print("-" * 72)
for nome, modelo in modelos.items():
usa_scaler = "Logística" in nome
Xtr = X_train_s if usa_scaler else X_train
Xte = X_test_s if usa_scaler else X_test
modelo.fit(Xtr, y_train)
acc_tr = accuracy_score(y_train, modelo.predict(Xtr))
acc_te = accuracy_score(y_test, modelo.predict(Xte))
f1_te = f1_score(y_test, modelo.predict(Xte))
# Cross-validation no treino
cv_scores = cross_val_score(modelo, Xtr, y_train,
cv=5, scoring="f1", n_jobs=-1)
cv_mean = cv_scores.mean()
print(f"{nome:<25} {acc_tr:>12.4f} {acc_te:>12.4f} "
f"{f1_te:>10.4f} {cv_mean:>10.4f}")
Saída:
Comparação completa de modelos:
Acc Treino Acc Teste F1 Teste CV Score
────────────────────────────────────────────────────────────────────────
Regressão Logística 0.8328 0.8250 0.7000 0.6854
Árvore (depth=3) 0.8250 0.8125 0.6897 0.6732
Árvore (depth=5) 0.8750 0.8250 0.7143 0.6901
Árvore (sem limite) 1.0000 0.7750 0.6418 0.6215
Random Forest (50) 1.0000 0.8500 0.7500 0.7213
Random Forest (200) 1.0000 0.8563 0.7568 0.7287
13. Exemplo Completo: Pipeline com Random Forest
import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.metrics import (accuracy_score, f1_score,
classification_report,
ConfusionMatrixDisplay)
import matplotlib.pyplot as plt
np.random.seed(42)
print("=" * 55)
print("PIPELINE COMPLETO — RANDOM FOREST")
print("=" * 55)
# ── Dataset: churn de telecomunicações simulado ─────────
n = 1000
df_telecom = pd.DataFrame({
"meses_contrato": np.random.randint(1, 72, n),
"chamadas_mes": np.random.randint(0, 50, n),
"dados_gb": np.random.uniform(0, 30, n).round(1),
"valor_mensal": np.random.uniform(30, 200, n).round(2),
"num_reclamacoes": np.random.randint(0, 10, n),
"tem_dependentes": np.random.randint(0, 2, n),
"contrato_anual": np.random.randint(0, 2, n),
"suporte_tecnico": np.random.randint(0, 2, n),
})
# Regra de churn
score_churn = (
-0.03 * df_telecom["meses_contrato"]
+ 0.5 * df_telecom["num_reclamacoes"]
- 1.5 * df_telecom["contrato_anual"]
- 0.8 * df_telecom["suporte_tecnico"]
+ 0.01 * df_telecom["valor_mensal"]
)
prob_churn = 1 / (1 + np.exp(-score_churn))
df_telecom["churn"] = (
np.random.random(n) < prob_churn
).astype(int)
print(f"\n[1] Dataset: {df_telecom.shape}")
print(f" Taxa de churn: {df_telecom['churn'].mean():.1%}")
# ── Preparação ──────────────────────────────────────────
features_tc = [c for c in df_telecom.columns if c != "churn"]
X_tc = df_telecom[features_tc].values
y_tc = df_telecom["churn"].values
X_tr_tc, X_te_tc, y_tr_tc, y_te_tc = train_test_split(
X_tc, y_tc, test_size=0.2, random_state=42, stratify=y_tc
)
print(f"\n[2] Treino: {len(X_tr_tc)}, Teste: {len(X_te_tc)}")
# ── Treinamento ─────────────────────────────────────────
rf_telecom = RandomForestClassifier(
n_estimators=200,
max_depth=None,
min_samples_leaf=5,
max_features="sqrt",
oob_score=True,
random_state=42,
n_jobs=-1
)
rf_telecom.fit(X_tr_tc, y_tr_tc)
y_pred_tc = rf_telecom.predict(X_te_tc)
y_proba_tc = rf_telecom.predict_proba(X_te_tc)[:, 1]
print(f"\n[3] Resultados:")
print(f" OOB Score: {rf_telecom.oob_score_:.4f}")
print(f" Acc Teste: {accuracy_score(y_te_tc, y_pred_tc):.4f}")
print(f" F1 Teste: {f1_score(y_te_tc, y_pred_tc):.4f}")
# CV
cv = cross_val_score(rf_telecom, X_tr_tc, y_tr_tc,
cv=5, scoring="f1", n_jobs=-1)
print(f" CV F1 (5-fold): {cv.mean():.4f} ± {cv.std():.4f}")
print(f"\n[4] Relatório completo:")
print(classification_report(y_te_tc, y_pred_tc,
target_names=["Ficou", "Cancelou"]))
# ── Visualizações ───────────────────────────────────────
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# Importância das features
imp = rf_telecom.feature_importances_
idx = np.argsort(imp)
axes[0].barh([features_tc[i] for i in idx],
imp[idx], color="steelblue",
alpha=0.8, edgecolor="white")
axes[0].set_title("Importância das Features")
axes[0].set_xlabel("Importância")
# Matriz de confusão
ConfusionMatrixDisplay.from_predictions(
y_te_tc, y_pred_tc,
display_labels=["Ficou", "Cancelou"],
cmap="Blues",
ax=axes[1]
)
axes[1].set_title("Matriz de Confusão")
plt.suptitle("Random Forest — Churn Telecom", fontsize=14)
plt.tight_layout()
plt.show()
# ── Predição em novos clientes ──────────────────────────
novos_clientes = np.array([
[3, 30, 15.0, 89.90, 5, 0, 0, 0], # alto risco
[48, 8, 10.5, 120.00, 0, 1, 1, 1], # baixo risco
])
probs = rf_telecom.predict_proba(novos_clientes)[:, 1]
print(f"\n[5] Predição para novos clientes:")
for i, prob in enumerate(probs):
risco = "ALTO" if prob > 0.5 else "BAIXO"
print(f" Cliente {i+1}: P(churn) = {prob:.1%} → Risco {risco}")
Saída final:
[5] Predição para novos clientes:
Cliente 1: P(churn) = 78.5% → Risco ALTO
Cliente 2: P(churn) = 12.3% → Risco BAIXO
Resumo da Aula
- Árvores de decisão dividem recursivamente o espaço usando perguntas binárias
- Gini e Entropia medem impureza do nó — a divisão que maximiza o ganho é escolhida
- Profundidade irrestrita causa overfitting —
max_depth,min_samples_leafcontrolam isso - Feature importance mede a redução média ponderada de impureza por feature
- Árvores têm alta variância: pequenas mudanças no treino geram árvores muito diferentes
- Random Forest combina bagging (bootstrap) com aleatoriedade de features
- Bagging reduz variância sem aumentar muito o viés — principal benefício do RF
- OOB score é uma estimativa gratuita de generalização, sem conjunto de validação
n_estimatorsmaior = mais estável, mas rendimentos decrescentes após ~100-200 árvores- Random Forest é robusto, não precisa de normalização e lida bem com outliers
- Para dados tabulares, Random Forest é frequentemente a melhor primeira escolha
Exercícios
-
Explique o que é impureza de Gini e como ela é usada para decidir a melhor divisão em uma árvore de decisão. Por que queremos minimizá-la?
✓ Resposta:Impureza de Gini mede o quão "misturadas" estão as classes em um nó. Matematicamente é
1 - Σpᵢ², onde pᵢ é a proporção de cada classe. Um nó puro (todos da mesma classe) tem Gini=0. Um nó completamente misturado (50/50 entre duas classes) tem Gini=0.5.Para decidir a melhor divisão, o algoritmo testa todas as features e todos os possíveis limiares, calculando o ganho de Gini de cada divisão: quanto a divisão reduz a impureza média ponderada dos nós filhos em relação ao nó pai. A divisão com maior ganho é escolhida.
Queremos minimizar a impureza porque nós puros fazem predições mais confiantes e corretas. Se um nó tem 95% de exemplos de uma classe, prever sempre essa classe resulta em apenas 5% de erro. Se o nó tem 50/50, qualquer predição estará errada metade das vezes.
-
Uma árvore de decisão treinada sem limite de profundidade atingiu 100% de acurácia no treino mas apenas 72% no teste. Quais são as causas e quais hiperparâmetros você ajustaria para melhorar a generalização?
✓ Resposta:A causa é overfitting clássico: sem limite de profundidade, a árvore cria um nó separado para cada exemplo de treino (ou pequenos grupos), memorizando o ruído em vez dos padrões reais. Ela aprende "regras" específicas demais que não generalizam.
Os hiperparâmetros a ajustar, em ordem de impacto:
max_depth: o mais direto. Comece testando valores de 3 a 10 com validação cruzada.min_samples_leaf: aumentar para 5, 10 ou 20 força cada folha a ter pelo menos N exemplos, evitando folhas com apenas 1-2 pontos (puro memorização).min_samples_split: aumentar para 10-50 evita dividir nós com poucos exemplos, onde a divisão pode ser baseada em ruído.max_features: limitar o número de features consideradas em cada divisão adiciona aleatoriedade que reduz overfitting.A abordagem correta é usar GridSearchCV ou RandomizedSearchCV para encontrar a combinação ótima via validação cruzada, em vez de ajustar um hiperparâmetro por vez.
-
Explique o conceito de bagging (Bootstrap Aggregating) com suas próprias palavras. Por que combinar muitas árvores ruins pode resultar em um modelo muito melhor do que uma árvore boa?
✓ Resposta:Bagging (Bootstrap Aggregating) é como consultar muitos especialistas independentes em vez de um só. Cada especialista treina em uma amostra levemente diferente dos dados (obtida por sorteio com reposição do conjunto original), então comete erros ligeiramente diferentes. A decisão final é tomada por votação majoritária.
Por que muitas árvores ruins superam uma árvore boa: cada árvore individual comete erros, mas esses erros são não correlacionados — cada árvore "erra diferente" por ter sido treinada em dados ligeiramente diferentes. Quando muitas árvores votam, os erros individuais se cancelam (porque estão na direção errada diferentes para cada árvore), mas os acertos se reforçam (porque a resposta certa é consistente). Matematicamente, a variância de uma média de N variáveis independentes é N vezes menor que a variância de cada uma. O viés não muda muito, mas a variância cai com √N. É exatamente o princípio da sabedoria das multidões.
-
Qual a diferença entre feature importance em uma árvore de decisão simples e em um Random Forest? Qual é mais confiável e por quê?
✓ Resposta:Em uma árvore de decisão simples, a feature importance é calculada em uma única árvore: mede a redução média ponderada de impureza causada por cada feature ao longo de todas as divisões onde ela foi usada. O resultado pode ser instável: se você treinar duas árvores com dados levemente diferentes, as importâncias podem ser muito diferentes.
No Random Forest, a feature importance é calculada como a média das importâncias em todas as N árvores do ensemble. Isso a torna muito mais estável e confiável. O desvio padrão entre as árvores também pode ser calculado, dando uma estimativa de incerteza da importância.
O Random Forest é mais confiável por dois motivos: primeiro, média sobre 100-200 árvores cancela as flutuações aleatórias de cada árvore individual. Segundo, como cada árvore usa subconjuntos diferentes de dados e features, a importância não é dominada por peculiaridades do conjunto de treino específico.
Uma limitação de ambos: features correlacionadas tendem a ter sua importância dividida entre elas — se duas features carregam a mesma informação, cada uma parece menos importante do que seria se estivesse sozinha.
-
O que é o OOB Score do Random Forest? Por que ele é uma estimativa válida de generalização sem precisar de um conjunto de validação separado?
✓ Resposta:O OOB Score (Out-of-Bag Score) é possível porque o bagging usa amostras bootstrap com reposição. Estatisticamente, cada amostra bootstrap inclui em média 63.2% dos exemplos originais, deixando os outros 36.8% de fora (out-of-bag).
Para cada exemplo do treino, existem várias árvores que não o usaram no treinamento (pois ele ficou fora do bootstrap dessas árvores). O OOB Score avalia cada exemplo usando apenas as árvores que não o viram durante o treino, calculando a predição de consenso dessas árvores e comparando com o label real.
Isso é válido como estimativa de generalização porque, do ponto de vista de cada árvore individual, os exemplos OOB são genuinamente "novos" — ela nunca os viu durante o treinamento. É funcionalmente equivalente a uma validação cruzada, sem o custo computacional de treinar o modelo N vezes.
-
Compare Regressão Logística e Random Forest em termos de: interpretabilidade, necessidade de normalização, capacidade de capturar relações não lineares, robustez a outliers e velocidade de treinamento. Quando você escolheria cada um?
✓ Resposta:Interpretabilidade: Regressão Logística é mais interpretável — cada coeficiente tem um significado direto (impacto no log-odds). Random Forest é uma "caixa preta" — você sabe quais features são importantes mas não como elas interagem.
Normalização: Regressão Logística precisa de normalização (gradiente descendente sensível a escala). Random Forest não precisa — árvores só fazem comparações ordinais (feature > limiar), então escala não afeta o resultado.
Relações não lineares: Regressão Logística só captura relações lineares (fronteira de decisão linear). Random Forest captura relações não lineares e interações complexas entre features automaticamente.
Robustez a outliers: Random Forest é muito mais robusto — divisões binárias não são afetadas por valores extremos da mesma forma que regressão, onde um outlier pode distorcer toda a reta. Lasso/Ridge ajudam a regressão, mas RF é naturalmente robusto.
Velocidade: Regressão Logística é muito mais rápida para treinar e predizer. Random Forest com 200 árvores pode ser 100x mais lento, especialmente em datasets grandes.
Escolha Regressão Logística quando: interpretabilidade é crítica (decisões de crédito reguladas, diagnóstico médico com explicação obrigatória), o dataset é muito grande e velocidade importa, você suspeita de relação linear, ou quer um baseline rápido.
Escolha Random Forest quando: performance é prioridade sobre interpretabilidade, há relações não lineares nos dados, o dataset tem outliers, ou você quer uma solução robusta com pouco ajuste de hiperparâmetros.
Referências
- Scikit-learn — Decision Trees
- Scikit-learn — Random Forest
- Scikit-learn — DecisionTreeClassifier
- Scikit-learn — RandomForestClassifier
- Real Python — Random Forest in Python
- StatQuest — Decision Trees
- StatQuest — Random Forests
- Original Random Forest Paper (Breiman, 2001)
- Interpretable ML Book (Molnar) — Trees