Skip to content

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.

2025-12-04T02:28:45.299441 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

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.

2025-12-04T02:28:45.381587 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

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.

2025-12-04T02:28:45.412558 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

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.

2025-12-04T02:28:45.449972 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

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.

2025-12-04T02:28:45.528536 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

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").

2025-12-04T02:28:45.569958 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

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.

2025-12-04T02:28:45.619144 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

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.

2025-12-04T02:28:45.710227 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

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.

2025-12-04T02:28:45.807966 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

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.

2025-12-04T02:28:45.917947 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

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

KMeans clustering

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.