K-Means
Introdução ao K-Means
O algoritmo K-Means foi aplicado com o objetivo de identificar padrões e agrupar os indivíduos em clusters com características semelhantes relacionadas à saúde e ao condicionamento físico. Diferente dos métodos supervisionados, como a árvore de decisão e o KNN, o K-Means é um algoritmo de aprendizado não supervisionado, que organiza os dados em grupos de acordo com a proximidade entre seus atributos, sem utilizar diretamente a variável-alvo is_fit como guia. Dessa forma, é possível verificar se os agrupamentos formados refletem, ao menos parcialmente, a divisão entre pessoas classificadas como “fit” e “não fit”, oferecendo uma perspectiva complementar sobre como os hábitos e indicadores de saúde se distribuem na base.
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).
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).
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).
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.
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.
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.
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).
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).
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.
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.
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.
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 |
|---|---|---|---|---|---|---|---|---|---|---|
| 78 | 186 | 109 | 71.4 | 121.7 | 9.2 | 8.4 | 2.57 | no | F | 1 |
| 60 | 190 | 111 | 84.6 | 131.7 | 4.6 | 2.81 | 2.37 | 0 | M | 1 |
| 48 | 153 | 59 | 60 | 109.1 | 10.4 | 0.26 | 3.29 | no | M | 1 |
| 68 | 191 | 72 | 61.9 | 116.7 | 9.3 | 9.66 | 3.25 | 0 | M | 1 |
| 62 | 197 | 48 | 77.3 | 133.8 | 8.1 | 8.96 | 2.39 | 0 | F | 1 |
| 53 | 169 | 78 | 105.6 | 108.9 | 7.4 | 0.18 | 1.51 | yes | M | 0 |
| 26 | 197 | 46 | 69.2 | 120.7 | null | 9.75 | 1.77 | 1 | M | 0 |
| 67 | 159 | 92 | 56.1 | 119 | 8.6 | 2.09 | 3.81 | no | M | 1 |
| 34 | 173 | 112 | 53.9 | 136.8 | 6.0 | 6.83 | 1.32 | 0 | M | 0 |
| 29 | 191 | 54 | 55.6 | 115.1 | 9.2 | 9.36 | 2.95 | no | F | 1 |
| 38 | 184 | 62 | 62.4 | 108.1 | 9.7 | 9.96 | 3.07 | yes | F | 1 |
| 40 | 178 | 55 | 82.3 | 112.3 | 8.5 | 7.64 | 3.84 | no | M | 1 |
| 50 | 161 | 109 | 72.6 | 136.3 | 7.7 | 5.8 | 1.76 | yes | F | 0 |
| 79 | 153 | 74 | 96.7 | 130.9 | null | 6.32 | 2.62 | 0 | M | 0 |
| 45 | 150 | 78 | 52.2 | 113.1 | 6.9 | 6.51 | 4.68 | 0 | F | 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 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 31 | 184 | 98 | 68.6 | 128.1 | 8.1 | 0.79 | 2.06 | 1 | 0 | 0 | 28.9461 |
| 56 | 156 | 72 | 61.8 | 133.4 | 5.6 | 5.28 | 4.32 | 0 | 0 | 1 | 29.5858 |
| 46 | 184 | 82 | 79.5 | 118.4 | 5.2 | 5.11 | 4.43 | 0 | 1 | 1 | 24.2202 |
| 34 | 163 | 55 | 65.3 | 104.6 | 6.1 | 1.05 | 2.7 | 1 | 1 | 0 | 20.7008 |
| 19 | 160 | 117 | 80.5 | 122.8 | 8.9 | 7.47 | 4.9 | 0 | 1 | 1 | 45.7031 |
| 49 | 170 | 117 | 54 | 108.3 | 6.2 | 7.7 | 3.82 | 0 | 0 | 0 | 40.4844 |
| 66 | 181 | 52 | 68.4 | 114.8 | 4 | 9.69 | 4.82 | 1 | 1 | 1 | 15.8725 |
| 20 | 166 | 91 | 76.9 | 130.2 | 7.1 | 3.26 | 3.06 | 0 | 1 | 1 | 33.0237 |
| 65 | 199 | 108 | 78.3 | 103.8 | 10.3 | 1.29 | 1.14 | 1 | 1 | 1 | 27.272 |
| 79 | 199 | 107 | 82.5 | 117.5 | 9.1 | 1.42 | 3.67 | 0 | 0 | 1 | 27.0195 |
| 32 | 157 | 90 | 69.5 | 114.1 | 7.6 | 3.33 | 4.42 | 1 | 0 | 0 | 36.5126 |
| 28 | 167 | 75 | 80.7 | 113.2 | 6.8 | 2.8 | 3.68 | 0 | 0 | 1 | 26.8923 |
| 79 | 169 | 73 | 64.6 | 102 | 8.6 | 1.51 | 2.82 | 0 | 0 | 0 | 25.5593 |
| 64 | 153 | 62 | 77.7 | 99.9 | 7 | 9.11 | 2.76 | 0 | 0 | 1 | 26.4855 |
| 69 | 192 | 73 | 58.2 | 105.7 | 7.5 | 5.6 | 3.48 | 0 | 1 | 1 | 19.8025 |
Divisão dos Dados
Diferentemente dos modelos supervisionados, o K-Means é um algoritmo não supervisionado e, portanto, não requer a separação em treino e teste. Após o pré-processamento, utilizei todo o conjunto de dados para formar os clusters com base apenas nas variáveis explicativas (idade, frequência cardíaca, pressão arterial, horas de sono, qualidade da nutrição, nível de atividade física, tabagismo, gênero e o BMI). A variável-alvo is_fit foi mantida de lado e utilizada apenas posteriormente para avaliar a correspondência entre os clusters encontrados e as classes reais (“fit” vs. “não fit”).
Como o K-Means é sensível à escala, apliquei padronização (z-score) às variáveis numéricas, garantindo que nenhum atributo dominasse a distância euclidiana. Além disso, para evitar redundância, substituí height_cm e weight_kg pela variável derivada bmi. Dessa forma, os agrupamentos refletem padrões de similaridade nos hábitos e indicadores de saúde, e a validação com is_fit serve apenas como referência externa de qualidade do clustering.
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.preprocessing import StandardScaler
df = pd.read_csv("./src/fitness_dataset.csv")
# sleep_hours
df["sleep_hours"] = pd.to_numeric(df["sleep_hours"], errors="coerce").fillna(df["sleep_hours"].median())
# smokes -> 0/1
df["smokes"] = (
df["smokes"].astype(str).str.strip().str.lower()
.map({"yes": 1, "no": 0, "1": 1, "0": 0})
).astype(int)
# gender -> 0/1
df["gender"] = df["gender"].replace({"F": 0, "M": 1}).astype(int)
# BMI
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 (y só para avaliação externa)
num_cols = ["age","heart_rate","blood_pressure","sleep_hours","nutrition_quality","activity_index","bmi"]
cat_cols = ["smokes","gender"]
target = "is_fit"
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]
# padroniza só numéricos
scaler = StandardScaler()
X_num_scaled = pd.DataFrame(scaler.fit_transform(X_num), columns=num_cols)
# matriz final p/ K-Means
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
print("Shape X:", X.shape)
print("Proporção 'is_fit'=1:", y.mean())
print("Observação: K-Means usa APENAS X; y é para avaliar depois.")
Treinamento do Modelo
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from io import StringIO
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
# --- Carregar base ---
df = pd.read_csv("./src/fitness_dataset.csv")
# --- Pré-processamento ---
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)
# BMI
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 (X) e alvo (y apenas para avaliação futura)
num_cols = ["age", "heart_rate", "blood_pressure", "sleep_hours",
"nutrition_quality", "activity_index", "bmi"]
cat_cols = ["smokes", "gender"]
target = "is_fit"
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]
scaler = StandardScaler()
X_num_scaled = pd.DataFrame(scaler.fit_transform(X_num), columns=num_cols)
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
# --- K-Means ---
kmeans = KMeans(n_clusters=2, init='k-means++', max_iter=300,
random_state=42, n_init=10)
labels = kmeans.fit_predict(X)
# --- PCA p/ visualização ---
pca = PCA(n_components=2, random_state=42)
X_2d = pca.fit_transform(X)
# --- Plot ---
plt.figure(figsize=(12, 10))
plt.scatter(X_2d[:, 0], X_2d[:, 1], c=labels, cmap='viridis', s=50, alpha=0.7)
# projeta centróides para o espaço PCA
centroids_2d = pca.transform(kmeans.cluster_centers_)
plt.scatter(centroids_2d[:, 0], centroids_2d[:, 1],
c='red', marker='*', s=200, label='Centroids (PCA proj.)')
plt.title('K-Means Clustering (Fitness Dataset)')
plt.xlabel('PCA 1')
plt.ylabel('PCA 2')
plt.legend()
buffer = StringIO()
plt.savefig(buffer, format="svg", transparent=True)
print(buffer.getvalue())
plt.close()
Avaliação do Modelo
O algoritmo K-Means foi aplicado com k = 2, refletindo a natureza binária da variável is_fit (fit e não fit). A análise gráfica mostra que os clusters foram formados, mas com forte sobreposição entre os grupos. Isso significa que, embora o algoritmo tenha conseguido identificar padrões de proximidade nos dados, a separação entre os perfis de indivíduos com boa condição física e os demais não é nítida.
O posicionamento dos centróides indica regiões médias de concentração, mas a distribuição densa e misturada dos pontos em torno deles revela que as variáveis utilizadas — como BMI, atividade física, nutrição e indicadores de saúde — possuem relevância, mas não criam divisões claras o suficiente para que o K-Means consiga formar agrupamentos bem distintos. Essa característica é comum em bases de dados relacionadas a hábitos e saúde, em que múltiplos fatores se combinam de maneira complexa e não linear.
Conclusão
O uso do K-Means permitiu uma exploração inicial da base, evidenciando como os indivíduos se distribuem em grupos de acordo com seus atributos de saúde e estilo de vida. Apesar da simplicidade e eficiência do algoritmo, os resultados mostram que os clusters obtidos não correspondem perfeitamente à divisão real entre “fit” e “não fit”.
De forma geral, a análise reforça que, embora o K-Means seja útil para identificar padrões gerais e tendências de proximidade, sua capacidade de representar a variável is_fit de forma fiel é limitada. Para análises mais robustas, seria necessário considerar técnicas adicionais, como algoritmos supervisionados (Árvore de Decisão, KNN ou Random Forest), ou mesmo explorar variações de clustering que lidem melhor com sobreposição de classes. Ainda assim, o exercício com K-Means foi importante para oferecer uma perspectiva não supervisionada da estrutura dos dados e validar a dificuldade inerente da tarefa de classificação no contexto do dataset de fitness.