Skip to content

Árvore de decisão

Introdução

A árvore de decisão é um algoritmo de aprendizado de máquina utilizado para resolver problemas de classificação e regressão. Seu funcionamento se baseia em uma estrutura hierárquica de nós, onde cada divisão (ramo) representa uma condição aplicada sobre uma variável, conduzindo a diferentes caminhos até chegar a um resultado final (folha). Essa técnica se destaca por sua simplicidade e interpretabilidade, permitindo compreender de forma clara quais fatores influenciam nas decisões do modelo. Além disso, as árvores de decisão conseguem lidar com variáveis numéricas e categóricas, sendo uma ferramenta versátil para análise e previsão em diferentes contextos.

Exploração dos Dados

O Dataset

Para esse projeto foi utilizada o Dataset Fitness Classification Dataset. Essa Base de dados posuí 2.000 linhas e 11 colunas. A variável dependente que será usada como objeto de classificação é a is_fit, ela indica se a pessoa é fit (1) ou não fit (0).

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:48.264193 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:48.336544 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:48.424330 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:48.479968 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:48.569343 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:48.643036 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:48.720499 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:48.794928 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:48.881866 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:48.910553 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:48.965727 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
43 193 80 62.4 109.9 8.9 4.17 2.08 yes F 0
64 199 104 72.7 136.6 6.4 1.7 3.47 0 F 0
66 190 65 73.5 116.2 8.3 2.5 1.67 0 M 0
55 175 45 87.5 128.2 8.5 5.52 4.15 no F 0
29 174 82 77.1 124 7.4 2.78 1.97 no M 1
69 171 74 72 111.1 7.7 6.77 3.09 yes F 0
42 167 103 85.7 138.2 9.4 7.25 4.18 yes F 1
20 181 56 70.3 115.6 6.8 1.22 2.55 1 F 0
75 165 72 89.3 100.5 9.3 7.15 1.82 yes M 1
64 184 53 71.6 142.1 7.6 4.11 3.47 yes M 0
43 156 117 60.7 113.2 5.6 3.91 2.55 yes F 0
78 178 111 83.2 122 7.8 9.29 2.36 no F 0
68 197 59 75.2 97.9 7.9 1.46 2.28 yes M 0
73 173 75 65.7 149.7 9.6 2.37 3.24 0 F 0
66 174 69 74.1 121.2 5.3 6.8 4.98 no 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
24 169 49 63.1 117.9 10.6 4.31 2.72 0 1 0 17.1563
46 178 97 70.4 122.8 8.7 6.38 1.72 0 0 0 30.6148
77 151 62 59.7 157.7 8.1 1.09 3.84 0 1 0 27.1918
47 163 72 58.9 120.3 7 0.04 4.98 1 1 0 27.0993
51 184 102 75.5 114.3 8 1 2.62 1 1 0 30.1276
41 183 60 70.9 132.2 4.3 0.99 2.16 1 1 0 17.9163
20 196 75 56.8 113.1 9.2 6.83 4.94 0 0 1 19.5231
67 159 92 56.1 119 8.6 2.09 3.81 0 1 1 36.391
74 164 79 56.9 101.9 10.4 0.28 3.86 0 1 1 29.3724
52 161 59 75 116.1 7.5 7.93 3.77 1 0 0 22.7615
57 157 94 75.6 116.7 10.4 5.13 3.38 0 0 1 38.1354
60 191 107 79.3 98.2 7.1 6.54 2.7 1 0 0 29.3303
20 166 81 65.1 146.9 5.8 3.53 3.46 0 0 1 29.3947
66 186 53 68.6 121.8 6.3 7.63 3.66 0 0 0 15.3197
50 161 109 72.6 136.3 7.7 5.8 1.76 1 0 0 42.0508

Divisão dos Dados

