K-Means
Objetivo
O objetivo geral deste roteiro é utilizar as bibliotecas pandas, numpy, matplotlib e scikit-learn, além de uma base escolhida no Kagle, para treinar e avaliar um algoritmo de K-Means.
Base de Dados
A base de dados escolhida para a realização deste roteiro foi a MBA Admission Dataset. Esta base possui 6194 linhas e 10 colunas, incluido uma coluna de ID da aplicação e uma coluna de status da admissão, esta é a váriavel dependente que será objeto da classificação.
Análise da Base
A seguir foi feita uma análise do significado e composição de cada coluna presente na base com a finalidade de indentificar possíveis problemas á serem tradados posteriormente.
Esta coluna é composta pelos ID's das aplicações realizadas, ou seja trata-se de um valor numérico lógico, único a cada aplicação, desta forma pode-se afirmar que esta coluna não terá relevância para o algoritmo e deverá ser retirada da base para treinamento.
Esta coluna é preenchida com o genêro do aplicante, contendo apenas valores textuais entre "male" e "female", não incluindo opções como "non-binary", "other" ou "prefer not to inform". Logo, estes dados, por serem textuais e apresentarem binariedade, deverão ser transformados em uma variável binária numérica para que se atinja um melhor desempenho do algoritmo.
Esta coluna é preenchida com valores booleanos que classificam o aplicantente como "estrangeiro" ou "não-estrangeiro". Logo, estes dados, por serem textuais e apresentarem binariedade, deveriam ser transformados em uma variável binária numérica para que se atinja um melhor desempenho do algoritmo.
Entretanto, a classificação desta coluna tambem poder ser notada na coluna "race", pois todos os valores nulos presentes na posterior são unicamente referentes a alunos estrangeiros.
Esta coluna representa a performance acadêmica prévia do aplicante, que é calculada a partir do histórico escolar. Neste as notas particulares de cada matéria podem variar de 0 á 4, 0 sendo a pior nota possível e 4 a maior. Neste caso os GPA's dos aplicantes variam entre 2.65 e 3.77, apresentando uma curva normal. Devido ao fato destes valores serem numéricos e a maioria das variáveis do modelo serem binárias ou dummies, esta deve ser padronizada para valores entre 0 e 1.
Esta coluna representa em que curso o aplicante deseja entrar, podendo assumir um de três valores textuais: "Humanities", "STEM" e "Business". Neste caso, como a variavel é textual, não apresenta binariedade e não possui noção de escala (como em "ruim", "regular" e "bom"), a técnica correta para o tratamento desta coluna será o "One Hot", transformando-a em 2 variáveis dummies.
Esta coluna representa a indentificação racial do aplicante, porém tambem há diversas linhas com valor nulo nesta coluna. Ao comparar o preenchimento desta coluna com as demais, percebe-se que o valor desta coluna so se apresenta nulo para estudantes estrangeiros, tornando a coluna "international" redundante.
Desta forma, para otimizar o modelo, devemos remover a coluna "international", prezando pela menor quantidade de colunas possível, e gerar dummies para cada valor registrado na coluna, pois esta não possui noção de escala (como em "ruim", "regular" e "bom").
Esta coluna representa o desempenho do aplicante na prova de adimissão, variando de 570 á 780, porém estas notas não apresentam uma curva normal, pois há muitos registros de notas menores que a média a mais do que há registos de notas maiores que a média. Devido ao fato destes valores serem numéricos e a maioria das variáveis do modelo serem binárias ou dummies, esta deve ser padronizada para valores entre 0 e 1.
Esta coluna representa o tempo de experiência prévia do aplicante no mercado, exibida em anos. Os valores podem variar de 1 á 9, apresentando uma curva normal. Devido ao fato destes valores serem numéricos e a maioria das variáveis do modelo serem binárias ou dummies, esta deve ser padronizada para valores entre 0 e 1.
Esta coluna representa a área de experiência prévia do aplicante no mercado, podendo assumir, nesta base um de quatorze valores textuais. E como esta coluna não apresenta binariedade e não possui noção de escala (como em "ruim", "regular" e "bom"), a técnica correta para o tratamento desta coluna será o "One Hot", transformando-a em 13 variáveis dummies.
Esta coluna apresenta valores em texto para os aplicantes admitos e na lista de espera, além de valores nulos para aqueles que não foram aceitos. Esta coluna é o objeto da classificação e portanto será separada das outras colunas da base, e os valores nulos deveram ser preenchidos.
Pré-processamento
Esta secção visa preparar os dados para o treinamento da árvore de decisão, atendendo as observações e análises feitas no tópico anterior.
| gender | gpa | gmat | work_exp | admission | race_Black | race_Hispanic | race_Other | race_White | race_international | major_Humanities | major_STEM | work_industry_Consulting | work_industry_Energy | work_industry_Financial Services | work_industry_Health Care | work_industry_Investment Banking | work_industry_Investment Management | work_industry_Media/Entertainment | work_industry_Nonprofit/Gov | work_industry_Other | work_industry_PE/VC | work_industry_Real Estate | work_industry_Retail | work_industry_Technology |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1 | 0.523243 | -0.225052 | -0.0164207 | Refused | False | True | False | False | False | False | True | False | False | False | False | False | False | False | False | False | True | False | False | False |
| 1 | -1.12661 | 0.586457 | 0.952244 | Refused | False | False | False | True | False | True | False | False | False | False | False | False | False | False | False | True | False | False | False | False |
| 1 | 1.31517 | 2.0066 | -0.985085 | Refused | False | True | False | False | False | False | True | True | False | False | False | False | False | False | False | False | False | False | False | False |
| 1 | -0.268685 | 0.180703 | 0.952244 | Refused | False | False | False | True | False | False | True | False | False | False | False | True | False | False | False | False | False | False | False | False |
| 0 | 0.721225 | 0.586457 | 0.952244 | Refused | True | False | False | False | False | False | True | False | False | False | False | False | False | False | False | False | True | False | False | False |
| 1 | -0.268685 | -0.427929 | -0.985085 | Refused | False | False | False | False | False | False | False | True | False | False | False | False | False | False | False | False | False | False | False | False |
| 1 | -0.598655 | -0.225052 | -0.0164207 | Refused | False | False | False | False | True | False | False | False | False | False | False | False | False | False | False | False | False | False | False | True |
| 0 | 0.457249 | 1.39797 | -0.0164207 | Refused | False | False | False | False | True | True | False | False | False | False | False | False | False | False | False | False | True | False | False | False |
| 0 | 0.655231 | -0.427929 | -0.985085 | Refused | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | False | True |
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from io import StringIO
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import LabelEncoder
from sklearn.inspection import permutation_importance
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score
import seaborn as sns
plt.figure(figsize=(12, 10))
label_encoder = LabelEncoder()
scaler = StandardScaler()
df = pd.read_csv("./docs/base/MBA.csv")
#Excluir as conlunas não desejadas
df = df.drop(columns= ["application_id", "international"])
#Preencher os valores nulos da coluna "race"
df["race"] = df["race"].fillna("international")
#Preencher os valores nulos da coluna "admission"
df["admission"] = df["admission"].fillna("Refused")
#Label encoding da coluna em texto binária
df["gender"] = label_encoder.fit_transform(df["gender"])
#Escolonando as váriaveis continuas
df["gpa"] = scaler.fit_transform(df[["gpa"]])
df["gmat"] = scaler.fit_transform(df[["gmat"]])
df["work_exp"] = scaler.fit_transform(df[["work_exp"]])
#Gerando dummies das colunas em texto não binárias
df = pd.get_dummies(df,columns= ["race", "major", "work_industry"], drop_first=True)
print(df.sample(frac=.0015).to_markdown(index=False))
| application_id | gender | international | gpa | major | race | gmat | work_exp | work_industry | admission |
|---|---|---|---|---|---|---|---|---|---|
| 5868 | Male | False | 3.46 | Business | Other | 730 | 4 | PE/VC | Admit |
| 1243 | Male | True | 3.22 | STEM | nan | 620 | 5 | PE/VC | nan |
| 3619 | Male | False | 3.34 | Business | Other | 650 | 5 | Other | nan |
| 5335 | Male | False | 3.3 | Business | Asian | 600 | 6 | Consulting | nan |
| 1920 | Female | False | 3.55 | STEM | White | 680 | 5 | Consulting | nan |
| 3448 | Female | False | 3.05 | Humanities | Black | 570 | 6 | Nonprofit/Gov | nan |
| 5932 | Male | False | 3.26 | Humanities | Asian | 670 | 3 | CPG | Admit |
| 1160 | Female | True | 2.97 | Business | nan | 600 | 2 | Consulting | nan |
| 5793 | Male | False | 3.11 | Humanities | White | 580 | 5 | Technology | nan |
Divisão dos dados
Devido a composição da coluna de admission, a seperação dos dados deve ser feita com maior atenção. Caso esta separação fosse feita com aleatoriedade, haveria a possibilidade de que a base de treinamento tornar-se enviesada. Portanto, esta deve ser executada com proporcionalidade a composição da coluna alvo. Tendo em vista situações como esta o sickit-learn já implementou o sorteamento extratificado como a opção stratify no comando train_test_split().
Além disto para o treinamento foi utilizado uma separação arbitrária da base em 70% treinamento e 30% validação.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from io import StringIO
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import LabelEncoder
from sklearn.inspection import permutation_importance
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score
import seaborn as sns
plt.figure(figsize=(12, 10))
label_encoder = LabelEncoder()
scaler = StandardScaler()
df = pd.read_csv("./docs/base/MBA.csv")
#Excluir as conlunas não desejadas
df = df.drop(columns= ["application_id", "international"])
#Preencher os valores nulos da coluna "race"
df["race"] = df["race"].fillna("international")
#Preencher os valores nulos da coluna "admission"
df["admission"] = df["admission"].fillna("Refused")
#Label encoding da coluna em texto binária
df["gender"] = label_encoder.fit_transform(df["gender"])
#Escolonando as váriaveis continuas
df["gpa"] = scaler.fit_transform(df[["gpa"]])
df["gmat"] = scaler.fit_transform(df[["gmat"]])
df["work_exp"] = scaler.fit_transform(df[["work_exp"]])
#Gerando dummies das colunas em texto não binárias
df = pd.get_dummies(df,columns= ["race", "major", "work_industry"], drop_first=True)
#Separar em vairaveis indenpendetes e dependente
X = df[["gender", "gpa", "major", "race", "gmat", "work_exp", "work_industry"]]
y = label_encoder.fit_transform(df["admission"])
#Separar em teste e validação
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
Treinamento do Modelo
import base64
from io import BytesIO
import matplotlib.pyplot as plt
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.cluster import KMeans
from sklearn.preprocessing import LabelEncoder, StandardScaler
from sklearn.decomposition import PCA
# Carregar os dados
df = pd.read_csv("./docs/base/MBA.csv")
# Excluir as colunas não desejadas
df = df.drop(columns=["application_id", "international"])
# Preencher valores nulos
df["race"] = df["race"].fillna("international")
df["admission"] = df["admission"].fillna("Refused")
# Label encoding da coluna em texto binária
label_encoder = LabelEncoder()
df["gender"] = label_encoder.fit_transform(df["gender"])
# Escalonar variáveis contínuas
scaler = StandardScaler()
df[["gpa", "gmat", "work_exp"]] = scaler.fit_transform(df[["gpa", "gmat", "work_exp"]])
# Gerar dummies
df = pd.get_dummies(df, columns=["race", "major", "work_industry"], drop_first=True)
# Separar variáveis independentes e dependente
X = df.drop("admission", axis=1)
y = label_encoder.fit_transform(df["admission"])
#Separar em teste e validação
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# Reduzir para 2 dimensões com PCA
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X_train)
# Treinar KMeans
kmeans = KMeans(n_clusters=3, init="k-means++", max_iter=100, random_state=42)
labels = kmeans.fit_predict(X_pca)
# Plot
plt.figure(figsize=(12, 10))
plt.scatter(X_pca[:, 0], X_pca[:, 1], c=labels, cmap="viridis", s=50)
plt.scatter(kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1],
c="red", marker="*", s=200, label="Centroids")
plt.title("K-Means Clustering Results (PCA 2D)")
plt.xlabel("PCA Feature 1")
plt.ylabel("PCA Feature 2")
plt.legend()
# Salvar em buffer png
buffer = BytesIO()
plt.savefig(buffer, format="png", transparent=True, bbox_inches="tight")
buffer.seek(0)
# Converter em base64
img_base64 = base64.b64encode(buffer.read()).decode("utf-8")
# Criar tag HTML para embutir no MkDocs
html_img = f'<img src="data:image/png;base64,{img_base64}" alt="KMeans clustering" />'
print(html_img)
Avaliação
Acurácia: 84.14%
Matriz de Confusão:
| Classe Pred 0 | Classe Pred 1 | Classe Pred 2 | |
|---|---|---|---|
| Classe Real 0 | 0 | 704 | 0 |
| Classe Real 1 | 0 | 4169 | 0 |
| Classe Real 2 | 0 | 82 | 0 |
import pandas as pd
from sklearn.metrics import accuracy_score, confusion_matrix
import numpy as np
from scipy.stats import mode
from sklearn.model_selection import train_test_split
from sklearn.cluster import KMeans
from sklearn.preprocessing import LabelEncoder, StandardScaler
from sklearn.decomposition import PCA
# Carregar os dados
df = pd.read_csv("./docs/base/MBA.csv")
# Excluir as colunas não desejadas
df = df.drop(columns=["application_id", "international"])
# Preencher valores nulos
df["race"] = df["race"].fillna("international")
df["admission"] = df["admission"].fillna("Refused")
# Label encoding da coluna em texto binária
label_encoder = LabelEncoder()
df["gender"] = label_encoder.fit_transform(df["gender"])
# Escalonar variáveis contínuas
scaler = StandardScaler()
df[["gpa", "gmat", "work_exp"]] = scaler.fit_transform(df[["gpa", "gmat", "work_exp"]])
# Gerar dummies
df = pd.get_dummies(df, columns=["race", "major", "work_industry"], drop_first=True)
X = df.drop("admission", axis=1)
y = label_encoder.fit_transform(df["admission"])
#Separar em teste e validação
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# Reduzir para 2 dimensões com PCA
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X_train)
# Treinar KMeans
kmeans = KMeans(n_clusters=3, init="k-means++", max_iter=100, random_state=42)
labels = kmeans.fit_predict(X_pca)
# Mapear clusters para classes reais por voto majoritário
cluster_map = {}
for c in np.unique(labels):
mask = labels == c
majority_class = mode(y_train[mask], keepdims=False)[0]
cluster_map[c] = majority_class
# Reatribuir clusters como classes previstas
y_pred = np.array([cluster_map[c] for c in labels])
# Calcular acurácia e matriz de confusão
acc = accuracy_score(y_train, y_pred)
cm = confusion_matrix(y_train, y_pred)
cm_df = pd.DataFrame(
cm,
index=[f"Classe Real {cls}" for cls in np.unique(y_train)],
columns=[f"Classe Pred {cls}" for cls in np.unique(y_train)]
)
print(f"Acurácia: {acc*100:.2f}%")
print("<br>Matriz de Confusão:")
print(cm_df.to_html())
Análise
Apesar de atingir 84,14% de acurácia, o modelo aparenta ser inadequado para a base, pois, ainda que tenha encontrado 3 clusters, estes não correspondem as classificações desejadas, de maneira que, para todos os clusters, a maioria dos pontos possuem o rótulo de Refused, fazendo com que todas as predições sejam classificadas como Refused.