Entrega Individual
Introdução
- Exploração de dados: Ao selecionar uma base no kaggle referentes a cinco tipos de remédio, remédio A, B, C, X e Y, tem como objetivo prever qual remédio o paciente teria uma resposta melhor. As colunas presentes nesse dataset são Idade, Sexo, Pressão Arterial, nivel de colesterol, nivel de sódio para potássio no sangue e remédio que seria nossa target.
Colunas
- Age (Idade): Essa coluna temos a idade dos pacientes, com a idade minima presente de 15, idade média de 44,3 e maxima de 74 sendo do tipo Integer.
- Sex (Sexo): Essa coluna tem o sexo de cada paciente, divididos em 52% Masculino e 48% feminino, dados do tipo String.
- Blood Pressure (Pressão Arterial): Essa coluna tem os niveis de pressão arterial de cada paciente sendo dividida em 39% HIGH, 29% NORMAL e 32% LOW, dados do tipo String.
- Cholesterol (nivel de colesterol): Essa coluna tem os niveis de colesterol de cada paciente sendo divididos em 52% HIGH e 49% NORMAL, dados do tipo String.
- Na_to_K (sódio para potássio): Essa coluna tem os a razão de sódio para potássio no sangue de um paciente, com a minima de 6,27, media de 16,1 e maxima de 38,2, dados do tipo Float/Decimal.
- Drug (remédio): Essa coluna tem os remédio de melhor resposta para o paciente, dados do tipo String.
Age | Sex | BP | Cholesterol | Na_to_K | Drug |
---|---|---|---|---|---|
36 | M | LOW | NORMAL | 11.424 | drugX |
16 | F | HIGH | NORMAL | 15.516 | drugY |
18 | F | NORMAL | NORMAL | 8.75 | drugX |
59 | F | LOW | HIGH | 10.444 | drugC |
47 | M | LOW | NORMAL | 33.542 | drugY |
51 | M | HIGH | HIGH | 18.295 | drugY |
18 | F | HIGH | NORMAL | 24.276 | drugY |
28 | F | NORMAL | HIGH | 12.879 | drugX |
42 | M | HIGH | NORMAL | 12.766 | drugA |
66 | F | NORMAL | NORMAL | 8.107 | drugX |
Pré-processamento
Primeiro foi feita uma verificação em todas as colunas procurando valores faltantes e substituindo eles pela mediana em valores numéricos ou pela moda em variáveis categóricas. Como vimos na descrição das colunas temos três que possuem dados categóricos do tipo String, sendo elas Sex(Sexo), Blood Pressure(Pressão Arterial) e Cholesterol(nivel de colesterol), para conseguirmos utilizar essas informações é necessario convertelas em numeros, oque foi feito utilizando a biblioteca scikit-learn que possui a função LabelEncoder(), em seguida aplicamos dois tipos de escalonamento às colunas numéricas Age e Na_to_K: padronização (z-score) e normalização min-max.
N-Age | Sex | BP | Cholesterol | N-Na_to_K | Drug |
---|---|---|---|---|---|
0.4 | 1 | 1 | 1 | 0.130411 | drugX |
0 | 0 | 0 | 1 | 0.291292 | drugY |
0.04 | 0 | 2 | 1 | 0.0252801 | drugX |
0.86 | 0 | 1 | 0 | 0.0918813 | drugC |
0.62 | 1 | 1 | 1 | 1 | drugY |
0.7 | 1 | 0 | 0 | 0.40055 | drugY |
0.04 | 0 | 0 | 1 | 0.635699 | drugY |
0.24 | 0 | 2 | 0 | 0.187615 | drugX |
0.52 | 1 | 0 | 1 | 0.183173 | drugA |
1 | 0 | 2 | 1 | 0 | drugX |
import pandas as pd
from sklearn.preprocessing import LabelEncoder
# Preprocess the data
def preprocess(df):
# Fill missing values
df['Age'].fillna(df['Age'].median(), inplace=True)
df['Sex'].fillna(df['Sex'].mode()[0], inplace=True)
df['BP'].fillna(df['BP'].mode()[0], inplace=True)
df['Cholesterol'].fillna(df['Cholesterol'].mode()[0], inplace=True)
df['Na_to_K'].fillna(df['Na_to_K'].median(), inplace=True)
df['Drug'].fillna(df['Drug'].mode()[0], inplace=True)
# Convert categorical variables
label_encoder = LabelEncoder()
df['Sex'] = label_encoder.fit_transform(df['Sex'])
df['BP'] = label_encoder.fit_transform(df['BP'])
df['Cholesterol'] = label_encoder.fit_transform(df['Cholesterol'])
# Select features
features = ['Age', 'Sex', 'BP', 'Cholesterol', 'Na_to_K', 'Drug']
return df[features]
# Load the dataset
df = pd.read_csv('https://raw.githubusercontent.com/alexandremartinelli11/machine-learning/refs/heads/main/data/kaggle/drug200.csv')
df = df.sample(n=10, random_state=42)
# Preprocessing
df = preprocess(df)
# Display the first few rows of the dataset
print(df.to_markdown(index=False))
Age | N-Age | Z-Age | Na_to_K | N-Na_to_K | Z-Na_to_K | |
---|---|---|---|---|---|---|
95 | 36 | 0.4 | -0.117416 | 11.424 | 0.130411 | -0.526121 |
15 | 16 | 0 | -1.23566 | 15.516 | 0.291292 | -0.0105705 |
30 | 18 | 0.04 | -1.12384 | 8.75 | 0.0252801 | -0.863018 |
158 | 59 | 0.86 | 1.16857 | 10.444 | 0.0918813 | -0.649591 |
128 | 47 | 0.62 | 0.49762 | 33.542 | 1 | 2.26052 |
115 | 51 | 0.7 | 0.72127 | 18.295 | 0.40055 | 0.339555 |
69 | 18 | 0.04 | -1.12384 | 24.276 | 0.635699 | 1.0931 |
170 | 28 | 0.24 | -0.564715 | 12.879 | 0.187615 | -0.342806 |
174 | 42 | 0.52 | 0.218058 | 12.766 | 0.183173 | -0.357043 |
45 | 66 | 1 | 1.55996 | 8.107 | 0 | -0.944029 |
import pandas as pd
from sklearn.preprocessing import LabelEncoder
def standardization(df):
df['Z-Age'] = df['Age'].apply(lambda x: (x-df['Age'].mean())/df['Age'].std())
df['N-Age'] = df['Age'].apply(lambda x: (x-df['Age'].min())/(df['Age'].max()-df['Age'].min()))
df['Z-Na_to_K'] = df['Na_to_K'].apply(lambda x: (x-df['Na_to_K'].mean())/df['Na_to_K'].std())
df['N-Na_to_K'] = df['Na_to_K'].apply(lambda x: (x-df['Na_to_K'].min())/(df['Na_to_K'].max()-df['Na_to_K'].min()))
df = df[['Age', 'N-Age', 'Z-Age', 'Na_to_K', 'N-Na_to_K', 'Z-Na_to_K']].dropna()
print(df.head(10).to_markdown())
def preprocess(df):
# Fill missing values
df['Age'].fillna(df['Age'].median(), inplace=True)
df['Sex'].fillna(df['Sex'].mode()[0], inplace=True)
df['BP'].fillna(df['BP'].mode()[0], inplace=True)
df['Cholesterol'].fillna(df['Cholesterol'].mode()[0], inplace=True)
df['Na_to_K'].fillna(df['Na_to_K'].median(), inplace=True)
df['Drug'].fillna(df['Drug'].mode()[0], inplace=True)
# Convert categorical variables
label_encoder = LabelEncoder()
df['Sex'] = label_encoder.fit_transform(df['Sex'])
df['BP'] = label_encoder.fit_transform(df['BP'])
df['Cholesterol'] = label_encoder.fit_transform(df['Cholesterol'])
# Select features
features = ['Age', 'Sex', 'BP', 'Cholesterol', 'Na_to_K', 'Drug']
return df[features]
# Load the dataset
df = pd.read_csv('https://raw.githubusercontent.com/alexandremartinelli11/machine-learning/refs/heads/main/data/kaggle/drug200.csv')
df = df.sample(n=10, random_state=42)
# Preprocessing
df = preprocess(df)
standardization(df)
Divisão dos Dados
O conjunto de dados foi dividido em 70% para treino e 30% para validação, garantindo que o modelo fosse treinado em uma parte significativa das observações, mas ainda avaliado em dados não vistos. O uso do conjunto de validação tem como objetivo detectar e reduzir o risco de overfitting.
Treinamento do Modelo
Foi utilizada a função permutation_importance()
para identificar as features de maior relevancia para o modelo, essa função funciona de seguinte forma: é calculada a acurácia
original do modelo e após isso ele vai em cada feature embaralhando/permutando os valores no conjunto de teste. Ao finalizar esse processo recalcula a acurácia
para cada dimensão permutada e compara o quanto ela caiu em relação a original.
Accuracy: 0.93
Feature Importances (Permutation):
Feature | Importance | Std |
---|---|---|
N-Na_to_K | 0.397222 | 0.070983 |
BP | 0.332222 | 0.037990 |
Cholesterol | 0.149444 | 0.041160 |
N-Age | 0.088889 | 0.035048 |
Relatório de Classificação:
precision | recall | f1-score | support | |
---|---|---|---|---|
0 | 1.000000 | 0.714286 | 0.833333 | 7.000000 |
1 | 0.428571 | 1.000000 | 0.600000 | 3.000000 |
2 | 1.000000 | 1.000000 | 1.000000 | 6.000000 |
3 | 1.000000 | 1.000000 | 1.000000 | 18.000000 |
4 | 1.000000 | 0.923077 | 0.960000 | 26.000000 |
accuracy | 0.933333 | 0.933333 | 0.933333 | 0.933333 |
macro avg | 0.885714 | 0.927473 | 0.878667 | 60.000000 |
weighted avg | 0.971429 | 0.933333 | 0.943222 | 60.000000 |
import numpy as np
import matplotlib.pyplot as plt
from io import StringIO
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
import seaborn as sns
import pandas as pd
from sklearn.preprocessing import LabelEncoder
from sklearn.inspection import permutation_importance
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
from itertools import cycle
plt.figure(figsize=(12, 10))
def standardization(df):
df['Z-Age'] = df['Age'].apply(lambda x: (x-df['Age'].mean())/df['Age'].std())
df['N-Age'] = df['Age'].apply(lambda x: (x-df['Age'].min())/(df['Age'].max()-df['Age'].min()))
df['Z-Na_to_K'] = df['Na_to_K'].apply(lambda x: (x-df['Na_to_K'].mean())/df['Na_to_K'].std())
df['N-Na_to_K'] = df['Na_to_K'].apply(lambda x: (x-df['Na_to_K'].min())/(df['Na_to_K'].max()-df['Na_to_K'].min()))
features = ['N-Age', 'Sex', 'BP', 'Cholesterol', 'N-Na_to_K', 'Drug']
return df[features]
def preprocess(df):
# Fill missing values
df['Age'].fillna(df['Age'].median(), inplace=True)
df['Sex'].fillna(df['Sex'].mode()[0], inplace=True)
df['BP'].fillna(df['BP'].mode()[0], inplace=True)
df['Cholesterol'].fillna(df['Cholesterol'].mode()[0], inplace=True)
df['Na_to_K'].fillna(df['Na_to_K'].median(), inplace=True)
df['Drug'].fillna(df['Drug'].mode()[0], inplace=True)
# Convert categorical variables
label_encoder = LabelEncoder()
df['Sex'] = label_encoder.fit_transform(df['Sex'])
df['BP'] = label_encoder.fit_transform(df['BP'])
df['Cholesterol'] = label_encoder.fit_transform(df['Cholesterol'])
df['Drug'] = label_encoder.fit_transform(df['Drug'])
# Select features
features = ['Age', 'Sex', 'BP', 'Cholesterol', 'Na_to_K', 'Drug']
return df[features]
# Load the dataset
df = pd.read_csv('https://raw.githubusercontent.com/alexandremartinelli11/machine-learning/refs/heads/main/data/kaggle/drug200.csv')
# Preprocessing
d = preprocess(df.copy())
d = standardization(d)
# Generate synthetic dataset
X = d[['N-Age', 'BP', 'Cholesterol', 'N-Na_to_K']]
y = d['Drug']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
# Train KNN model
knn = KNeighborsClassifier(n_neighbors=3)
knn.fit(X_train, y_train)
predictions = knn.predict(X_test)
print(f"Accuracy: {accuracy_score(y_test, predictions):.2f}")
r = permutation_importance(
knn,
X_test,
y_test,
n_repeats=30,
random_state=42,
scoring='accuracy'
)
feature_importance = pd.DataFrame({
'Feature': X.columns,
'Importance': r.importances_mean,
'Std': r.importances_std
})
report_dict = classification_report(y_test, predictions, output_dict=True)
report_df = pd.DataFrame(report_dict).transpose()
cm = confusion_matrix(y_test, predictions)
labels = knn.classes_
cm_df = pd.DataFrame(cm, index=labels, columns=labels)
# ordenar e mostrar (HTML igual ao seu exemplo)
feature_importance = feature_importance.sort_values(by='Importance', ascending=False)
print("<br>Feature Importances (Permutation):")
print(feature_importance.to_html(index=False))
print("<h3>Relatório de Classificação:</h3>")
print(report_df.to_html(classes="table table-bordered table-striped", border=0))
# Escalar features
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
# Reduzir para 2 dimensões (apenas para visualização)
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X_scaled)
# Split train/test
X_train, X_test, y_train, y_test = train_test_split(X_pca, y, test_size=0.3, random_state=42)
# Treinar KNN
knn = KNeighborsClassifier(n_neighbors=3)
knn.fit(X_train, y_train)
predictions = knn.predict(X_test)
# Visualize decision boundary
h = 0.02 # Step size in mesh
x_min, x_max = X_pca[:, 0].min() - 1, X_pca[:, 0].max() + 1
y_min, y_max = X_pca[:, 1].min() - 1, X_pca[:, 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()])
Z = Z.reshape(xx.shape)
plt.contourf(xx, yy, Z, cmap=plt.cm.RdYlBu, alpha=0.3)
sns.scatterplot(x=X_pca[:, 0], y=X_pca[:, 1], hue=y, style=y, palette="deep", s=100)
plt.xlabel("Feature 1")
plt.ylabel("Feature 2")
plt.title("KNN Decision Boundary (k=3)")
# Display the plot
buffer = StringIO()
plt.savefig(buffer, format="svg", transparent=True)
print(buffer.getvalue())
Avaliação do Modelo
O modelo KNN (k=3) obteve aproximadamente 0,93 de acurácia nos dados de teste, indicando bom desempenho geral. A análise da curva ROC multiclasse (One-vs-Rest) mostra que todas as classes, exceto uma, apresentam área sob a curva (AUC) igual a 1,0, enquanto a última classe apresenta AUC de 0,99, sinalizando que o modelo praticamente não erra na maioria das classes, mas pode cometer alguns erros sutis na previsão de uma classe específica. Apesar da alta acurácia e AUC, é recomendável comparar o desempenho nos dados de treino e teste para verificar possíveis sinais de overfitting. Para aprimorar o modelo, podemos testar diferentes Ks no KNN e usar técnicas de balanceamento de classes caso haja desbalanceamento.