Nesta etapa separei o conjunto em treino (70%) e teste (30%) para avaliar a árvore em dados não vistos. Usei random_state=42 para reprodutibilidade e stratify=y para manter a proporção de is_fit em treino e teste. Antes do split, finalizei o pré-processamento (imputação da mediana em sleep_hours, padronização de smokes para 0/1 e de gender para 0/1).

Nota: em um pipeline mais rígido, a imputação/codificação seria ajustada no treino e aplicada no teste para evitar vazamento; aqui mantive a simplicidade do material.

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 matplotlib.pyplot as plt
import pandas as pd

from io import StringIO
from sklearn import tree
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from sklearn.metrics import accuracy_score

plt.figure(figsize=(12, 10))

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

label_encoder = LabelEncoder()

#Tratamento de dados nulos e categóricos
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)

# Carregar o conjunto de dados
x = df[['age', 'height_cm', 'weight_kg', 'heart_rate', 'blood_pressure',
       'sleep_hours', 'nutrition_quality', 'activity_index', 'smokes',
       'gender']]
y = df['is_fit']

# Dividir os dados em conjuntos de treinamento e teste
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.3, random_state=42, stratify=y)

Engenharia: criei bmi = peso(kg)/altura(m)² (cálculo linha a linha, sem vazamento). Features: age, bmi, heart_rate, blood_pressure, sleep_hours, nutrition_quality, activity_index, smokes, gender.

Objetivo: avaliar o impacto do BMI na acurácia e na simplicidade da árvore. Mantive height_cm e weight_kg na base tratada para transparência, mas retirei essas colunas apenas na seleção das features do Cenário B para evitar redundância com o bmi.

Comparação justa: ambos os cenários usam o mesmo split (70/30, random_state=42, stratify=y).

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

from io import StringIO
from sklearn import tree
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from sklearn.metrics import accuracy_score

plt.figure(figsize=(12, 10))

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

label_encoder = LabelEncoder()

# Tratamento de dados nulos e categóricos
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 = peso(kg) / (altura(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([np.inf, -np.inf], np.nan).fillna(bmi.median())

# Features (com BMI, substitui altura e peso)
x = df[['age', 'bmi', 'heart_rate', 'blood_pressure',
        'sleep_hours', 'nutrition_quality', 'activity_index', 'smokes', 'gender']]
y = df['is_fit']


x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.3, random_state=42, stratify=y)

Primeiro Treinamento do Modelo

Acurácia da validação: 0.6883

Importância das Features:

Feature Importância
activity_index 0.232233
nutrition_quality 0.175132
bmi 0.123847
age 0.115741
smokes 0.109310
sleep_hours 0.086928
heart_rate 0.082296
blood_pressure 0.064948
gender 0.009565

Árvore de Decisão (SVG):
2025-10-28T15:18:51.180292 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from io import StringIO
from sklearn import tree
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from sklearn.metrics import accuracy_score

plt.figure(figsize=(12, 10))

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

label_encoder = LabelEncoder()

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)

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([np.inf, -np.inf], np.nan).fillna(bmi.median())

x = df[['age','bmi','heart_rate','blood_pressure',
        'sleep_hours','nutrition_quality','activity_index',
        'smokes','gender']]
y = df['is_fit'].astype(int)

x_train, x_test, y_train, y_test = train_test_split(
    x, y, test_size=0.3, random_state=42, stratify=y
)

classifier = tree.DecisionTreeClassifier(random_state=42)
classifier.fit(x_train, y_train)

acc = accuracy_score(y_test, classifier.predict(x_test))
print(f"<b>Acurácia da validação:</b> {acc:.4f}<br>")

imp = pd.Series(classifier.feature_importances_, index=x.columns)
imp_df = pd.DataFrame({"Feature": imp.index, "Importância": imp.values}).sort_values("Importância", ascending=False)
print("<br><b>Importância das Features:</b>")
print(imp_df.to_html(index=False))

tree.plot_tree(classifier, max_depth=4, fontsize=10,
               feature_names=x.columns, class_names=["Não Fit","Fit"], filled=True)

