Skip to content

KNN

Introdução ao KNN

O algoritmo K-Nearest Neighbors (KNN) foi utilizado como alternativa para realizar tarefas de classificação. Esse método classifica uma nova observação com base nos exemplos mais próximos do conjunto de treino, considerando a similaridade entre seus atributos. Por sua simplicidade e flexibilidade, o KNN não exige pressupostos sobre a distribuição dos dados e oferece uma análise fundamentada na proximidade entre instâncias, funcionando como complemento às previsões obtidas com a árvore de decisão.

Descrição sobre o banco

Para mais informações, cheque a página sobre Árvore de decisão, aqui tem toda a explicação necessária para compreender as variáveis e as outras coisas.

Análise dos dados

Tipo: numérica contínua

O que é: idade em anos.

Para que serve: pode relacionar-se com hábitos e condição física.

Ação necessária: nenhuma obrigatória; só checar faixas implausíveis (não observei no geral).

2025-10-28T15:18:54.163048 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

Tipo: numérica contínua

O que é: altura em centímetros.

Para que serve: isoladamente costuma ter pouco poder; combinada ao peso forma o BMI.

Ação necessária: checar valores muito fora do plausível. Sugestão: considerar substituir altura e peso por bmi(Índice de Massa Corporal).

2025-10-28T15:18:54.230787 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

Tipo: numérica contínua

O que é: peso em quilogramas.

Para que serve: junto com a altura permite calcular BMI = peso(kg) / (altura(m))², que costuma ser mais informativo para a árvore.

Ação necessária: manter como numérica ou criar bmi e remover height_cm/weight_kg das features (deixando só o bmi).

2025-10-28T15:18:54.317172 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

Tipo: numérica contínua

O que é: frequência cardíaca (bpm).

Para que serve: indicador de condicionamento cardiovascular; pode ajudar na separação das classes.

Ação necessária: nenhuma obrigatória; apenas conferir plausibilidade de valores extremos.

2025-10-28T15:18:54.371242 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

Tipo: numérica contínua

O que é: medida sintética de pressão arterial fornecida pelo dataset.

Para que serve: sinal de saúde geral que pode complementar a predição.

Ação necessária: nenhuma obrigatória; só verificar extremos muito fora do usual.

2025-10-28T15:18:54.553864 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

Tipo: numérica contínua

O que é: horas de sono por dia.

Para que serve: hábito de descanso; costuma ter correlação com “estar fit”.

Ação necessária: possui valores ausentes (160 valores); imputar com a mediana.

2025-10-28T15:18:54.626775 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

Tipo: numérica contínua (escala)

O que é: qualidade da nutrição (escala contínua, ex.: 0–10).

Para que serve: proxy de alimentação saudável; geralmente relevante.

Ação necessária: nenhuma; manter como numérica (só garantir faixa válida).

2025-10-28T15:18:54.703374 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

Tipo: numérica contínua (escala)

O que é: nível de atividade física (escala contínua, ex.: 0–10).

Para que serve: costuma ser uma das variáveis mais importantes para is_fit.

Ação necessária: nenhuma; manter como numérica (garantir faixa válida).

2025-10-28T15:18:54.779341 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

Tipo: categórica binária

O que é: status de tabagismo (sim/não).

Para que serve: fator de estilo de vida; pode ajudar a separar perfis.

Ação necessária: tipos mistos no bruto (“yes/no” e “1/0”). Padronizar para binário numérico (no→0, yes→1) e converter para int.

2025-10-28T15:18:54.863981 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

Tipo: categórica binária

O que é: gênero (F/M).

Para que serve: possível moderador de outros efeitos; em geral fraco sozinho.

Ação necessária: codificar para numérico (F→0, M→1) e converter para int.

2025-10-28T15:18:54.892335 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

Tipo: categórica binária (target)

O que é: rótulo de condição física (1 = fit, 0 = não fit).

Para que serve: variável dependente a ser prevista.

Ação necessária: checar balanceamento das classes.

2025-10-28T15:18:54.946743 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

