Skip to content

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 – idade
  • height_cm – altura
  • weight_kg – peso
  • heart_rate – frequência cardíaca
  • blood_pressure – pressão arterial
  • sleep_hours – horas de sono
  • nutrition_quality – qualidade nutricional
  • activity_index – índice de atividade física
  • smokes – hábito de fumar
  • gender – gênero
  • is_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_hours apresentou 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=42 garante 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 é:

\[\min_{\alpha} \frac{1}{2} \alpha^T P \alpha - \sum_{i=1}^{n} \alpha_i\]

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 é:

\[f(x) = \sum_{i} \alpha_i y_i K(x_i, x) + b\]
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:

Acurácia TREINO: 0.7857 (78.57%)
Acurácia TESTE:  0.7283 (72.83%)

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:

Matriz de Confusão - SVM


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:

Distribuição das Previsões


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:

Real vs Pred


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:

Distribuição da Função de Decisão


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

  1. ✅ Usar SVM do scikit-learn para comparação

    from sklearn.svm import SVC
    svm_sklearn = SVC(kernel='rbf', C=1.0, gamma='scale')
    svm_sklearn.fit(X_train, y_train)
    y_pred_sklearn = svm_sklearn.predict(X_test)
    

  2. 🔧 Ajustar hiperparâmetro sigma do RBF

    for sigma in [0.1, 0.5, 1.0, 2.0, 5.0]:
        # Retreinar modelo com novo sigma
        # Comparar resultados
        pass
    

  3. ⚖️ Aplicar balanceamento de classes

    from sklearn.utils.class_weight import compute_class_weight
    class_weights = compute_class_weight('balanced', 
                                         classes=np.unique(y_train),
                                         y=y_train)
    

  4. 📉 Reduzir dimensionalidade com PCA

    from sklearn.decomposition import PCA
    pca = PCA(n_components=5)
    X_train_pca = pca.fit_transform(X_train)
    X_test_pca = pca.transform(X_test)
    

  5. ✔️ Validação cruzada estratificada

    from sklearn.model_selection import cross_val_score
    scores = cross_val_score(SVC(kernel='rbf'), X_train, y_train, 
                             cv=5, scoring='accuracy')
    


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