buffer = StringIO()
plt.savefig(buffer, format="svg", bbox_inches="tight", transparent=True)
print("<br><b>Árvore de Decisão (SVG):</b><br>")
print(buffer.getvalue())

Avaliação do primeiro modelo

O modelo com todas as variáveis atingiu 68,83% de precisão. As que mais pesaram foram activity_index (23,2%) e nutrition_quality (17,5%), seguidas por bmi (12,4%), age (11,6%) e smokes (10,9%). Já sleep_hours (8,7%), heart_rate (8,2%), blood_pressure (6,5%) e, principalmente, gender (1,0%) tiveram impacto baixo. Em resumo: o sinal principal vem de atividade física e qualidade da alimentação, com um complemento do BMI e idade.

Segundo Treinamento do Modelo

Acurácia da validação: 0.6900

Importância das Features:

Feature Importância
activity_index 0.259955
nutrition_quality 0.211840
bmi 0.196297
age 0.134762
smokes 0.109310
sleep_hours 0.087836

Árvore de Decisão (SVG):
2025-10-28T15:18:52.297466 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from io import StringIO
from sklearn import tree
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from sklearn.metrics import accuracy_score

plt.figure(figsize=(12, 10))

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

label_encoder = LabelEncoder()

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)

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([np.inf, -np.inf], np.nan).fillna(bmi.median())

x = df[['activity_index', 'nutrition_quality', 'bmi', 
        'age', 'sleep_hours', 'smokes']]
y = df['is_fit'].astype(int)

x_train, x_test, y_train, y_test = train_test_split(
    x, y, test_size=0.3, random_state=42, stratify=y
)

classifier = tree.DecisionTreeClassifier(random_state=42)
classifier.fit(x_train, y_train)

acc = accuracy_score(y_test, classifier.predict(x_test))
print(f"<b>Acurácia da validação:</b> {acc:.4f}<br>")

imp = pd.Series(classifier.feature_importances_, index=x.columns)
imp_df = pd.DataFrame({"Feature": imp.index, "Importância": imp.values}).sort_values("Importância", ascending=False)
print("<br><b>Importância das Features:</b>")
print(imp_df.to_html(index=False))

tree.plot_tree(classifier, max_depth=4, fontsize=10,
               feature_names=x.columns, class_names=["Não Fit","Fit"], filled=True)

buffer = StringIO()
plt.savefig(buffer, format="svg", bbox_inches="tight", transparent=True)
print("<br><b>Árvore de Decisão (SVG):</b><br>")
print(buffer.getvalue())

Avaliação do segundo modelo

Ao remover as variáveis mais fracas (gender, blood_pressure e heart_rate) e manter apenas as mais relevantes, a precisão subiu levemente para 69,00% (+0,17 p.p.). A importância ficou ainda mais concentrada em activity_index (26,0%), nutrition_quality (21,2%) e bmi (19,6%), com age (13,5%), smokes (10,9%) e sleep_hours (8,8%) completando o conjunto.

Conclusão desta comparação: tirar as variáveis com pouco sinal reduz ruído e deixa a árvore mais simples, mantendo (ou melhorando) a precisão. Para a avaliação final, faz sentido seguir com o modelo compacto com BMI.

Relatório Final

Árvores de decisão não pedem normalização, então o que realmente ajudou aqui foi tratar a base (padronizar smokes/gender, imputar sleep_hours) e criar o bmi. Comparei dois modelos e fiquei com o compacto com BMI: é mais simples e bateu ~69,00% de precisão (contra 68,83% do completo). As variáveis que mais fizeram diferença foram activity_index, nutrition_quality e bmi; idade, sono e tabagismo somaram um pouco. Mantive split 70/30 estratificado — reduzir o teste não melhora o modelo e ainda piora a avaliação. Resumo: dados limpos + bons atributos valem mais do que mexer em escala quando o modelo é uma árvore.