Pré-processamento

Nesta etapa tratei e preparei os dados para treinar a Árvore de Decisão. Antes do tratamento, a base apresentava valores ausentes em sleep_hours, tipos mistos em smokes (valores como yes/no e 0/1 ao mesmo tempo) e variáveis categóricas em texto (gender com F/M). Abaixo, o que foi feito:

• Padronização de categóricas

  • smokes: normalizei rótulos e converti para binário numérico (no→0, yes→1, cobrindo também 0/1 em string).

  • gender: converti F→0 e M→1.

• Valores ausentes

  • sleep_hours: converti para numérico e imputei a mediana.

• Tipos e consistência

  • Garanti que as variáveis contínuas ficaram em formato numérico, sem strings residuais/espaços.

• Criação de nova variável

  • Criei a variável BMI (peso(kg) / altura(m)²) para avaliar seu impacto. Na exploração, mantenho height_cm e weight_kg para referência; na modelagem, comparo dois cenários: (A) sem BMI (altura + peso) e (B) com apenas BMI, evitando usar os três juntos no mesmo modelo para não introduzir redundância.
age height_cm weight_kg heart_rate blood_pressure sleep_hours nutrition_quality activity_index smokes gender is_fit
52 168 95 60.2 128.4 7.3 3.87 1.34 no M 0
37 182 73 59.4 108.5 8.8 4.11 1.87 0 M 1
22 150 95 67 121.6 6.2 2.54 1.58 yes F 0
58 191 62 59 136.6 8.8 5.74 1.94 no F 0
39 190 65 68.3 107.5 7.3 8.3 1.52 yes F 0
79 199 107 82.5 117.5 9.1 1.42 3.67 0 F 1
65 162 99 69.1 105.5 6.3 6.87 4.18 yes F 0
45 153 98 75.9 125.2 8.8 3.72 1.5 0 M 0
42 158 84 62.6 129.3 6.1 3.82 2.95 yes F 0
19 180 96 98.8 95.9 7.4 2.39 3.56 no F 0
48 193 101 96.6 121.2 7.5 6.38 3.6 0 M 1
29 187 71 68.4 117.1 6.0 9.95 3.61 0 F 1
31 166 81 68 128.8 8.3 0.88 2.88 no M 0
62 152 91 59 105.7 9.0 4.16 4 0 M 1
43 181 91 96.1 121.4 null 3.99 4.52 1 M 1
import pandas as pd

df = pd.read_csv("./src/fitness_dataset.csv")

df["sleep_hours"] = df["sleep_hours"].fillna(df["sleep_hours"].median())

df["smokes"] = (
    df["smokes"].astype(str).str.strip().str.lower()
      .map({"yes": 1, "no": 0, "1": 1, "0": 0})
).astype(int)

df["gender"] = df["gender"].replace({"F": 0, "M": 1}).astype(int)

h_m = pd.to_numeric(df["height_cm"], errors="coerce") / 100.0
bmi = pd.to_numeric(df["weight_kg"], errors="coerce") / (h_m**2)
df["bmi"] = bmi.replace([float("inf"), float("-inf")], pd.NA).fillna(bmi.median())

print(df.sample(n=15).to_markdown(index=False))
age height_cm weight_kg heart_rate blood_pressure sleep_hours nutrition_quality activity_index smokes gender is_fit bmi
77 171 90 66.5 104.8 4.6 9.2 1.98 0 0 0 30.7787
79 184 91 73.2 115.4 5.6 1.96 4.46 0 0 0 26.8785
31 198 100 82.9 108.4 9.1 8.02 2.9 0 1 1 25.5076
40 183 87 72.2 124.2 8.9 6.67 3.59 0 1 1 25.9787
75 191 109 66.4 128.8 11.4 2.98 3.87 0 0 0 29.8786
78 178 113 85.4 134.3 6.6 9.2 2.75 1 1 0 35.6647
40 161 108 53.1 125.3 6.7 1.58 3.31 1 1 0 41.6651
21 164 110 60.7 131.2 7.3 0.38 4.06 0 1 1 40.8983
38 188 57 81.2 110.6 6.6 8.47 4.96 0 1 1 16.1272
66 174 82 75.9 128.8 8.1 7.23 3.47 0 0 1 27.0842
52 171 54 53.8 109.5 6.8 10 1.3 1 1 0 18.4672
75 152 118 67.9 110.9 7.6 4.56 2.67 0 0 0 51.0734
43 154 69 73.8 133.2 6.6 2.66 2.05 0 1 0 29.0943
35 158 104 68.1 118.1 5.6 6.92 1.45 0 0 0 41.66
56 164 108 70.1 140.9 9.1 4.15 2.06 0 0 0 40.1547

