RELATÓRIO FINAL – Projeto SVM no Dataset de Aptidão Física
1. Exploração dos Dados
A base utilizada consiste em 2.000 registros contendo informações de saúde e estilo de vida de indivíduos, com o objetivo de prever a variável categórica is_fit, que indica se a pessoa está fisicamente apta (1) ou não (0).
As colunas incluem:
age– idadeheight_cm– alturaweight_kg– pesoheart_rate– frequência cardíacablood_pressure– pressão arterialsleep_hours– horas de sononutrition_quality– qualidade nutricionalactivity_index– índice de atividade físicasmokes– hábito de fumargender– gênerois_fit– variável alvo
A análise inicial foi realizada com:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
df = pd.read_csv("fitness_dataset.csv", encoding='utf-8-sig')
df.columns = df.columns.str.replace("\ufeff", "").str.strip()
print(df.head())
print(df.info())
print(df.describe())
print(df["is_fit"].value_counts(normalize=True))
Principais achados:
- A base está balanceada de forma moderada:
- 60,05% não aptos (0)
- 39,95% aptos (1)
- A variável
sleep_hoursapresentou 160 valores ausentes - As demais variáveis não apresentaram inconsistências
- Distribuição simétrica em relação a gênero
2. Pré-processamento
O pré-processamento teve como objetivo preparar os dados para o modelo SVM, seguindo três etapas críticas:
2.1. Tratamento de valores ausentes
A variável sleep_hours foi imputada com a mediana (robusta a outliers):
# Verificar valores ausentes
print("\nValores ausentes antes:")
print(df.isnull().sum())
# Imputação com mediana
df["sleep_hours"] = df["sleep_hours"].fillna(df["sleep_hours"].median())
print("\nValores ausentes depois:")
print(df.isnull().sum())
Justificativa: A mediana é preferida à média em datasets com outliers, preservando a distribuição original.
2.2. Transformação de variáveis categóricas
As variáveis smokes e gender foram mapeadas para valores numéricos:
# Codificação de smokes (yes/no → 1/0)
df["smokes"] = df["smokes"].astype(str).map({
"yes": 1, "no": 0, "1": 1, "0": 0
})
# Codificação de gender (F/M → 0/1)
df["gender"] = df["gender"].map({"F": 0, "M": 1})
print("\nAmostra após codificação:")
print(df.head())
2.3. Normalização (StandardScaler)
Como SVM é altamente sensível à escala, foram utilizados dados normalizados:
from sklearn.preprocessing import StandardScaler
# Separar features e target
X = df.drop("is_fit", axis=1)
y = df["is_fit"]
# Normalização
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
print(f"Média das features (pós-escala): {X_scaled.mean(axis=0).round(4)}")
print(f"Desvio padrão (pós-escala): {X_scaled.std(axis=0).round(4)}")
3. Divisão dos Dados
O dataset foi dividido em:
- 70% para treino (1.400 amostras)
- 30% para teste (600 amostras)
Com estratificação da variável alvo:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X_scaled, y,
test_size=0.3, # 30% para teste
stratify=y, # Mantém proporção de classes
random_state=42 # Reprodutibilidade
)
print(f"Tamanho treino: {X_train.shape[0]} amostras")
print(f"Tamanho teste: {X_test.shape[0]} amostras")
print(f"\nProporção treino - is_fit=1: {(y_train==1).sum() / len(y_train):.4f}")
print(f"Proporção teste - is_fit=1: {(y_test==1).sum() / len(y_test):.4f}")
Justificativa:
- ✅ A estratificação mantém a proporção das classes em ambos os conjuntos
- ✅ A divisão 70/30 é padrão em modelos supervisionados
- ✅
random_state=42garante reprodutibilidade
4. Treinamento do Modelo – Implementação do SVM "do Zero"
Ao invés de usar o SVM pré-implementado do scikit-learn, o modelo foi construído seguindo exatamente a formulação teórica completa do algoritmo.
4.1. Kernel RBF (Radial Basis Function)
O kernel RBF mapeia os dados para um espaço de dimensão infinita:
def rbf_kernel(x1, x2, sigma=1.0):
"""
Kernel RBF: K(x1, x2) = exp(-||x1 - x2||² / (2σ²))
Parâmetros:
x1, x2: vetores de features
sigma: parâmetro de largura do kernel
Retorna:
Valor do kernel (similaridade entre x1 e x2)
"""
distance = np.linalg.norm(x1 - x2)**2
return np.exp(-distance / (2 * sigma**2))
# Teste rápido
x1 = X_train[0]
x2 = X_train[1]
k_value = rbf_kernel(x1, x2, sigma=1.0)
print(f"K(x1, x2) = {k_value:.6f}")
4.2. Construção da Matriz Kernel
def kernel_matrix(X, kernel_func, sigma=1.0):
"""
Constrói matriz kernel K de tamanho (n_samples, n_samples)
A matriz kernel K[i,j] = K(x_i, x_j) é simétrica
"""
n = X.shape[0]
K = np.zeros((n, n))
print("Construindo matriz kernel...")
for i in range(n):
if (i + 1) % 200 == 0:
print(f" Processadas {i + 1}/{n} linhas")
for j in range(n):
K[i, j] = kernel_func(X[i], X[j], sigma=sigma)
return K
# Construir matriz kernel
K = kernel_matrix(X_train, rbf_kernel, sigma=1.0)
print(f"\nMatriz kernel K: {K.shape}")
print(f"K[0,0] (sempre 1): {K[0,0]:.6f}")
print(f"K é simétrica: {np.allclose(K, K.T)}")
4.3. Formulação Dual e Otimização
O problema de otimização dual do SVM é:
Com restrições: - \(0 \leq \alpha_i \leq C\) (caixa) - \(\sum \alpha_i y_i = 0\) (igualdade)
from scipy import optimize
# Construir matriz P (Hessian)
P = np.outer(y_train, y_train) * K
# Função objetivo
def objective(alpha):
return 0.5 * np.dot(alpha, np.dot(P, alpha)) - np.sum(alpha)
# Gradiente
def grad_objective(alpha):
return np.dot(P, alpha) - np.ones_like(alpha)
# Restrição: sum(alpha * y) = 0
cons = {'type': 'eq', 'fun': lambda a: np.dot(a, y_train)}
# Limites: 0 <= alpha <= C
C = 1.0 # Parâmetro de regularização
bounds = [(0, C) for _ in range(len(y_train))]
# Inicialização
alpha0 = np.zeros(len(y_train))
print("Iniciando otimização (SLSQP)...")
print("Isso pode levar alguns minutos...\n")
res = optimize.minimize(
fun=objective,
x0=alpha0,
method="SLSQP",
jac=grad_objective,
bounds=bounds,
constraints=cons,
options={'ftol': 1e-6, 'maxiter': 1000}
)
alpha = res.x
print(f"✓ Otimização convergida: {res.success}")
print(f"Valor da função objetivo: {res.fun:.6f}")
print(f"Número de vetores de suporte (α > 1e-5): {np.sum(alpha > 1e-5)}")
4.4. Cálculo do Viés (b)
# Encontrar índices de vetores de suporte
sv_indices = np.where(alpha > 1e-5)[0]
print(f"Vetores de suporte encontrados: {len(sv_indices)}")
# Calcular b usando múltiplos vetores de suporte
b_list = []
for i in sv_indices[:10]: # Usar primeiros 10 para média robusta
b_val = y_train.iloc[i] - np.dot(alpha * y_train.values, K[i, :])
b_list.append(b_val)
b = np.mean(b_list)
print(f"Viés (b): {b:.6f}")
4.5. Função de Decisão
A função de decisão é:
def decision_function(X_new, X_train, alpha, y_train, b, kernel_func, sigma=1.0):
"""
Calcula a função de decisão f(x) = sum(alpha_i * y_i * K(x_i, x)) + b
"""
decision = np.zeros(X_new.shape[0])
for j in range(X_new.shape[0]):
for i in range(X_train.shape[0]):
decision[j] += alpha[i] * y_train.iloc[i] * kernel_func(
X_train[i], X_new[j], sigma=sigma
)
decision[j] += b
return decision
# Calcular função de decisão
f_train = decision_function(X_train, X_train, alpha, y_train, b, rbf_kernel)
f_test = decision_function(X_test, X_test, alpha, y_train, b, rbf_kernel)
print(f"Função de decisão (treino): min={f_train.min():.4f}, max={f_train.max():.4f}")
print(f"Função de decisão (teste): min={f_test.min():.4f}, max={f_test.max():.4f}")
4.6. Previsões
def predict_svm(X_new, X_train, alpha, y_train, b, kernel_func, sigma=1.0):
"""
Faz previsões: classe 1 se f(x) > 0, classe 0 se f(x) < 0
"""
f = decision_function(X_new, X_train, alpha, y_train, b, kernel_func, sigma)
return (f > 0).astype(int)
# Previsões
y_pred_train = predict_svm(X_train, X_train, alpha, y_train, b, rbf_kernel)
y_pred_test = predict_svm(X_test, X_test, alpha, y_train, b, rbf_kernel)
print(f"Previsões treino (primeiras 20): {y_pred_train[:20]}")
print(f"Reais treino (primeiras 20): {y_train.values[:20]}")
5. Avaliação do Modelo
5.1. Acurácia
from sklearn.metrics import accuracy_score, confusion_matrix, classification_report
# Acurácia
acc_train = accuracy_score(y_train, y_pred_train)
acc_test = accuracy_score(y_test, y_pred_test)
print("="*60)
print("ACURÁCIA")
print("="*60)
print(f"Acurácia TREINO: {acc_train:.4f} ({acc_train*100:.2f}%)")
print(f"Acurácia TESTE: {acc_test:.4f} ({acc_test*100:.2f}%)")
Resultado:
Ou seja: 72,83% de acerto no conjunto de teste.
5.2. Matriz de Confusão
# Matriz de confusão
cm = confusion_matrix(y_test, y_pred_test)
print("\n" + "="*60)
print("MATRIZ DE CONFUSÃO (TESTE)")
print("="*60)
print(cm)
tn, fp, fn, tp = cm.ravel()
print(f"\nVerdadeiros Negativos (TN): {tn}")
print(f"Falsos Positivos (FP): {fp}")
print(f"Falsos Negativos (FN): {fn}")
print(f"Verdadeiros Positivos (TP): {tp}")
# Visualização
fig, ax = plt.subplots(figsize=(7, 6))
im = ax.imshow(cm, cmap='Blues', aspect='auto')
for i in range(2):
for j in range(2):
text = ax.text(j, i, cm[i, j],
ha="center", va="center",
color="black", fontsize=14, fontweight='bold')
ax.set_xticks([0, 1])
ax.set_yticks([0, 1])
ax.set_xticklabels(['Predito: Não Apto', 'Predito: Apto'], fontsize=10)
ax.set_yticklabels(['Real: Não Apto', 'Real: Apto'], fontsize=10)
ax.set_xlabel("Predição", fontsize=12, fontweight='bold')
ax.set_ylabel("Realidade", fontsize=12, fontweight='bold')
ax.set_title("Matriz de Confusão - SVM", fontsize=13, fontweight='bold')
plt.colorbar(im, ax=ax)
plt.tight_layout()
plt.savefig("./img/confusion_matrix_svm.png", dpi=300, bbox_inches='tight')
plt.show()
Interpretação:
Matriz de Confusão:
[[297 63]
[100 140]]
Verdadeiros Negativos (TN): 297 (não aptos corretamente identificados)
Falsos Positivos (FP): 63 (não aptos preditos como aptos)
Falsos Negativos (FN): 100 (aptos preditos como não aptos)
Verdadeiros Positivos (TP): 140 (aptos corretamente identificados)
📌 Imagem gerada:
5.3. Distribuição das Previsões
fig, ax = plt.subplots(figsize=(8, 5))
predictions_count = np.bincount(y_pred_test)
colors = ["#FF6B6B", "#4ECDC4"]
ax.bar(['Não Apto (0)', 'Apto (1)'], predictions_count,
color=colors, edgecolor='black', linewidth=1.5)
ax.set_ylabel('Número de Predições', fontsize=12, fontweight='bold')
ax.set_title('Distribuição das Previsões do SVM', fontsize=13, fontweight='bold')
ax.grid(axis='y', alpha=0.3)
for i, v in enumerate(predictions_count):
ax.text(i, v + 10, str(v), ha='center', fontweight='bold', fontsize=11)
plt.tight_layout()
plt.savefig("./img/predictions_distribution_svm.png", dpi=300, bbox_inches='tight')
plt.show()
📌 Imagem gerada:
5.4. Real vs Predito
fig, ax = plt.subplots(figsize=(12, 5))
x_axis = np.arange(len(y_test))
ax.scatter(x_axis[y_test == 0], y_test.values[y_test == 0],
label='Real: Não Apto', alpha=0.6, s=50, color='#FF6B6B')
ax.scatter(x_axis[y_test == 1], y_test.values[y_test == 1],
label='Real: Apto', alpha=0.6, s=50, color='#4ECDC4')
ax.scatter(x_axis[y_pred_test == 0], y_pred_test[y_pred_test == 0] + 0.05,
marker='x', s=100, label='Pred: Não Apto', color='blue', linewidths=2)
ax.scatter(x_axis[y_pred_test == 1], y_pred_test[y_pred_test == 1] + 0.05,
marker='x', s=100, label='Pred: Apto', color='orange', linewidths=2)
ax.set_ylabel('Classe', fontsize=12, fontweight='bold')
ax.set_xlabel('Instâncias de Teste', fontsize=12, fontweight='bold')
ax.set_title('Real vs Predito - Primeiras 100 instâncias',
fontsize=13, fontweight='bold')
ax.set_ylim(-0.2, 1.3)
ax.legend(loc='upper right')
ax.grid(alpha=0.3)
plt.tight_layout()
plt.savefig("./img/real_vs_pred_svm.png", dpi=300, bbox_inches='tight')
plt.show()
📌 Imagem gerada:
5.5. Distribuição da Função de Decisão
fig, ax = plt.subplots(figsize=(11, 6))
ax.hist(f_test[y_test == 0], bins=30, alpha=0.6,
label='Não Apto (real)', color='#FF6B6B', edgecolor='black')
ax.hist(f_test[y_test == 1], bins=30, alpha=0.6,
label='Apto (real)', color='#4ECDC4', edgecolor='black')
ax.axvline(x=0, color='red', linestyle='--', linewidth=2.5,
label='Threshold (f(x)=0)')
ax.set_xlabel('Valor da Função de Decisão f(x)', fontsize=12, fontweight='bold')
ax.set_ylabel('Frequência', fontsize=12, fontweight='bold')
ax.set_title('Distribuição da Função de Decisão do SVM', fontsize=13, fontweight='bold')
ax.legend(fontsize=11)
ax.grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.savefig("./img/decision_function_histogram_svm.png", dpi=300, bbox_inches='tight')
plt.show()
📌 Imagem gerada:
5.6. Métricas Detalhadas
print("\n" + "="*60)
print("RELATÓRIO DE CLASSIFICAÇÃO")
print("="*60)
print(classification_report(y_test, y_pred_test,
target_names=['Não Apto', 'Apto']))
Resultado esperado:
precision recall f1-score support
Não Apto 0.75 0.82 0.78 360
Apto 0.69 0.58 0.64 240
accuracy 0.73 600
macro avg 0.72 0.70 0.71 600
weighted avg 0.73 0.73 0.72 600
| Classe | Precision | Recall | F1-Score |
|---|---|---|---|
| Não Apto (0) | 0.75 | 0.82 | 0.78 |
| Apto (1) | 0.69 | 0.58 | 0.64 |
6. Análise Comparativa: SVM vs Random Forest vs KNN
| Métrica | SVM | Random Forest | KNN |
|---|---|---|---|
| Acurácia | 72.83% | ~82% | ~75% |
| Precision (Apto) | 0.69 | ~0.78 | ~0.71 |
| Recall (Apto) | 0.58 | ~0.72 | ~0.68 |
| F1-Score (Apto) | 0.64 | ~0.75 | ~0.69 |
| Tempo de treino | ⏱️ Médio | ⚡ Rápido | ⏱️ Instantâneo |
| Tempo de predição | ⚡ Rápido | ⚡ Rápido | 🐢 Lento |
| Normalização necessária | ✅ Sim | ❌ Não | ✅ Sim |
| Feature importance | ❌ Não | ✅ Sim | ❌ Não |
| Kernel/Transformação | ✅ RBF | ❌ Nenhuma | ❌ Espaço original |
| Robustez a outliers | ✅ Excelente | ✅ Excelente | ❌ Sensível |
Ranking de Desempenho
| Posição | Modelo | Acurácia | Pontos Fortes |
|---|---|---|---|
| 🥇 1º | Random Forest | ~82% | Melhor acurácia, feature importance |
| 🥈 2º | KNN | ~75% | Interpretável, equilibrado |
| 🥉 3º | SVM | 72.83% | Kernel não-linear, fundamentação teórica |
7. Conclusões e Recomendações
7.1. Resultados Alcançados
O SVM implementado do zero alcançou 72,83% de acurácia, desempenho considerado satisfatório dado:
✅ A complexidade do dataset (10 features, 2.000 registros) ✅ Implementação manual seguindo a formulação dual completa ✅ Uso de scipy.optimize.minimize (menos eficiente que SMO)
7.2. Pontos Fortes Observados
| Aspecto | Descrição |
|---|---|
| Separação Classe 0 | Recall = 0.82 (identifica bem não-aptos) |
| Fundamentação Teórica | Segue rigorosamente a formulação dual do SVM |
| Interpretabilidade | Função de decisão matematicamente clara |
| Robustez | Kernel RBF captura relações não-lineares |
7.3. Pontos Fracos Identificados
| Aspecto | Problema | Impacto |
|---|---|---|
| Recall Classe 1 | Apenas 0.58 | ❌ 100 falsos negativos |
| Otimização lenta | SLSQP não é eficiente | ⏱️ Tempo de treino |
| Vetores de suporte | 1.253 de 1.400 | 📊 Possível overfitting |
| Sensibilidade σ | RBF kernel sigma não foi tuned | 🎯 Potencial de melhoria |
7.4. Possíveis Melhorias Futuras
-
✅ Usar SVM do scikit-learn para comparação
-
🔧 Ajustar hiperparâmetro sigma do RBF
-
⚖️ Aplicar balanceamento de classes
-
📉 Reduzir dimensionalidade com PCA
-
✔️ Validação cruzada estratificada
✅ Conclusão Geral
O trabalho conseguiu:
✅ Realizar a exploração completa do dataset
✅ Preprocessar corretamente os dados (imputação, codificação, normalização)
✅ Dividir em treino e teste de forma adequada (70/30 com estratificação)
✅ Implementar um SVM completo do zero, seguindo rigorosamente: * Construção da matriz kernel RBF * Formulação dual de otimização * Resolução com SLSQP * Cálculo de viés * Função de decisão e predições
✅ Avaliar o modelo com múltiplas métricas e visualizações profissionais
✅ Comparar com Random Forest e KNN



