Skip to content

Entrega Individual

  1. Alexandre Martinelli

Introdução

  1. 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

  1. 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.
  2. Sex (Sexo): Essa coluna tem o sexo de cada paciente, divididos em 52% Masculino e 48% feminino, dados do tipo String.
  3. 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.
  4. 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.
  5. 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.
  6. 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
2025-09-28T23:59:31.746222 image/svg+xml Matplotlib v3.10.6, https://matplotlib.org/ 2025-09-28T23:59:31.965381 image/svg+xml Matplotlib v3.10.6, https://matplotlib.org/

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.

Referências

Material for MkDocs