Divisão dos Dados

Para o modelo KNN, optei por separar o conjunto em treino (80%) e teste (20%), de forma a avaliar o desempenho do classificador em dados não vistos. O parâmetro random_state=42 foi utilizado para garantir reprodutibilidade, e stratify=y assegurou que a proporção entre as classes (is_fit = 0 e is_fit = 1) fosse preservada em ambas as partições.

Antes da divisão, o pré-processamento já havia sido realizado: imputação da mediana em sleep_hours, padronização de smokes (0/1), codificação de gender (0/1) e criação da variável bmi em substituição às variáveis originais height_cm e weight_kg. Como o KNN é sensível a diferenças de escala, também foi aplicada a padronização dos atributos numéricos após a definição das features finais.

Essa estratégia garante que o modelo seja treinado em um subconjunto representativo e avaliado de maneira justa, evitando que a acurácia reflita apenas o aprendizado sobre o conjunto total de dados.

Features: age, height_cm, weight_kg, heart_rate, blood_pressure, sleep_hours, nutrition_quality, activity_index, smokes, gender.

Objetivo: servir de referência para comparar com a versão engenheirada.

Mesma configuração de split (70/30, random_state=42, stratify=y).

import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

# Carregar base
df = pd.read_csv("./src/fitness_dataset.csv")

# Tratamento
df["sleep_hours"] = pd.to_numeric(df["sleep_hours"], errors="coerce").fillna(df["sleep_hours"].median())

df["smokes"] = (
    df["smokes"].astype(str).str.strip().str.lower()
      .map({"yes": 1, "no": 0, "1": 1, "0": 0})
).astype(int)

df["gender"] = df["gender"].replace({"F": 0, "M": 1}).astype(int)

# Criar BMI e substituir height/weight
h_m = pd.to_numeric(df["height_cm"], errors="coerce") / 100.0
bmi = pd.to_numeric(df["weight_kg"], errors="coerce") / (h_m**2)
df["bmi"] = bmi.replace([float("inf"), float("-inf")], pd.NA).fillna(bmi.median())

# Features e alvo
num_cols = ["age", "heart_rate", "blood_pressure", "sleep_hours",
            "nutrition_quality", "activity_index", "bmi"]
cat_cols = ["smokes", "gender"]
target = "is_fit"

# Garantir numéricos válidos
for c in num_cols:
    df[c] = pd.to_numeric(df[c], errors="coerce").fillna(df[c].median())

X_num = df[num_cols]
X_cat = df[cat_cols]

# Padronizar atributos numéricos
scaler = StandardScaler()
X_num_scaled = pd.DataFrame(scaler.fit_transform(X_num), columns=num_cols)

# Concatenar
X = pd.concat([X_num_scaled.reset_index(drop=True),
               X_cat.reset_index(drop=True)], axis=1).values
y = df[target].astype(int).values

# Divisão treino/teste (80/20)
X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.2,
    random_state=42,
    stratify=y
)

print("Tamanho treino:", X_train.shape[0])
print("Tamanho teste:", X_test.shape[0])
print("Proporção classes treino:", y_train.mean())
print("Proporção classes teste:", y_test.mean())

Treinamento do Modelo

Acurácia (KNN k=5): 0.71

import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from sklearn.preprocessing import StandardScaler

