Projeto de Predição de Doenças Cardiovasculares
Este projeto tem como objetivo aplicar técnicas de Machine Learning para prever a presença de doenças cardiovasculares a partir de variáveis clínicas. O desenvolvimento segue etapas bem definidas, cada uma com critérios e pontuação, conforme especificado na rubrica do projeto.
Etapas do Projeto
1. Exploração dos Dados
Descrição do Conjunto de Dados
O dataset utilizado foi obtido no Kaggle e reúne informações provenientes de cinco bases distintas do UCI Machine Learning Repository.
Após a remoção de duplicatas, o conjunto final contém 918 observações e 12 variáveis, sendo 11 atributos preditores e 1 variável alvo (HeartDisease).
Variáveis
- Age: idade do paciente (anos)
- Sex: sexo (M = Masculino, F = Feminino)
- ChestPainType: tipo de dor no peito
- TA: Angina Típica
- ATA: Angina Atípica
- NAP: Dor Não-Anginosa
- ASY: Assintomático
- RestingBP: pressão arterial em repouso (mm Hg)
- Cholesterol: colesterol sérico (mg/dl)
- FastingBS: glicemia em jejum (>120 mg/dl = 1, caso contrário = 0)
- RestingECG: resultados do eletrocardiograma em repouso
- MaxHR: frequência cardíaca máxima atingida
- ExerciseAngina: angina induzida por exercício (Y/N)
- Oldpeak: depressão do segmento ST
- ST_Slope: inclinação do segmento ST (Up, Flat, Down)
- HeartDisease: variável alvo (0 = normal, 1 = presença de doença)
Estatísticas Descritivas e Visualizações
- Idade: varia entre ~28 e 77 anos, com média em torno de 53 anos.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from io import BytesIO
# carregar dataset
df = pd.read_csv("https://raw.githubusercontent.com/bligui/Machine-Learning-Ana/refs/heads/main/dados/heart.csv")
# tratar dados
df.fillna(df.median(numeric_only=True), inplace=True)
df = df.drop_duplicates()
# --- PLOT ---
fig, ax = plt.subplots(1, 1, figsize=(8, 5))
# histograma normalizado
ax.hist(df["Age"], bins=20, color="pink", edgecolor="lightcoral", alpha=0.6, density=True)
# curva de densidade (gaussiana simples)
x_vals = np.linspace(df["Age"].min(), df["Age"].max(), 200)
mean = df["Age"].mean()
std = df["Age"].std()
pdf = (1 / (std * np.sqrt(2 * np.pi))) * np.exp(-0.5 * ((x_vals - mean) / std) ** 2)
ax.plot(x_vals, pdf, color="darkred", linewidth=2, label="Curva de densidade")
ax.set_title("Distribuição da Idade")
ax.set_xlabel("Idade")
ax.set_ylabel("Densidade")
ax.grid(axis="y", linestyle="--", alpha=0.6)
ax.legend()
buffer = BytesIO()
plt.savefig(buffer, format="svg", bbox_inches="tight")
buffer.seek(0)
print(buffer.getvalue().decode("utf-8"))
- Sexo: há predominância do sexo masculino no conjunto.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from io import BytesIO
# carregar dataset
df = pd.read_csv("https://raw.githubusercontent.com/bligui/Machine-Learning-Ana/refs/heads/main/dados/heart.csv")
# tratar dados
df.fillna(df.median(numeric_only=True), inplace=True)
df = df.drop_duplicates()
# --- PLOT: Distribuição por Sexo ---
fig, ax = plt.subplots(1, 1, figsize=(6, 4))
counts = df["Sex"].value_counts().sort_index()
ax.bar(counts.index, counts.values, color=["pink", "skyblue"], edgecolor="lightcoral")
ax.set_title("Distribuição por Sexo")
ax.set_xlabel("Sexo (F=0, M=1)")
ax.set_ylabel("Contagem")
ax.set_xticks([0, 1])
ax.set_xticklabels(["Feminino", "Masculino"])
ax.grid(axis="y", linestyle="--", alpha=0.6)
# salvar em buffer como SVG
buffer = BytesIO()
plt.savefig(buffer, format="svg", bbox_inches="tight")
buffer.seek(0)
print(buffer.getvalue().decode("utf-8"))
- Colesterol: grande variabilidade, com valores fora da faixa esperada em alguns casos.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from io import BytesIO
# carregar dataset
df = pd.read_csv("https://raw.githubusercontent.com/bligui/Machine-Learning-Ana/refs/heads/main/dados/heart.csv")
# tratar dados
df.fillna(df.median(numeric_only=True), inplace=True)
df = df.drop_duplicates()
# --- PLOT: Boxplot do Colesterol ---
fig, ax = plt.subplots(figsize=(7, 5))
bp = ax.boxplot(df["Cholesterol"], patch_artist=True, widths=0.5)
# customização do estilo
for box in bp["boxes"]:
box.set(facecolor="pink", edgecolor="lightcoral", linewidth=1.2)
for whisker in bp["whiskers"]:
whisker.set(color="lightcoral", linewidth=1.2)
for cap in bp["caps"]:
cap.set(color="lightcoral", linewidth=1.2)
for median in bp["medians"]:
median.set(color="darkred", linewidth=1.5)
ax.set_title("Boxplot do Colesterol")
ax.set_xlabel("Colesterol (mg/dl)")
ax.set_xticks([]) # remove eixo X categórico, já que é uma única variável
ax.grid(axis="y", linestyle="--", alpha=0.6)
# salvar em buffer como SVG
buffer = BytesIO()
plt.savefig(buffer, format="svg", bbox_inches="tight")
buffer.seek(0)
print(buffer.getvalue().decode("utf-8"))
- Pressão Arterial em Repouso: média próxima de 130 mm Hg, condizente com casos de hipertensão.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from io import BytesIO
# carregar dataset
df = pd.read_csv("https://raw.githubusercontent.com/bligui/Machine-Learning-Ana/refs/heads/main/dados/heart.csv")
# tratar dados
df.fillna(df.median(numeric_only=True), inplace=True)
df = df.drop_duplicates()
# --- PLOT: Boxplot da Pressão Arterial em Repouso ---
fig, ax = plt.subplots(figsize=(7, 5))
bp = ax.boxplot(df["RestingBP"], patch_artist=True, widths=0.5)
# customização
for box in bp["boxes"]:
box.set(facecolor="pink", edgecolor="lightcoral", linewidth=1.2)
for whisker in bp["whiskers"]:
whisker.set(color="lightcoral", linewidth=1.2)
for cap in bp["caps"]:
cap.set(color="lightcoral", linewidth=1.2)
for median in bp["medians"]:
median.set(color="darkred", linewidth=1.5)
ax.set_title("Boxplot da Pressão Arterial em Repouso")
ax.set_xlabel("Pressão Arterial (mm Hg)")
ax.set_xticks([]) # remove eixo X categórico
ax.grid(axis="y", linestyle="--", alpha=0.6)
# salvar em buffer como SVG
buffer = BytesIO()
plt.savefig(buffer, format="svg", bbox_inches="tight")
buffer.seek(0)
print(buffer.getvalue().decode("utf-8"))
- MaxHR: varia entre 60 e 202, indicando ampla faixa de condicionamento físico.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from io import BytesIO
# carregar dataset
df = pd.read_csv("https://raw.githubusercontent.com/bligui/Machine-Learning-Ana/refs/heads/main/dados/heart.csv")
# tratar dados
df.fillna(df.median(numeric_only=True), inplace=True)
df = df.drop_duplicates()
# --- PLOT: Histograma + Curva de Densidade ---
fig, ax = plt.subplots(figsize=(8, 5))
# histograma normalizado
ax.hist(df["MaxHR"], bins=20, color="pink", edgecolor="lightcoral", alpha=0.6, density=True)
# curva de densidade gaussiana
x_vals = np.linspace(df["MaxHR"].min(), df["MaxHR"].max(), 200)
mean = df["MaxHR"].mean()
std = df["MaxHR"].std()
pdf = (1 / (std * np.sqrt(2 * np.pi))) * np.exp(-0.5 * ((x_vals - mean) / std) ** 2)
ax.plot(x_vals, pdf, color="darkred", linewidth=2, label="Curva de densidade")
ax.set_title("Distribuição da Frequência Cardíaca Máxima (MaxHR)")
ax.set_xlabel("MaxHR")
ax.set_ylabel("Frequência")
ax.grid(axis="y", linestyle="--", alpha=0.6)
ax.legend()
# salvar em buffer como SVG
buffer = BytesIO()
plt.savefig(buffer, format="svg", bbox_inches="tight")
buffer.seek(0)
print(buffer.getvalue().decode("utf-8"))
- Distribuição da Variável Alvo (HeartDisease): aproximadamente 55% dos pacientes apresentam diagnóstico positivo, o que gera uma base relativamente balanceada para treinamento.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from io import BytesIO
# carregar dataset
df = pd.read_csv("https://raw.githubusercontent.com/bligui/Machine-Learning-Ana/refs/heads/main/dados/heart.csv")
# tratar dados
df.fillna(df.median(numeric_only=True), inplace=True)
df = df.drop_duplicates()
# --- PLOT: Distribuição da Variável Alvo ---
fig, ax = plt.subplots(figsize=(6, 4))
counts = df["HeartDisease"].value_counts().sort_index()
ax.bar(counts.index, counts.values, color=["pink", "lightcoral"], edgecolor="darkred")
ax.set_title("Distribuição da Variável Alvo (HeartDisease)")
ax.set_xlabel("HeartDisease (0 = Não, 1 = Sim)")
ax.set_ylabel("Contagem")
ax.set_xticks([0, 1])
ax.set_xticklabels(["Não", "Sim"])
ax.grid(axis="y", linestyle="--", alpha=0.6)
# salvar em buffer como SVG
buffer = BytesIO()
plt.savefig(buffer, format="svg", bbox_inches="tight")
buffer.seek(0)
print(buffer.getvalue().decode("utf-8"))
Conclusões
- Distribuição da idade mostra maior concentração entre 45 e 60 anos.
- Proporção por sexo evidencia predominância masculina.
- Boxplots de colesterol e pressão arterial revelam a presença de outliers que devem ser tratados no pré-processamento.
- Relação entre ChestPainType e HeartDisease indica que pacientes assintomáticos (ASY) têm maior probabilidade de diagnóstico positivo.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from io import BytesIO
# carregar dataset
df = pd.read_csv("https://raw.githubusercontent.com/bligui/Machine-Learning-Ana/refs/heads/main/dados/heart.csv")
# tratar dados
df.fillna(df.median(numeric_only=True), inplace=True)
df = df.drop_duplicates()
# --- PLOT: ChestPainType vs HeartDisease ---
fig, ax = plt.subplots(figsize=(8, 5))
# contar combinações
counts = df.groupby(["ChestPainType", "HeartDisease"]).size().unstack(fill_value=0)
# pegar as classes de HeartDisease dinamicamente (0 e 1)
cols = counts.columns.tolist()
# posições das barras
x = np.arange(len(counts.index))
width = 0.35
# barras
bars1 = ax.bar(x - width/2, counts[cols[0]], width, label="Não", color="pink", edgecolor="darkred")
bars2 = ax.bar(x + width/2, counts[cols[1]], width, label="Sim", color="salmon", edgecolor="darkred")
# adicionar valores de contagem acima das barras
for bar in list(bars1) + list(bars2):
height = bar.get_height()
ax.text(bar.get_x() + bar.get_width()/2, height + 0.5, str(int(height)),
ha="center", va="bottom", fontsize=9)
ax.set_title("Relação entre ChestPainType e HeartDisease")
ax.set_xlabel("Tipo de Dor no Peito")
ax.set_ylabel("Contagem")
ax.set_xticks(x)
ax.set_xticklabels(counts.index) # usa os nomes originais (TA, ATA, NAP, ASY)
ax.legend(title="HeartDisease")
ax.grid(axis="y", linestyle="--", alpha=0.6)
# salvar em buffer como SVG
buffer = BytesIO()
plt.savefig(buffer, format="svg", bbox_inches="tight")
buffer.seek(0)
print(buffer.getvalue().decode("utf-8"))
2. Pré-processamento
Limpeza dos Dados
Antes de qualquer análise ou modelagem, é essencial garantir que os dados estejam consistentes e utilizáveis.
Neste passo, foram realizadas duas etapas principais:
- Tratamento de valores ausentes: substituímos os valores nulos nas colunas numéricas pela mediana, pois ela é menos sensível a outliers do que a média.
- Detecção de outliers: utilizamos o Z-Score para identificar valores que se desviam muito da distribuição normal dos dados. Embora os outliers não tenham sido removidos nesta etapa, sua identificação é fundamental para entender possíveis distorções.
Número de outliers detectados: 19
Age | Sex | ChestPainType | RestingBP | Cholesterol | FastingBS | RestingECG | MaxHR | ExerciseAngina | Oldpeak | ST_Slope | HeartDisease |
---|---|---|---|---|---|---|---|---|---|---|---|
63 | F | ATA | 140 | 195 | 0 | Normal | 179 | N | 0 | Up | 0 |
53 | M | NAP | 145 | 518 | 0 | Normal | 130 | N | 0 | Flat | 1 |
65 | M | ASY | 160 | 0 | 1 | ST | 122 | N | 1.2 | Flat | 1 |
56 | M | ASY | 130 | 0 | 0 | LVH | 122 | Y | 1 | Flat | 1 |
54 | M | ATA | 108 | 309 | 0 | Normal | 156 | N | 0 | Up | 0 |
67 | M | ASY | 125 | 254 | 1 | Normal | 163 | N | 0.2 | Flat | 1 |
56 | M | ASY | 120 | 0 | 0 | ST | 148 | N | 0 | Flat | 1 |
69 | M | NAP | 142 | 271 | 0 | LVH | 126 | N | 0.3 | Up | 0 |
46 | M | TA | 140 | 272 | 1 | Normal | 175 | N | 2 | Flat | 1 |
58 | M | ASY | 120 | 0 | 0 | LVH | 106 | Y | 1.5 | Down | 1 |
import pandas as pd
import numpy as np
def preprocess(df):
df.fillna(df.median(numeric_only=True), inplace=True)
num_df = df.select_dtypes(include=[np.number])
z_scores = np.abs((num_df - num_df.mean()) / num_df.std())
outliers = (z_scores > 3).sum().sum()
# imprime só o número (sem quebrar a tabela depois)
print(f"**Número de outliers detectados:** {outliers}\n")
return df
df = pd.read_csv("https://raw.githubusercontent.com/bligui/Machine-Learning-Ana/refs/heads/main/dados/heart.csv")
df = preprocess(df)
# imprime tabela em markdown (sem texto junto)
print(df.sample(n=10, random_state=42).to_markdown(index=False))
Codificação de Variáveis Categóricas
Os algoritmos de Machine Learning trabalham apenas com valores numéricos. Portanto, variáveis categóricas como "Sex"
, "ChestPainType"
, "RestingECG"
, "ExerciseAngina"
e "ST_Slope"
foram convertidas em números usando mapeamento direto.
Exemplos:
- "M"
→ 1
e "F"
→ 0
- "ASY"
→ 3
, "NAP"
→ 2
, "ATA"
→ 1
, "TA"
→ 0
Essa etapa é essencial para que o modelo consiga interpretar as categorias sem perder o significado original.
Age | Sex | ChestPainType | RestingBP | Cholesterol | FastingBS | RestingECG | MaxHR | ExerciseAngina | Oldpeak | ST_Slope | HeartDisease |
---|---|---|---|---|---|---|---|---|---|---|---|
63 | 0 | 1 | 140 | 195 | 0 | 0 | 179 | 0 | 0 | 0 | 0 |
53 | 1 | 2 | 145 | 518 | 0 | 0 | 130 | 0 | 0 | 1 | 1 |
65 | 1 | 3 | 160 | 0 | 1 | 1 | 122 | 0 | 1.2 | 1 | 1 |
56 | 1 | 3 | 130 | 0 | 0 | 2 | 122 | 1 | 1 | 1 | 1 |
54 | 1 | 1 | 108 | 309 | 0 | 0 | 156 | 0 | 0 | 0 | 0 |
67 | 1 | 3 | 125 | 254 | 1 | 0 | 163 | 0 | 0.2 | 1 | 1 |
56 | 1 | 3 | 120 | 0 | 0 | 1 | 148 | 0 | 0 | 1 | 1 |
69 | 1 | 2 | 142 | 271 | 0 | 2 | 126 | 0 | 0.3 | 0 | 0 |
46 | 1 | 0 | 140 | 272 | 1 | 0 | 175 | 0 | 2 | 1 | 1 |
58 | 1 | 3 | 120 | 0 | 0 | 2 | 106 | 1 | 1.5 | 2 | 1 |
import pandas as pd
import numpy as np
def preprocess(df):
# Preencher valores ausentes
df.fillna(df.median(numeric_only=True), inplace=True)
# Detectar outliers (Z-Score)
num_df = df.select_dtypes(include=[np.number])
z_scores = np.abs((num_df - num_df.mean()) / num_df.std())
outliers = (z_scores > 3).sum().sum()
# Encoding categórico
df["Sex"] = df["Sex"].map({"M": 1, "F": 0})
df["ChestPainType"] = df["ChestPainType"].map({"TA": 0, "ATA": 1, "NAP": 2, "ASY": 3})
df["RestingECG"] = df["RestingECG"].map({"Normal": 0, "ST": 1, "LVH": 2})
df["ExerciseAngina"] = df["ExerciseAngina"].map({"Y": 1, "N": 0})
df["ST_Slope"] = df["ST_Slope"].map({"Up": 0, "Flat": 1, "Down": 2})
return df
# Carregar dataset
df = pd.read_csv("https://raw.githubusercontent.com/bligui/Machine-Learning-Ana/refs/heads/main/dados/heart.csv")
# Pré-processamento
df = preprocess(df)
# Exibir amostra (markdown)
print(df.sample(n=10, random_state=42).to_markdown(index=False))
Normalização e Padronização
Depois da limpeza e codificação, aplicamos técnicas de reescalonamento dos dados numéricos:
- Min-Max Normalization: transforma os valores de cada coluna para o intervalo
[0, 1]
. Essa técnica é útil quando queremos preservar a proporção entre valores, mas garantir que todos estejam no mesmo intervalo. - Standardization (Z-Score): transforma os dados para que tenham média 0 e desvio padrão 1, centralizando a distribuição. Essa técnica é mais adequada quando os dados seguem (ou se aproximam de) uma distribuição normal.
Ambas as técnicas têm como objetivo evitar que variáveis com escalas diferentes dominem o aprendizado dos algoritmos de Machine Learning.
Standardized (Z-score)
Age | Sex | ChestPainType | RestingBP | Cholesterol | FastingBS | RestingECG | MaxHR | ExerciseAngina | Oldpeak | ST_Slope | HeartDisease |
---|---|---|---|---|---|---|---|---|---|---|---|
1.00599 | -1.93711 | -1.34435 | 0.410685 | -0.034736 | -0.551041 | -0.748772 | 1.65711 | -0.823108 | -0.831979 | -1.05154 | -1.11251 |
-0.0541624 | 0.515671 | -0.270275 | 0.680749 | 2.91816 | -0.551041 | -0.748772 | -0.26745 | -0.823108 | -0.831979 | 0.595753 | 0.897891 |
1.21802 | 0.515671 | 0.803804 | 1.49094 | -1.81744 | 1.81277 | 0.491973 | -0.581664 | -0.823108 | 0.293123 | 0.595753 | 0.897891 |
0.263883 | 0.515671 | 0.803804 | -0.129442 | -1.81744 | -0.551041 | 1.73272 | -0.581664 | 1.21358 | 0.105606 | 0.595753 | 0.897891 |
0.0518527 | 0.515671 | -1.34435 | -1.31772 | 1.00746 | -0.551041 | -0.748772 | 0.753746 | -0.823108 | -0.831979 | -1.05154 | -1.11251 |
1.43005 | 0.515671 | 0.803804 | -0.399506 | 0.504648 | 1.81277 | -0.748772 | 1.02868 | -0.823108 | -0.644462 | 0.595753 | 0.897891 |
0.263883 | 0.515671 | 0.803804 | -0.66957 | -1.81744 | -0.551041 | 0.491973 | 0.439532 | -0.823108 | -0.831979 | 0.595753 | 0.897891 |
1.64208 | 0.515671 | -0.270275 | 0.51871 | 0.660063 | -0.551041 | 1.73272 | -0.424557 | -0.823108 | -0.550703 | -1.05154 | -1.11251 |
-0.796268 | 0.515671 | -2.41843 | 0.410685 | 0.669205 | 1.81277 | -0.748772 | 1.50001 | -0.823108 | 1.04319 | 0.595753 | 0.897891 |
0.475913 | 0.515671 | 0.803804 | -0.66957 | -1.81744 | -0.551041 | 1.73272 | -1.21009 | 1.21358 | 0.574398 | 2.24305 | 0.897891 |
Min-Max Normalized
Age | Sex | ChestPainType | RestingBP | Cholesterol | FastingBS | RestingECG | MaxHR | ExerciseAngina | Oldpeak | ST_Slope | HeartDisease |
---|---|---|---|---|---|---|---|---|---|---|---|
0.714286 | 0 | 0.333333 | 0.7 | 0.323383 | 0 | 0 | 0.838028 | 0 | 0.295455 | 0 | 0 |
0.510204 | 1 | 0.666667 | 0.725 | 0.859038 | 0 | 0 | 0.492958 | 0 | 0.295455 | 0.5 | 1 |
0.755102 | 1 | 1 | 0.8 | 0 | 1 | 0.5 | 0.43662 | 0 | 0.431818 | 0.5 | 1 |
0.571429 | 1 | 1 | 0.65 | 0 | 0 | 1 | 0.43662 | 1 | 0.409091 | 0.5 | 1 |
0.530612 | 1 | 0.333333 | 0.54 | 0.512438 | 0 | 0 | 0.676056 | 0 | 0.295455 | 0 | 0 |
0.795918 | 1 | 1 | 0.625 | 0.421227 | 1 | 0 | 0.725352 | 0 | 0.318182 | 0.5 | 1 |
0.571429 | 1 | 1 | 0.6 | 0 | 0 | 0.5 | 0.619718 | 0 | 0.295455 | 0.5 | 1 |
0.836735 | 1 | 0.666667 | 0.71 | 0.44942 | 0 | 1 | 0.464789 | 0 | 0.329545 | 0 | 0 |
0.367347 | 1 | 0 | 0.7 | 0.451078 | 1 | 0 | 0.809859 | 0 | 0.522727 | 0.5 | 1 |
0.612245 | 1 | 1 | 0.6 | 0 | 0 | 1 | 0.323944 | 1 | 0.465909 | 1 | 1 |
import pandas as pd
import numpy as np
def preprocess(df):
# Preencher valores ausentes
df.fillna(df.median(numeric_only=True), inplace=True)
# Detectar outliers com Z-Score
num_df = df.select_dtypes(include=[np.number])
z_scores = np.abs((num_df - num_df.mean()) / num_df.std())
outliers = (z_scores > 3).sum().sum()
# Encoding categórico
df["Sex"] = df["Sex"].map({"M": 1, "F": 0})
df["ChestPainType"] = df["ChestPainType"].map({"TA": 0, "ATA": 1, "NAP": 2, "ASY": 3})
df["RestingECG"] = df["RestingECG"].map({"Normal": 0, "ST": 1, "LVH": 2})
df["ExerciseAngina"] = df["ExerciseAngina"].map({"Y": 1, "N": 0})
df["ST_Slope"] = df["ST_Slope"].map({"Up": 0, "Flat": 1, "Down": 2})
return df
# Carregar dataset
df = pd.read_csv("https://raw.githubusercontent.com/bligui/Machine-Learning-Ana/refs/heads/main/dados/heart.csv")
# Pré-processamento
df = preprocess(df)
# Selecionar colunas numéricas
numeric_cols = df.select_dtypes(include=[np.number]).columns
# Min-Max Normalization
df_minmax = df.copy()
df_minmax[numeric_cols] = (df_minmax[numeric_cols] - df_minmax[numeric_cols].min()) / (df_minmax[numeric_cols].max() - df_minmax[numeric_cols].min())
# Standardization (Z-score)
df_standard = df.copy()
df_standard[numeric_cols] = (df_standard[numeric_cols] - df_standard[numeric_cols].mean()) / df_standard[numeric_cols].std()
# Mostrar resultados em tabelas markdown
print("### Min-Max Normalized\n")
print(df_minmax.sample(n=10, random_state=42).to_markdown(index=False))
print("\n### Standardized (Z-score)\n")
print(df_standard.sample(n=10, random_state=42).to_markdown(index=False))
3. Divisão dos Dados
Após o pré-processamento do dataset (preenchimento de valores ausentes, codificação de variáveis categóricas e normalização/padronização), os dados foram divididos em conjuntos de treino e teste.
- Treinamento (70%): utilizado para ajustar os parâmetros do modelo Decision Tree.
- Teste (30%): utilizado para avaliar o desempenho do modelo em dados não vistos.
No código, a divisão foi feita com train_test_split
do scikit-learn, garantindo uma separação aleatória, mas reproduzível com random_state=42
.
4. Treinamento do Modelo
O modelo Decision Tree Classifier foi criado e treinado com os dados de treino (X_train
e y_train
).
- Cada nó da árvore representa uma decisão baseada em uma feature.
- Cada folha da árvore representa a classe final prevista (0 = No Disease
ou 1 = Disease
).
O treinamento foi realizado com:
5. Avaliação do Modelo
O modelo treinado foi avaliado utilizando o conjunto de teste (X_test, y_test)
.
- Acurácia (Accuracy): proporção de previsões corretas.
- Visualização da árvore: interpretação das regras de decisão aprendidas pelo modelo.
No código, a avaliação foi feita com:
Validation Accuracy: 0.8333
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from io import StringIO
from sklearn import tree
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
def preprocess(df):
# Preencher valores ausentes
df.fillna(df.median(numeric_only=True), inplace=True)
# Detectar outliers com Z-Score
num_df = df.select_dtypes(include=[np.number])
z_scores = np.abs((num_df - num_df.mean()) / num_df.std())
outliers = (z_scores > 3).sum().sum()
# Encoding categórico
df["Sex"] = df["Sex"].map({"M": 1, "F": 0})
df["ChestPainType"] = df["ChestPainType"].map({"TA": 0, "ATA": 1, "NAP": 2, "ASY": 3})
df["RestingECG"] = df["RestingECG"].map({"Normal": 0, "ST": 1, "LVH": 2})
df["ExerciseAngina"] = df["ExerciseAngina"].map({"Y": 1, "N": 0})
df["ST_Slope"] = df["ST_Slope"].map({"Up": 0, "Flat": 1, "Down": 2})
return df
# Carregar dataset
df = pd.read_csv("https://raw.githubusercontent.com/bligui/Machine-Learning-Ana/refs/heads/main/dados/heart.csv")
# Pré-processamento
df = preprocess(df)
# Selecionar colunas numéricas
numeric_cols = df.select_dtypes(include=[np.number]).columns
# Min-Max Normalization
df_minmax = df.copy()
df_minmax[numeric_cols] = (df_minmax[numeric_cols] - df_minmax[numeric_cols].min()) / (df_minmax[numeric_cols].max() - df_minmax[numeric_cols].min())
# Standardization (Z-score)
df_standard = df.copy()
df_standard[numeric_cols] = (df_standard[numeric_cols] - df_standard[numeric_cols].mean()) / df_standard[numeric_cols].std()
# carregar dataset
df = pd.read_csv("https://raw.githubusercontent.com/bligui/Machine-Learning-Ana/refs/heads/main/dados/heart.csv")
df = preprocess(df)
# dividir variáveis
X = df.drop("HeartDisease", axis=1)
y = df["HeartDisease"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# limitar profundidade da árvore
classifier = tree.DecisionTreeClassifier(max_depth=3, random_state=42)
classifier.fit(X_train, y_train)
# avaliar modelo
y_pred = classifier.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
print(f"Validation Accuracy: {accuracy:.4f}")
# plotar árvore mais compacta
plt.figure(figsize=(12, 8))
tree.plot_tree(classifier, feature_names=X.columns, class_names=["No Disease", "Disease"], filled=True, fontsize=9)
buffer = StringIO()
plt.savefig(buffer, format="svg", transparent=True)
print(buffer.getvalue())
6. Relatório Final
O desenvolvimento do projeto começou pela exploração do conjunto de dados, onde foram observadas características importantes como a predominância do sexo masculino e a maior probabilidade de diagnóstico positivo entre pacientes assintomáticos. Também foi identificada a presença de valores extremos em colesterol e pressão arterial, o que reforçou a necessidade de um bom pré-processamento.
O resultado obtido apresentou uma acurácia em torno de 83%, mostrando que o modelo conseguiu capturar bem os padrões presentes no conjunto de dados. A análise da árvore evidenciou a relevância de variáveis como ChestPainType
, ST_Slope
e Oldpeak
.
Embora os resultados tenham sido positivos, algumas melhorias podem ser consideradas. Entre elas, destacar o uso de mais de um modelo para comparar desempenhos, a aplicação de validação cruzada para obter uma medida mais estável e a análise de métricas além da acurácia, como precisão e recall. Essas ações simples já poderiam aumentar a confiabilidade do modelo e oferecer uma visão mais completa de seu desempenho.
Em resumo, o projeto cumpriu o objetivo de construir um protótipo capaz de prever a presença de doenças cardiovasculares a partir de variáveis clínicas, apresentando bons resultados e mostrando potencial para evoluir em versões futuras.