Exploração de Dados
Descrição da base de dados e código de exploração
O câncer de mama é o tipo de câncer mais comum entre mulheres em todo o mundo, responsável por aproximadamente 25% de todos os casos e afetando milhões de pessoas todos os anos. Ele se desenvolve quando células da mama começam a crescer de forma descontrolada, formando tumores que podem ser identificados por exames de imagem (raios-X) ou detectados como nódulos.
O principal desafio no diagnóstico é diferenciar corretamente os tumores malignos (cancerosos) dos benignos (não cancerosos). O objetivo deste projeto é desenvolver um modelo de classificação supervisionada capaz de prever, com base em atributos numéricos das células, se um tumor é maligno ou benigno, e estabelecer um diagnóstico confiável.
Sobre o Dataset
Total de registros: 569 amostras
Variável alvo: diagnosis (M = maligno, B = benigno)
Número de variáveis preditoras: 30 atributos numéricos relacionados ao tamanho, textura, formato e concavidade das células.
| id | diagnosis | radius_mean | texture_mean | perimeter_mean | area_mean | smoothness_mean | compactness_mean | concavity_mean | concave points_mean | symmetry_mean | fractal_dimension_mean | radius_se | texture_se | perimeter_se | area_se | smoothness_se | compactness_se | concavity_se | concave points_se | symmetry_se | fractal_dimension_se | radius_worst | texture_worst | perimeter_worst | area_worst | smoothness_worst | compactness_worst | concavity_worst | concave points_worst | symmetry_worst | fractal_dimension_worst |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 87930 | B | 12.47 | 18.6 | 81.09 | 481.9 | 0.09965 | 0.1058 | 0.08005 | 0.03821 | 0.1925 | 0.06373 | 0.3961 | 1.044 | 2.497 | 30.29 | 0.006953 | 0.01911 | 0.02701 | 0.01037 | 0.01782 | 0.003586 | 14.97 | 24.64 | 96.05 | 677.9 | 0.1426 | 0.2378 | 0.2671 | 0.1015 | 0.3014 | 0.0875 |
| 859575 | M | 18.94 | 21.31 | 123.6 | 1130 | 0.09009 | 0.1029 | 0.108 | 0.07951 | 0.1582 | 0.05461 | 0.7888 | 0.7975 | 5.486 | 96.05 | 0.004444 | 0.01652 | 0.02269 | 0.0137 | 0.01386 | 0.001698 | 24.86 | 26.58 | 165.9 | 1866 | 0.1193 | 0.2336 | 0.2687 | 0.1789 | 0.2551 | 0.06589 |
| 8670 | M | 15.46 | 19.48 | 101.7 | 748.9 | 0.1092 | 0.1223 | 0.1466 | 0.08087 | 0.1931 | 0.05796 | 0.4743 | 0.7859 | 3.094 | 48.31 | 0.00624 | 0.01484 | 0.02813 | 0.01093 | 0.01397 | 0.002461 | 19.26 | 26 | 124.9 | 1156 | 0.1546 | 0.2394 | 0.3791 | 0.1514 | 0.2837 | 0.08019 |
| 907915 | B | 12.4 | 17.68 | 81.47 | 467.8 | 0.1054 | 0.1316 | 0.07741 | 0.02799 | 0.1811 | 0.07102 | 0.1767 | 1.46 | 2.204 | 15.43 | 0.01 | 0.03295 | 0.04861 | 0.01167 | 0.02187 | 0.006005 | 12.88 | 22.91 | 89.61 | 515.8 | 0.145 | 0.2629 | 0.2403 | 0.0737 | 0.2556 | 0.09359 |
| 921385 | B | 11.54 | 14.44 | 74.65 | 402.9 | 0.09984 | 0.112 | 0.06737 | 0.02594 | 0.1818 | 0.06782 | 0.2784 | 1.768 | 1.628 | 20.86 | 0.01215 | 0.04112 | 0.05553 | 0.01494 | 0.0184 | 0.005512 | 12.26 | 19.68 | 78.78 | 457.8 | 0.1345 | 0.2118 | 0.1797 | 0.06918 | 0.2329 | 0.08134 |
| 927241 | M | 20.6 | 29.33 | 140.1 | 1265 | 0.1178 | 0.277 | 0.3514 | 0.152 | 0.2397 | 0.07016 | 0.726 | 1.595 | 5.772 | 86.22 | 0.006522 | 0.06158 | 0.07117 | 0.01664 | 0.02324 | 0.006185 | 25.74 | 39.42 | 184.6 | 1821 | 0.165 | 0.8681 | 0.9387 | 0.265 | 0.4087 | 0.124 |
| 9012000 | M | 22.01 | 21.9 | 147.2 | 1482 | 0.1063 | 0.1954 | 0.2448 | 0.1501 | 0.1824 | 0.0614 | 1.008 | 0.6999 | 7.561 | 130.2 | 0.003978 | 0.02821 | 0.03576 | 0.01471 | 0.01518 | 0.003796 | 27.66 | 25.8 | 195 | 2227 | 0.1294 | 0.3885 | 0.4756 | 0.2432 | 0.2741 | 0.08574 |
| 853201 | M | 17.57 | 15.05 | 115 | 955.1 | 0.09847 | 0.1157 | 0.09875 | 0.07953 | 0.1739 | 0.06149 | 0.6003 | 0.8225 | 4.655 | 61.1 | 0.005627 | 0.03033 | 0.03407 | 0.01354 | 0.01925 | 0.003742 | 20.01 | 19.52 | 134.9 | 1227 | 0.1255 | 0.2812 | 0.2489 | 0.1456 | 0.2756 | 0.07919 |
| 8611161 | B | 13.34 | 15.86 | 86.49 | 520 | 0.1078 | 0.1535 | 0.1169 | 0.06987 | 0.1942 | 0.06902 | 0.286 | 1.016 | 1.535 | 12.96 | 0.006794 | 0.03575 | 0.0398 | 0.01383 | 0.02134 | 0.004603 | 15.53 | 23.19 | 96.66 | 614.9 | 0.1536 | 0.4791 | 0.4858 | 0.1708 | 0.3527 | 0.1016 |
| 911673 | B | 13.9 | 16.62 | 88.97 | 599.4 | 0.06828 | 0.05319 | 0.02224 | 0.01339 | 0.1813 | 0.05536 | 0.1555 | 0.5762 | 1.392 | 14.03 | 0.003308 | 0.01315 | 0.009904 | 0.004832 | 0.01316 | 0.002095 | 15.14 | 21.8 | 101.2 | 718.9 | 0.09384 | 0.2006 | 0.1384 | 0.06222 | 0.2679 | 0.07698 |
Pré processamento
Explicação dos processos realizados no pré-processamento
Antes do treinamento do modelo, foi realizado um pré-processamento para garantir a qualidade e consistência dos dados:
Remoção de colunas irrelevantes – A coluna id foi descartada, pois não contribui para o aprendizado do modelo.
Tratamento de valores ausentes – Foram encontrados valores faltantes em algumas variáveis (concavity_worst e concave points_worst). Esses valores foram preenchidos utilizando a mediana.
Codificação de variáveis categóricas – A variável alvo diagnosis foi transformada em valores numéricos por meio de Label Encoding (M = 1, B = 0), permitindo sua utilização pelo algoritmo de aprendizado.
Todos os processos de pré-processamento feitos no algoritmo de árvore de decisão foram feitos também no algoritmo de KNN.
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
#carregamento da base
df = pd.read_csv('https://raw.githubusercontent.com/MariaLuizazz/MACHINE-LEARNING-PESSOAL/refs/heads/main/dados/breast-cancer.csv')
#pré processamento
#remoção da coluna id pois é irrelevante para o modelo
df = df.drop(columns=['id'])
#conversão de letra para número
label_encoder = LabelEncoder()
df['diagnosis'] = label_encoder.fit_transform(df['diagnosis'])
#features escolhidas, todas menos diagnosis e id
X = df.drop(columns=['diagnosis'])
y = df['diagnosis']
#imputação com mediana de valores ausentes nas features concavity_worts e concavity points_worst
df['concavity_mean'].fillna(df['concavity_mean'].median(), inplace=True)
df['concave points_mean'].fillna(df['concave points_mean'].median(), inplace=True)
print(df.sample(n=10, random_state=42).to_markdown(index=False))
| diagnosis | radius_mean | texture_mean | perimeter_mean | area_mean | smoothness_mean | compactness_mean | concavity_mean | concave points_mean | symmetry_mean | fractal_dimension_mean | radius_se | texture_se | perimeter_se | area_se | smoothness_se | compactness_se | concavity_se | concave points_se | symmetry_se | fractal_dimension_se | radius_worst | texture_worst | perimeter_worst | area_worst | smoothness_worst | compactness_worst | concavity_worst | concave points_worst | symmetry_worst | fractal_dimension_worst |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 12.47 | 18.6 | 81.09 | 481.9 | 0.09965 | 0.1058 | 0.08005 | 0.03821 | 0.1925 | 0.06373 | 0.3961 | 1.044 | 2.497 | 30.29 | 0.006953 | 0.01911 | 0.02701 | 0.01037 | 0.01782 | 0.003586 | 14.97 | 24.64 | 96.05 | 677.9 | 0.1426 | 0.2378 | 0.2671 | 0.1015 | 0.3014 | 0.0875 |
| 1 | 18.94 | 21.31 | 123.6 | 1130 | 0.09009 | 0.1029 | 0.108 | 0.07951 | 0.1582 | 0.05461 | 0.7888 | 0.7975 | 5.486 | 96.05 | 0.004444 | 0.01652 | 0.02269 | 0.0137 | 0.01386 | 0.001698 | 24.86 | 26.58 | 165.9 | 1866 | 0.1193 | 0.2336 | 0.2687 | 0.1789 | 0.2551 | 0.06589 |
| 1 | 15.46 | 19.48 | 101.7 | 748.9 | 0.1092 | 0.1223 | 0.1466 | 0.08087 | 0.1931 | 0.05796 | 0.4743 | 0.7859 | 3.094 | 48.31 | 0.00624 | 0.01484 | 0.02813 | 0.01093 | 0.01397 | 0.002461 | 19.26 | 26 | 124.9 | 1156 | 0.1546 | 0.2394 | 0.3791 | 0.1514 | 0.2837 | 0.08019 |
| 0 | 12.4 | 17.68 | 81.47 | 467.8 | 0.1054 | 0.1316 | 0.07741 | 0.02799 | 0.1811 | 0.07102 | 0.1767 | 1.46 | 2.204 | 15.43 | 0.01 | 0.03295 | 0.04861 | 0.01167 | 0.02187 | 0.006005 | 12.88 | 22.91 | 89.61 | 515.8 | 0.145 | 0.2629 | 0.2403 | 0.0737 | 0.2556 | 0.09359 |
| 0 | 11.54 | 14.44 | 74.65 | 402.9 | 0.09984 | 0.112 | 0.06737 | 0.02594 | 0.1818 | 0.06782 | 0.2784 | 1.768 | 1.628 | 20.86 | 0.01215 | 0.04112 | 0.05553 | 0.01494 | 0.0184 | 0.005512 | 12.26 | 19.68 | 78.78 | 457.8 | 0.1345 | 0.2118 | 0.1797 | 0.06918 | 0.2329 | 0.08134 |
| 1 | 20.6 | 29.33 | 140.1 | 1265 | 0.1178 | 0.277 | 0.3514 | 0.152 | 0.2397 | 0.07016 | 0.726 | 1.595 | 5.772 | 86.22 | 0.006522 | 0.06158 | 0.07117 | 0.01664 | 0.02324 | 0.006185 | 25.74 | 39.42 | 184.6 | 1821 | 0.165 | 0.8681 | 0.9387 | 0.265 | 0.4087 | 0.124 |
| 1 | 22.01 | 21.9 | 147.2 | 1482 | 0.1063 | 0.1954 | 0.2448 | 0.1501 | 0.1824 | 0.0614 | 1.008 | 0.6999 | 7.561 | 130.2 | 0.003978 | 0.02821 | 0.03576 | 0.01471 | 0.01518 | 0.003796 | 27.66 | 25.8 | 195 | 2227 | 0.1294 | 0.3885 | 0.4756 | 0.2432 | 0.2741 | 0.08574 |
| 1 | 17.57 | 15.05 | 115 | 955.1 | 0.09847 | 0.1157 | 0.09875 | 0.07953 | 0.1739 | 0.06149 | 0.6003 | 0.8225 | 4.655 | 61.1 | 0.005627 | 0.03033 | 0.03407 | 0.01354 | 0.01925 | 0.003742 | 20.01 | 19.52 | 134.9 | 1227 | 0.1255 | 0.2812 | 0.2489 | 0.1456 | 0.2756 | 0.07919 |
| 0 | 13.34 | 15.86 | 86.49 | 520 | 0.1078 | 0.1535 | 0.1169 | 0.06987 | 0.1942 | 0.06902 | 0.286 | 1.016 | 1.535 | 12.96 | 0.006794 | 0.03575 | 0.0398 | 0.01383 | 0.02134 | 0.004603 | 15.53 | 23.19 | 96.66 | 614.9 | 0.1536 | 0.4791 | 0.4858 | 0.1708 | 0.3527 | 0.1016 |
| 0 | 13.9 | 16.62 | 88.97 | 599.4 | 0.06828 | 0.05319 | 0.02224 | 0.01339 | 0.1813 | 0.05536 | 0.1555 | 0.5762 | 1.392 | 14.03 | 0.003308 | 0.01315 | 0.009904 | 0.004832 | 0.01316 | 0.002095 | 15.14 | 21.8 | 101.2 | 718.9 | 0.09384 | 0.2006 | 0.1384 | 0.06222 | 0.2679 | 0.07698 |
Divisão dos dados, Treinamento do Modelo e Avaliação do Modelo.
O dataset foi dividido em conjuntos de treino e teste com uma proporção de 80% treino e 20% teste.
O questionamento principal foi: Quais features são mais relevantes para o diagnóstico de câncer de mama de acordo com o que foi fornecido no meu dataset?
Ao analisar o dataset, foi bom lembrar que trata-se da previsão de um diagnóstico, é preciso entender a base e o que eu quero prever. Em uma pesquisa rápida para entender melhor, concluí que: para a escolha das minhas features eu deveria ficar atenta às minhas variáveis mais relevantes.
Se o nódulo é redondo, pequeno e com bordas suaves → mais provável benigno. Se o nódulo é grande, irregular, com bordas cheias de reentrâncias → mais provável maligno. Nódulos malignos costumam ser maiores, com contornos irregulares e não lisos, enquanto benignos tendem a ser mais arredondados e bem delimitados.
Portanto, as variáveis mais importantes do meu dataset para o diagnóstico seriam aquelas que especificam tamanho, formato e textura do nódulo. No caso, as variáveis escolhidas foram: texture_mean e radius_mean.
TESTE 1
Ao analiser o primeiro gráfico, o modelo paresentou sinais de overfitting, realizando uma validação cruzada e matriz de confusão foi possível constatar que de acordo com o modelo sem balancemanto e com k = 3 que:
102: Pacientes saudáveis corretamente diagnosticados
5: Pacientes saudáveis erroneamente diagnosticados com câncer (Falso Positivo)
14: Pacientes com câncer erroneamente diagnosticados como saudáveis (Falso Negativo - GRAVE)
50: Pacientes com câncer corretamente diagnosticados
Com esses erros o gráfico resultou da seguinte forma:
Accuracy: 0.89 Validação Cruzada: 0.873 ± 0.037 Matriz de Confusão: [[102 5] [ 14 50]]
import numpy as np
import matplotlib.pyplot as plt
from io import StringIO
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score
import seaborn as sns
import pandas as pd
from sklearn.preprocessing import LabelEncoder
plt.figure(figsize=(12,10))
#carregamento da base
df = pd.read_csv('https://raw.githubusercontent.com/MariaLuizazz/MACHINE-LEARNING-PESSOAL/refs/heads/main/dados/breast-cancer.csv')
#Préprocess
#remoção da coluna id pois é irrelevante para o modelo
df = df.drop(columns=['id'])
#conversão de letra para número
label_encoder = LabelEncoder()
df['diagnosis'] = label_encoder.fit_transform(df['diagnosis'])
#imputação com mediana de valores ausentes nas features concavity_worts e concavity points_worst
df['concavity_mean'].fillna(df['concavity_mean'].median(), inplace=True)
df['concave points_mean'].fillna(df['concave points_mean'].median(), inplace=True)
#escolha de features
X = df[['radius_mean', 'texture_mean']]
y = df['diagnosis']
#Separação de treino e teste
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42, stratify=y)
#Treianamento do KNN
knn = KNeighborsClassifier(n_neighbors=3)
knn.fit(X_train, y_train)
#Teste e validação
predictions = knn.predict(X_test)
print(f"Accuracy: {accuracy_score(y_test, predictions):.2f}")
# Mapeia os rótulos: 0 -> Benigno, 1 -> Maligno
labels_map = {0: "Benigno", 1: "Maligno"}
y_labels = y.map(labels_map)
#Preparação para o gráfico da fronteira de decisão(malha de visualização)
h = 0.02
x_min, x_max = X.iloc[:, 0].min() - 1, X.iloc[:, 0].max() + 1
y_min, y_max = X.iloc[:, 1].min() - 1, X.iloc[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, h),np.arange(y_min, y_max, h))
#Prevendo classe em cada ponto
Z = knn.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
#gráfico final
plt.contourf(xx, yy, Z, cmap=plt.cm.RdYlGn_r, alpha=0.3)
sns.scatterplot(x=X.iloc[:, 0], y=X.iloc[:, 1], hue=y_labels, style=y_labels, palette={'Benigno': 'green', 'Maligno': 'red'}, s=100) #motivooooooo do errroo
plt.xlabel("radius_mean")
plt.ylabel("texture_mean")
plt.title("KNN Decision Boundary (k=3) - Diagnóstico de Câncer")
plt.legend(title="Diagnóstico")
#Exibição do gráfico
buffer = StringIO()
plt.savefig(buffer, format="svg", transparent=True)
print(buffer.getvalue())
# 1. Validação Cruzada
from sklearn.model_selection import cross_val_score
scores = cross_val_score(knn, X, y, cv=5)
print(f"Validação Cruzada: {scores.mean():.3f} ± {scores.std():.3f}")
# 3. Matriz de Confusão
from sklearn.metrics import confusion_matrix
print("Matriz de Confusão:")
print(confusion_matrix(y_test, predictions))
TESTE 2
No teste 2 ocorreu aplicação da técnica smote apenas nos dados de treino para balanceamento, sem vazmento de informações para o teste. Realizando uma validação cruzada e matriz de confusão foi possível constatar que de acordo com o modelo co balancemento e com k = 11 que:
O modelo balanceado com K=11 é MAIS SEGURO que a versão anterior, reduzindo os perigosos falsos negativos em 28,6%.
101: Pacientes saudáveis corretamente diagnosticados
6: Pacientes saudáveis erroneamente diagnosticados com câncer (Falso Positivo)
10: Pacientes com câncer erroneamente diagnosticados como saudáveis (Falso Negativo - GRAVE)
54: Pacientes com câncer corretamente diagnosticados
- Conclusão: a quantidade de falsos negativos cairam, e de positivos aumentou.
Accuracy: 0.91 Validação Cruzada: 0.884 ± 0.036 Matriz de Confusão: [[101 6] [ 10 54]]
import numpy as np
import matplotlib.pyplot as plt
from io import StringIO
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score
import seaborn as sns
import pandas as pd
from sklearn.preprocessing import LabelEncoder
from imblearn.over_sampling import SMOTE
plt.figure(figsize=(12,10))
#carregamento da base
df = pd.read_csv('https://raw.githubusercontent.com/MariaLuizazz/MACHINE-LEARNING-PESSOAL/refs/heads/main/dados/breast-cancer.csv')
#Préprocess
#remoção da coluna id pois é irrelevante para o modelo
df = df.drop(columns=['id'])
#conversão de letra para número
label_encoder = LabelEncoder()
df['diagnosis'] = label_encoder.fit_transform(df['diagnosis'])
#imputação com mediana de valores ausentes nas features concavity_worts e concavity points_worst
df['concavity_mean'].fillna(df['concavity_mean'].median(), inplace=True)
df['concave points_mean'].fillna(df['concave points_mean'].median(), inplace=True)
#escolha de features
# Em vez de usar apenas 2 features
X = df[['radius_mean', 'texture_mean']]
y = df['diagnosis']
#Separação de treino e teste
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42, stratify=y)
smote = SMOTE(random_state=42)
X_train_balanced, y_train_balanced = smote.fit_resample(X_train, y_train)
#Treianamento do KNN
knn = KNeighborsClassifier(n_neighbors=11)
knn.fit(X_train_balanced, y_train_balanced)
#Teste e validação
predictions = knn.predict(X_test)
print(f"Accuracy: {accuracy_score(y_test, predictions):.2f}")
# Mapeia os rótulos: 0 -> Benigno, 1 -> Maligno
labels_map = {0: "Benigno", 1: "Maligno"}
y_labels = y.map(labels_map)
#Preparação para o gráfico da fronteira de decisão(malha de visualização)
h = 0.02
x_min, x_max = X.iloc[:, 0].min() - 1, X.iloc[:, 0].max() + 1
y_min, y_max = X.iloc[:, 1].min() - 1, X.iloc[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, h),np.arange(y_min, y_max, h))
#Prevendo classe em cada ponto
Z = knn.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
#gráfico final
plt.contourf(xx, yy, Z, cmap=plt.cm.RdYlGn_r, alpha=0.3)
sns.scatterplot(x=X.iloc[:, 0], y=X.iloc[:, 1], hue=y_labels, style=y_labels, palette={'Benigno': 'green', 'Maligno': 'red'}, s=100) #motivooooooo do errroo
plt.xlabel("radius_mean")
plt.ylabel("texture_mean")
plt.title("KNN Decision Boundary (k=11) - Diagnóstico de Câncer (Com Balanceamento SMOTE)")
plt.legend(title="Diagnóstico")
#Exibição do gráfico
buffer = StringIO()
plt.savefig(buffer, format="svg", transparent=True)
print(buffer.getvalue())
# 1. Validação Cruzada
from sklearn.model_selection import cross_val_score
scores = cross_val_score(knn, X, y, cv=5)
print(f"Validação Cruzada: {scores.mean():.3f} ± {scores.std():.3f}")
# 3. Matriz de Confusão
from sklearn.metrics import confusion_matrix
print("Matriz de Confusão:")
print(confusion_matrix(y_test, predictions))
Relatorio final
-
O modelo KNN com k=3 memoriza os dados de treino e usa distância para fazer previsões. Para cada novo tumor, ele encontra os 3 tumores mais similares no conjunto de treino e decide pela maioria, a mesma coisa ocorre com k = 11.
-
Sobre a Avaliação: Após a etapa de treino e teste, o processo entregou uma acurácia de 86% que nos mostra que o modelo acerta 86 em cada 100 previsões.
-
Sobre a Visualização: A fronteira de decisão mostra como o modelo separa tumores benignos de malignos. Áreas coloridas mostram onde o modelo prevê cada classe. Observando o modelo e a acurácia o modelo apresenta sinais de overfitting.
Conclusões - O balanceamento com SMOTE foi crucial para melhorar a detecção de casos malignos - k=11 mostrou-se superior a k=3 para este problema médico - A troca especificidade-sensibilidade foi clinicamente vantajosa - O modelo balanceado é mais seguro para aplicação clínica