df = pd.read_csv("./src/fitness_dataset.csv")

# Tratamento dos dados
df["sleep_hours"] = pd.to_numeric(df["sleep_hours"], errors="coerce")
df["sleep_hours"] = df["sleep_hours"].fillna(df["sleep_hours"].median())
df["smokes"] = df["smokes"].replace({"yes": 1, "no": 0, "1": 1, "0": 0}).astype(int)
df["gender"] = df["gender"].replace({"F": 0, "M": 1}).astype(int)

# bmi = weight_kg / (height_m^2)
h_m = pd.to_numeric(df["height_cm"], errors="coerce") / 100.0
bmi = pd.to_numeric(df["weight_kg"], errors="coerce") / (h_m ** 2)
df["bmi"] = (
    bmi.replace([float("inf"), float("-inf")], pd.NA)
       .fillna(bmi.median())
)


# Features e target
num_cols = [
    "age", "heart_rate", "blood_pressure",
    "sleep_hours", "nutrition_quality", "activity_index", "bmi"
]
cat_cols = ["smokes", "gender"]
target = "is_fit"

# Garantir tipos numéricos
for c in num_cols:
    df[c] = pd.to_numeric(df[c], errors="coerce")
    df[c] = df[c].fillna(df[c].median())

X_num = df[num_cols]
X_cat = df[cat_cols]

# Padronização dos dados numéricos
scaler = StandardScaler()
X_num = pd.DataFrame(scaler.fit_transform(X_num), columns=num_cols)

# Junta tudo
X = pd.concat([X_num, X_cat.reset_index(drop=True)], axis=1).values
y = df[target].astype(int).values

# Split
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.20, random_state=42, stratify=y
)

class KNNClassifier:
    def __init__(self, k=5):
        self.k = k

    def fit(self, X, y):
        self.X_train = X
        self.y_train = np.array(y)

    def predict(self, X):
        return np.array([self._predict(x) for x in X])

    def _predict(self, x):
        distances = np.sqrt(((self.X_train - x) ** 2).sum(axis=1))
        k_idx = np.argsort(distances)[:self.k]
        k_labels = self.y_train[k_idx]
        vals, counts = np.unique(k_labels, return_counts=True)
        return vals[np.argmax(counts)]

# Treinar e avaliar
knn = KNNClassifier(k=5)
knn.fit(X_train, y_train)
y_pred = knn.predict(X_test)

acc = accuracy_score(y_test, y_pred)
print(f"Acurácia (KNN k={knn.k}): {acc:.2f}")

Usando o Scikit-Learn

Acurácia: 0.6250 2025-10-28T15:18:55.352332 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from io import StringIO

from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score, balanced_accuracy_score

# === Carregar e tratar como na base_tratada ===
df = pd.read_csv("./src/fitness_dataset.csv")

# sleep_hours -> mediana
df["sleep_hours"] = pd.to_numeric(df["sleep_hours"], errors="coerce")
df["sleep_hours"] = df["sleep_hours"].fillna(df["sleep_hours"].median())

# smokes -> normaliza rótulos e converte p/ 0/1
df["smokes"] = (
    df["smokes"].astype(str).str.strip().str.lower()
      .map({"yes": 1, "no": 0, "1": 1, "0": 0})
).astype(int)

# gender -> F=0, M=1
df["gender"] = df["gender"].replace({"F": 0, "M": 1}).astype(int)

# bmi = weight_kg / (height_m^2)
h_m = pd.to_numeric(df["height_cm"], errors="coerce") / 100.0
bmi = pd.to_numeric(df["weight_kg"], errors="coerce") / (h_m ** 2)
df["bmi"] = (
    bmi.replace([float("inf"), float("-inf")], pd.NA)
       .fillna(bmi.median())
)

# === Features e alvo ===
# Usa BMI e remove height_cm/weight_kg para evitar redundância
num_cols = [
    "age", "heart_rate", "blood_pressure", "sleep_hours",
    "nutrition_quality", "activity_index", "bmi"
]
cat_cols = ["smokes", "gender"]
target = "is_fit"

