Random Forest
Random Forest Learning Algorithm
Os Random Forests são um método de ensemble que combina várias árvores de decisão para aumentar a precisão e a capacidade de generalização do modelo. A ideia central é introduzir aleatoriedade controlada: cada árvore é treinada sobre um bootstrap (amostra com reposição) do conjunto de dados e, a cada divisão, considera apenas um subconjunto aleatório de atributos. Essa estratégia (bagging + seleção aleatória de features) descorrela as árvores individuais, reduz a variância e torna o modelo mais robusto a ruído e overfitting. Em classificação, a predição final é dada pelo voto da maioria; em regressão, pela média das previsões.
O Random Forest oferece importâncias de atributos e a estimativa de erro out-of-bag (OOB), que funciona como uma validação embutida sem precisar de holdout extra. Entre os pontos de atenção estão o custo computacional e a menor interpretabilidade quando comparado a uma única árvore. No geral, é uma escolha sólida e versátil para dados tabulares, equilibrando baixo viés das árvores profundas com baixa variância obtida pelo ensemble.
Cars Purchase Decision
Este projeto tem como objetivo aplicar técnicas de Machine Learning para compreender os fatores que influenciam a decisão de compra de automóveis. A partir de um conjunto de dados com informações sobre idade, gênero e salário anual dos clientes, foi construída uma árvore de decisão capaz de classificar se um indivíduo provavelmente realizará a compra ou não.
Exploração dos Dados
Estatísticas Descritivas
Para o projeto foi utilizado o dataset Cars - Purchase Decision Dataset e contém detalhes de clientes que consideraram comprar um automóvel, juntamente com seus salários.
O conjunto de dados contém 1000 registros e 5 variáveis. A variável alvo é Purchased (0 = não comprou, 1 = comprou). Entre as variáveis explicativas, temos Gender (categórica), Age (numérica) e AnnualSalary (numérica).
Variáveis
-
User ID: Código do Cliente
-
Gender: Gênero do Cliente
-
Age: Idade do Cliente em anos
-
AnnualSalary: Salário anual do Cliente
-
Purchased: Se o cliente realizou a compra
Estatísticas Descritivas e Visualizações
O gráfico mostra a relação entre idade e salário dos clientes, destacando quem realizou a compra e quem não comprou:
import pandas as pd
import matplotlib.pyplot as plt
from io import BytesIO
# Carregar dataset
url = "https://raw.githubusercontent.com/EnzoMalagoli/machine-learning/refs/heads/main/data/car_data.csv"
df = pd.read_csv(url)
# --- ETAPA 1: Data Cleaning
df["Age"].fillna(df["Age"].median(), inplace=True)
df["Gender"].fillna(df["Gender"].mode()[0], inplace=True)
df["AnnualSalary"].fillna(df["AnnualSalary"].median(), inplace=True)
# --- ETAPA 2: Encoding
df["Gender"] = df["Gender"].map({"Male": 1, "Female": 0})
# --- ETAPA 3: Normalização
for col in ["Age", "AnnualSalary"]:
cmin, cmax = df[col].min(), df[col].max()
df[col] = 0.0 if cmax == cmin else (df[col] - cmin) / (cmax - cmin)
df0 = df[df["Purchased"] == 0]
df1 = df[df["Purchased"] == 1]
# --- PLOT: Dispersão Idade x Salário ---
fig, ax = plt.subplots(1, 1, figsize=(7, 5))
ax.scatter(
df0["Age"], df0["AnnualSalary"],
label="Não comprou (0)", alpha=0.4,
color="lightcoral", edgecolor="darkred", linewidth=0.8
)
ax.scatter(
df1["Age"], df1["AnnualSalary"],
label="Comprou (1)", alpha=0.4,
color="skyblue", edgecolor="navy", linewidth=0.8
)
ax.set_title("Idade x Salário por Decisão de Compra")
ax.set_xlabel("Idade")
ax.set_ylabel("Salário Anual")
ax.grid(linestyle="--", alpha=0.6)
ax.legend()
buffer = BytesIO()
plt.savefig(buffer, format="svg", bbox_inches="tight")
buffer.seek(0)
print(buffer.getvalue().decode("utf-8"))
Info
A visualização deixa claro que idade e salário exercem influência relevante no comportamento de compra
O próximo gráfico apresenta a distribuição de clientes por gênero:
import pandas as pd
import matplotlib.pyplot as plt
from io import BytesIO
# Carregar dataset
url = "https://raw.githubusercontent.com/EnzoMalagoli/machine-learning/refs/heads/main/data/car_data.csv"
df = pd.read_csv(url)
# --- ETAPA 1: Data Cleaning
df["Gender"].fillna(df["Gender"].mode()[0], inplace=True)
counts = df["Gender"].value_counts()
# --- PLOT: Distribuição por Gênero ---
fig, ax = plt.subplots(1, 1, figsize=(6, 4))
ax.bar(
counts.index, counts.values,
color=["pink", "skyblue"], edgecolor="lightcoral"
)
ax.set_title("Distribuição por Gênero")
ax.set_xlabel("Gênero")
ax.set_ylabel("Quantidade")
ax.grid(axis="y", linestyle="--", alpha=0.6)
buffer = BytesIO()
plt.savefig(buffer, format="svg", bbox_inches="tight")
buffer.seek(0)
print(buffer.getvalue().decode("utf-8"))
Info
Observa-se que há uma leve predominância de mulheres no dataset.
O último gráfico apresenta a distribuição do salário anual dos clientes, permitindo visualizar a mediana, a dispersão dos valores e a presença de possíveis extremos:
import pandas as pd
import matplotlib.pyplot as plt
from io import BytesIO
# Carregar dataset
url = "https://raw.githubusercontent.com/EnzoMalagoli/machine-learning/refs/heads/main/data/car_data.csv"
df = pd.read_csv(url)
# --- ETAPA 1: Data Cleaning
df["AnnualSalary"].fillna(df["AnnualSalary"].median(), inplace=True)
# --- PLOT: Boxplot
fig, ax = plt.subplots(figsize=(7, 5))
bp = ax.boxplot(df["AnnualSalary"], patch_artist=True, widths=0.5)
for box in bp["boxes"]:
box.set(facecolor="skyblue", edgecolor="navy", linewidth=1.2)
for whisker in bp["whiskers"]:
whisker.set(color="navy", linewidth=1.2)
for cap in bp["caps"]:
cap.set(color="navy", linewidth=1.2)
for median in bp["medians"]:
median.set(color="darkred", linewidth=1.5)
ax.set_title("Distribuição do Salário Anual")
ax.set_ylabel("Salário Anual")
ax.set_xticks([])
ax.grid(axis="y", linestyle="--", alpha=0.6)
buffer = BytesIO()
plt.savefig(buffer, format="svg", bbox_inches="tight")
buffer.seek(0)
print(buffer.getvalue().decode("utf-8"))
Info
O gráfico evidencia que a maior parte dos salários está concentrada em uma faixa intermediária, entre aproximadamente 50 mil e 90 mil, com a mediana em torno de 70 mil.
Pré-processamento
Pré-processamento de dados brutos deve ser a primeira etapa ao lidar com datasets de todos tamanhos.
Data Cleaning
O processo de data cleaning garante que o conjunto utilizado seja confiável e esteja livre de falhas que possam distorcer os resultados. Consiste em identificar e corrigir problemas como valores ausentes, dados inconsistentes ou informações que não fazem sentido. Essa limpeza permite que a base seja mais fiel à realidade e forneça condições adequadas para a construção de modelos de Machine Learning.
No código, a limpeza foi feita dessa forma: possíveis valores vazios em idade, gênero e salário foram preenchidos com informações representativas, como a mediana ou o valor mais frequente.
| Gender | Age | AnnualSalary |
|---|---|---|
| Female | 53 | 90500 |
| Male | 59 | 135500 |
| Male | 55 | 39000 |
| Male | 25 | 59500 |
| Female | 36 | 63000 |
| Male | 41 | 73500 |
| Female | 47 | 42500 |
| Female | 41 | 67500 |
| Male | 32 | 77500 |
| Female | 46 | 135500 |
import pandas as pd
def preprocess(df):
df['Age'].fillna(df['Age'].median(), inplace=True)
df['Gender'].fillna(df['Gender'].mode()[0], inplace=True)
df['AnnualSalary'].fillna(df['AnnualSalary'].median(), inplace=True)
features = ['Gender', 'Age', 'AnnualSalary']
return df[features]
df = pd.read_csv('https://raw.githubusercontent.com/EnzoMalagoli/machine-learning/refs/heads/main/data/car_data.csv')
df = df.sample(n=10, random_state=42)
df = preprocess(df)
print(df.sample(n=10).to_markdown(index=False))
Encoding Categorical Variables
O processo de encoding de variáveis categóricas transforma informações em formato de texto em valores numéricos, permitindo que algoritmos de Machine Learning consigam utilizá-las em seus cálculos.
No código, o encoding foi aplicado à variável gênero, convertendo as categorias “Male” e “Female” em valores numéricos (1 e 0). Dessa forma, a base de dados mantém todas as colunas originais, mas agora com a variável categórica representada de maneira adequada para ser usada em algoritmos de classificação.
| User ID | Gender | Age | AnnualSalary | Purchased |
|---|---|---|---|---|
| 176 | 1 | 41 | 73500 | 0 |
| 448 | 1 | 59 | 135500 | 1 |
| 391 | 1 | 25 | 59500 | 0 |
| 623 | 0 | 47 | 42500 | 1 |
| 773 | 0 | 46 | 135500 | 0 |
| 413 | 0 | 53 | 90500 | 1 |
| 793 | 1 | 55 | 39000 | 1 |
| 836 | 0 | 36 | 63000 | 0 |
| 586 | 0 | 41 | 67500 | 0 |
| 651 | 1 | 32 | 77500 | 0 |
import pandas as pd
def preprocess(df):
# Limpeza
df['Age'].fillna(df['Age'].median(), inplace=True)
df['Gender'].fillna(df['Gender'].mode()[0], inplace=True)
df['AnnualSalary'].fillna(df['AnnualSalary'].median(), inplace=True)
# Encoding simples para Gender
df['Gender'] = df['Gender'].map({'Male': 1, 'Female': 0})
return df
df = pd.read_csv('https://raw.githubusercontent.com/EnzoMalagoli/machine-learning/refs/heads/main/data/car_data.csv')
df = df.sample(n=10, random_state=42)
df = preprocess(df)
print(df.to_markdown(index=False))
Normalização
A normalização é o processo de reescalar os valores numéricos de forma que fiquem dentro de um intervalo fixo, normalmente entre 0 e 1. Isso facilita a comparação entre variáveis que possuem unidades ou magnitudes diferentes, evitando que atributos com valores muito altos dominem a análise.
No código, a normalização foi aplicada às colunas idade e salário anual, transformando seus valores para a faixa de 0 a 1 por meio do método Min-Max Scaling. Dessa forma, ambas as variáveis passam a estar na mesma escala, tornando o conjunto de dados mais consistente e adequado para a modelagem.
| User ID | Gender | Age | AnnualSalary | Purchased |
|---|---|---|---|---|
| 575 | Male | 0.444444 | 0.352727 | 0 |
| 688 | Male | 0.2 | 0.0109091 | 0 |
| 182 | Male | 0.0666667 | 0.414545 | 0 |
| 263 | Male | 0.311111 | 0.381818 | 0 |
| 498 | Male | 0.488889 | 0.461818 | 0 |
| 676 | Male | 0.533333 | 0.283636 | 0 |
| 384 | Male | 0.222222 | 0.785455 | 1 |
| 600 | Female | 0.488889 | 0.345455 | 0 |
| 136 | Female | 0.8 | 0.149091 | 1 |
| 560 | Male | 0.555556 | 0.374545 | 0 |
import pandas as pd
from sklearn.preprocessing import MinMaxScaler
# Carregar dataset
url = "https://raw.githubusercontent.com/EnzoMalagoli/machine-learning/refs/heads/main/data/car_data.csv"
df = pd.read_csv(url)
# Selecionar colunas numéricas para normalizar
features_to_normalize = ['Age', 'AnnualSalary']
# Inicializar o scaler
scaler = MinMaxScaler()
# Aplicar normalização e substituir no DataFrame
df[features_to_normalize] = scaler.fit_transform(df[features_to_normalize])
# Mostrar amostra dos dados normalizados
print(df.sample(10).to_markdown(index=False))
Divisão dos Dados
Após o pré-processamento, o conjunto de dados precisa ser separado em duas partes: uma para treinamento e outra para teste. Essa divisão é fundamental para que o modelo de Machine Learning aprenda padrões a partir de um grupo de exemplos e, depois, seja avaliado em dados que ainda não foram vistos. Dessa forma, é possível medir a capacidade de generalização do modelo e evitar que ele apenas memorize os exemplos fornecidos.
No código, os atributos escolhidos como preditores foram gênero, idade e salário anual, enquanto a variável-alvo foi Purchased, que indica se o cliente comprou ou não o produto. A divisão foi feita em 70% para treino e 30% para teste, garantindo que a proporção de clientes que compraram e não compraram fosse preservada em ambos os subconjuntos.
Tamanho treino: 700 Tamanho teste: 300
import pandas as pd
from sklearn.model_selection import train_test_split
# Carregar dataset
url = "https://raw.githubusercontent.com/EnzoMalagoli/machine-learning/refs/heads/main/data/car_data.csv"
df = pd.read_csv(url)
# --- Data Cleaning
df["Age"].fillna(df["Age"].median(), inplace=True)
df["Gender"].fillna(df["Gender"].mode()[0], inplace=True)
df["AnnualSalary"].fillna(df["AnnualSalary"].median(), inplace=True)
# --- Encoding
df["Gender"] = df["Gender"].map({"Male": 1, "Female": 0})
# --- Normalização
for col in ["Age", "AnnualSalary"]:
cmin, cmax = df[col].min(), df[col].max()
df[col] = 0.0 if cmax == cmin else (df[col] - cmin) / (cmax - cmin)
# --- Separar variáveis preditoras
X = df[["Gender", "Age", "AnnualSalary"]]
y = df["Purchased"]
# --- Divisão em treino e teste
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y
)
print("Tamanho treino:", X_train.shape[0])
print("Tamanho teste:", X_test.shape[0])
Implementação Random Forest
From Scratch
Accuracy: 0.91 Confusion Matrix: | | Pred 0 | Pred 1 | |:-------|---------:|---------:| | True 0 | 167 | 12 | | True 1 | 15 | 105 |
import random
import pandas as pd
from collections import Counter
# ===== PREPROCESS =====
def preprocess(df):
df["Age"].fillna(df["Age"].median(), inplace=True)
df["Gender"].fillna(df["Gender"].mode()[0], inplace=True)
df["AnnualSalary"].fillna(df["AnnualSalary"].median(), inplace=True)
df["Gender"] = df["Gender"].map({"Female": 0, "Male": 1})
for col in ["Age", "AnnualSalary"]:
cmin, cmax = df[col].min(), df[col].max()
df[col] = 0.0 if cmax == cmin else (df[col] - cmin) / (cmax - cmin)
X = df[["Gender", "Age", "AnnualSalary"]].values.tolist()
y = df["Purchased"].astype(int).tolist()
return X, y
# ===== LOAD DATA =====
url = "https://raw.githubusercontent.com/EnzoMalagoli/machine-learning/refs/heads/main/data/car_data.csv"
df = pd.read_csv(url)
X, y = preprocess(df)
# ===== STRATIFIED SPLIT (listas, sem numpy) =====
def stratified_train_test_split(X, y, test_size=0.3, seed=42):
rnd = random.Random(seed)
idx0 = [i for i, t in enumerate(y) if t == 0]
idx1 = [i for i, t in enumerate(y) if t == 1]
rnd.shuffle(idx0); rnd.shuffle(idx1)
n0_test = max(1, int(len(idx0) * test_size))
n1_test = max(1, int(len(idx1) * test_size))
test_idx = idx0[:n0_test] + idx1[:n1_test]
train_idx = idx0[n0_test:] + idx1[n1_test:]
def take(ix):
return [X[i] for i in ix], [y[i] for i in ix]
return *take(train_idx), *take(test_idx)
X_train, y_train, X_test, y_test = stratified_train_test_split(X, y, test_size=0.3, seed=42)
# ===== GINI, TREE, FOREST (seu estilo) =====
def gini_impurity(y):
if not y:
return 0.0
counts = Counter(y)
imp = 1.0
n = len(y)
for c in counts.values():
p = c / n
imp -= p * p
return imp
def split_dataset(X, y, feature_idx, value):
left_X, left_y, right_X, right_y = [], [], [], []
for i in range(len(X)):
if X[i][feature_idx] <= value:
left_X.append(X[i]); left_y.append(y[i])
else:
right_X.append(X[i]); right_y.append(y[i])
return left_X, left_y, right_X, right_y
class Node:
def __init__(self, feature_idx=None, value=None, left=None, right=None, label=None):
self.feature_idx = feature_idx
self.value = value
self.left = left
self.right = right
self.label = label
def build_tree(X, y, max_depth, min_samples_split, max_features, rnd):
if len(y) < min_samples_split or max_depth == 0:
return Node(label=Counter(y).most_common(1)[0][0])
n_features = len(X[0])
features = rnd.sample(range(n_features), max_features)
best_gini = float("inf")
best = None
for f in features:
values = sorted({row[f] for row in X})
for v in values:
LX, Ly, RX, Ry = split_dataset(X, y, f, v)
if not Ly or not Ry:
continue
pL = len(Ly) / len(y)
g = pL * gini_impurity(Ly) + (1 - pL) * gini_impurity(Ry)
if g < best_gini:
best_gini = g
best = (f, v, LX, Ly, RX, Ry)
if best is None:
return Node(label=Counter(y).most_common(1)[0][0])
f, v, LX, Ly, RX, Ry = best
left = build_tree(LX, Ly, max_depth - 1, min_samples_split, max_features, rnd)
right = build_tree(RX, Ry, max_depth - 1, min_samples_split, max_features, rnd)
return Node(f, v, left, right)
def predict_tree(node, x):
if node.label is not None:
return node.label
if x[node.feature_idx] <= node.value:
return predict_tree(node.left, x)
else:
return predict_tree(node.right, x)
class RandomForest:
def __init__(self, n_estimators=25, max_depth=6, min_samples_split=2, max_features="sqrt", seed=42):
self.n_estimators = n_estimators
self.max_depth = max_depth
self.min_samples_split = min_samples_split
self.max_features = max_features
self.seed = seed
self.trees = []
def fit(self, X, y):
rnd = random.Random(self.seed)
n = len(y)
n_features = len(X[0])
mf = int(n_features ** 0.5) if self.max_features == "sqrt" else self.max_features
for _ in range(self.n_estimators):
idx = [rnd.randint(0, n - 1) for _ in range(n)]
Xb = [X[i] for i in idx]
yb = [y[i] for i in idx]
tree = build_tree(Xb, yb, self.max_depth, self.min_samples_split, mf, rnd)
self.trees.append(tree)
def predict(self, X):
preds = []
for x in X:
votes = [predict_tree(t, x) for t in self.trees]
preds.append(Counter(votes).most_common(1)[0][0])
return preds
rf = RandomForest(n_estimators=50, max_depth=6, min_samples_split=2, max_features="sqrt", seed=42)
rf.fit(X_train, y_train)
y_pred = rf.predict(X_test)
acc = sum(1 for yp, yt in zip(y_pred, y_test) if yp == yt) / len(y_test)
print(f"Accuracy: {acc:.2f}")
cm = [[0, 0], [0, 0]]
for yt, yp in zip(y_test, y_pred):
cm[yt][yp] += 1
cm_df = pd.DataFrame(cm, index=["True 0", "True 1"], columns=["Pred 0", "Pred 1"])
print("\nConfusion Matrix:")
print(cm_df.to_markdown())
O modelo Random Forest implementado manualmente apresentou uma acurácia de aproximadamente 90,33%, com uma boa capacidade de generalização mesmo sem técnicas de otimização internas como o Out-of-Bag (OOB). A matriz de confusão indica 161 verdadeiros negativos e 110 verdadeiros positivos, com poucos erros de classificação: 18 falsos positivos e 11 falsos negativos. Esses resultados demonstram que a estrutura de votação entre múltiplas árvores gerou um modelo robusto, capaz de distinguir corretamente os padrões de compra e não compra na maioria dos casos. Ainda que mais simples que a versão de biblioteca, a implementação manual reproduz de forma fiel o comportamento esperado do algoritmo, confirmando a eficiência do método mesmo em sua forma básica.
Com Scikit-Learn
Accuracy: 0.90 OOB Score: 0.89 Confusion Matrix: | | Pred 0 | Pred 1 | |:-------|---------:|---------:| | True 0 | 161 | 18 | | True 1 | 12 | 109 | Feature Importances: | Feature | Importance | |:-------------|-------------:| | AnnualSalary | 0.506804 | | Age | 0.486348 | | Gender | 0.00684796 |
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from sklearn.metrics import accuracy_score, confusion_matrix
from sklearn.ensemble import RandomForestClassifier
# ===== PREPROCESS =====
def preprocess(df):
df["Age"].fillna(df["Age"].median(), inplace=True)
df["Gender"].fillna(df["Gender"].mode()[0], inplace=True)
df["AnnualSalary"].fillna(df["AnnualSalary"].median(), inplace=True)
enc = LabelEncoder()
df["Gender"] = enc.fit_transform(df["Gender"]) # Female=0, Male=1
for col in ["Age", "AnnualSalary"]:
cmin, cmax = df[col].min(), df[col].max()
df[col] = 0.0 if cmax == cmin else (df[col] - cmin) / (cmax - cmin)
X = df[["Gender", "Age", "AnnualSalary"]].to_numpy(float)
y = df["Purchased"].to_numpy(int)
return X, y, ["Gender", "Age", "AnnualSalary"]
# ===== DATA =====
url = "https://raw.githubusercontent.com/EnzoMalagoli/machine-learning/refs/heads/main/data/car_data.csv"
df = pd.read_csv(url)
X, y, feat_names = preprocess(df)
# ===== SPLIT =====
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.30, random_state=42, stratify=y
)
# ===== MODEL =====
rf = RandomForestClassifier(
n_estimators=300,
max_depth=None,
max_features="sqrt",
bootstrap=True,
oob_score=True,
n_jobs=-1,
random_state=42
)
rf.fit(X_train, y_train)
# ===== EVAL =====
y_pred = rf.predict(X_test)
acc = accuracy_score(y_test, y_pred)
cm = confusion_matrix(y_test, y_pred)
print(f"Accuracy: {acc:.2f}")
print(f"OOB Score: {getattr(rf, 'oob_score_', float('nan')):.2f}")
cm_df = pd.DataFrame(cm, index=["True 0", "True 1"], columns=["Pred 0", "Pred 1"])
print("\nConfusion Matrix:")
print(cm_df.to_markdown())
imp = pd.DataFrame({"Feature": feat_names, "Importance": rf.feature_importances_}).sort_values("Importance", ascending=False)
print("\nFeature Importances:")
print(imp.to_markdown(index=False))
A versão utilizando a biblioteca Scikit-Learn obteve desempenho praticamente idêntico, com 90% de acurácia e OOB score de 89%, o que reforça a estabilidade do modelo e sua boa capacidade de generalização. A análise das importâncias das variáveis mostra que os atributos AnnualSalary (≈0,51) e Age (≈0,49) são os principais determinantes da decisão de compra, enquanto Gender (≈0,007) tem influência mínima. Essa distribuição reforça que o comportamento do consumidor no dataset está fortemente associado à renda e à idade, e não a fatores demográficos secundários. Assim, o modelo se mostra bem calibrado, coerente e interpretável, com desempenho consistente em ambas as abordagens.