# Garantir numéricos válidos nas contínuas
for c in num_cols:
    df[c] = pd.to_numeric(df[c], errors="coerce")
    df[c] = df[c].fillna(df[c].median())

X_num = df[num_cols]
X_cat = df[cat_cols]

# Padronização (KNN é sensível à escala)
scaler = StandardScaler()
X_num_scaled = pd.DataFrame(scaler.fit_transform(X_num), columns=num_cols)

# Matriz final
X = pd.concat([X_num_scaled.reset_index(drop=True),
               X_cat.reset_index(drop=True)], axis=1).values
y = df[target].astype(int).values

# Split
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# PCA 2D só para visualização/decisão do gráfico
pca = PCA(n_components=2, random_state=42)
X_train_2d = pca.fit_transform(X_train)
X_test_2d = pca.transform(X_test)

# KNN
knn = KNeighborsClassifier(n_neighbors=5)
knn.fit(X_train_2d, y_train)
pred = knn.predict(X_test_2d)

# Métricas
acc = accuracy_score(y_test, pred)
bacc = balanced_accuracy_score(y_test, pred)
print(f"Acurácia: {acc:.4f}")

# Plot
plt.figure(figsize=(12, 10))
h = 0.05
x_min, x_max = X_train_2d[:, 0].min() - 1, X_train_2d[:, 0].max() + 1
y_min, y_max = X_train_2d[:, 1].min() - 1, X_train_2d[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
                     np.arange(y_min, y_max, h))

Z = knn.predict(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)

plt.contourf(xx, yy, Z, cmap=plt.cm.RdYlBu, alpha=0.3)
sns.scatterplot(
    x=X_train_2d[:, 0], y=X_train_2d[:, 1], hue=y_train,
    palette="deep", s=100, edgecolor="k", alpha=0.8, legend=True
)

plt.xlabel("PCA 1")
plt.ylabel("PCA 2")
plt.title("KNN Decision Boundary (Fitness Dataset, com BMI)")
plt.tight_layout()

# Renderiza 1 SVG no site e fecha a figura
buf = StringIO()
plt.savefig(buf, format="svg", bbox_inches="tight", transparent=True)
print(buf.getvalue())
plt.close()

Avaliação do Modelo

O modelo KNN, configurado com k = 5, apresentou uma acurácia de aproximadamente 62% no conjunto de teste, com balanced accuracy em torno de 59%. Isso significa que o desempenho ficou apenas um pouco acima do acaso (50%), refletindo a dificuldade do algoritmo em distinguir corretamente entre indivíduos classificados como “fit” e “não fit”.

A visualização da fronteira de decisão confirma esse resultado: as classes aparecem bastante sobrepostas no espaço de duas dimensões (após PCA), e a divisão entre elas não é nítida. Esse comportamento indica que as variáveis utilizadas (idade, indicadores de saúde, hábitos de sono, nutrição, atividade física, tabagismo, gênero e BMI) possuem alguma relevância, mas não oferecem separação clara o suficiente para que o KNN construa regiões bem definidas para cada classe. Além disso, a irregularidade da fronteira de decisão ressalta a sensibilidade do método a ruídos e à distribuição dos dados, algo esperado nesse tipo de problema.

Conclusão

O uso do KNN para prever a condição física (fit vs. não fit) demonstrou resultados modestos, mas ainda válidos como exercício de classificação e comparação com outros algoritmos, como a árvore de decisão. Apesar da simplicidade e da interpretação intuitiva do método, a análise gráfica mostrou que as classes são altamente sobrepostas, o que limita a capacidade do modelo de generalizar.

De maneira geral, os resultados indicam que o KNN pode servir como uma abordagem inicial para explorar o dataset, mas melhorias dependem de ajustes nos hiperparâmetros (como o valor de k), da criação de novas features ou da aplicação de modelos mais robustos, capazes de lidar melhor com a complexidade e o ruído dos dados.