K-Nearest Neighbors
Algoritmo KNN
O algoritmo K-Nearest Neighbors (KNN) é uma técnica de aprendizado supervisionado usada para classificação e regressão. Ele funciona comparando uma nova amostra com as mais próximas no conjunto de treino, de acordo com uma medida de distância, geralmente a euclidiana. Na etapa de classificação, a classe atribuída é definida pela maioria entre os k vizinhos mais próximos.
Cars Purchase Decision
Este projeto tem como objetivo aplicar técnicas de Machine Learning para compreender os fatores que influenciam a decisão de compra de automóveis. A partir de um conjunto de dados com informações sobre idade, gênero e salário anual dos clientes, foi construída uma árvore de decisão capaz de classificar se um indivíduo provavelmente realizará a compra ou não.
Exploração dos Dados
Estatísticas Descritivas
Para o projeto foi utilizado o dataset Cars - Purchase Decision Dataset e contém detalhes de clientes que consideraram comprar um automóvel, juntamente com seus salários.
O conjunto de dados contém 1000 registros e 5 variáveis. A variável alvo é Purchased (0 = não comprou, 1 = comprou). Entre as variáveis explicativas, temos Gender (categórica), Age (numérica) e AnnualSalary (numérica).
Variáveis
-
User ID: Código do Cliente
-
Gender: Gênero do Cliente
-
Age: Idade do Cliente em anos
-
AnnualSalary: Salário anual do Cliente
-
Purchased: Se o cliente realizou a compra
Estatísticas Descritivas e Visualizações
O gráfico mostra a relação entre idade e salário dos clientes, destacando quem realizou a compra e quem não comprou:
import pandas as pd
import matplotlib.pyplot as plt
from io import BytesIO
# Carregar dataset
url = "https://raw.githubusercontent.com/EnzoMalagoli/machine-learning/refs/heads/main/data/car_data.csv"
df = pd.read_csv(url)
# --- ETAPA 1: Data Cleaning
df["Age"].fillna(df["Age"].median(), inplace=True)
df["Gender"].fillna(df["Gender"].mode()[0], inplace=True)
df["AnnualSalary"].fillna(df["AnnualSalary"].median(), inplace=True)
# --- ETAPA 2: Encoding
df["Gender"] = df["Gender"].map({"Male": 1, "Female": 0})
# --- ETAPA 3: Normalização
for col in ["Age", "AnnualSalary"]:
cmin, cmax = df[col].min(), df[col].max()
df[col] = 0.0 if cmax == cmin else (df[col] - cmin) / (cmax - cmin)
df0 = df[df["Purchased"] == 0]
df1 = df[df["Purchased"] == 1]
# --- PLOT: Dispersão Idade x Salário ---
fig, ax = plt.subplots(1, 1, figsize=(7, 5))
ax.scatter(
df0["Age"], df0["AnnualSalary"],
label="Não comprou (0)", alpha=0.4,
color="lightcoral", edgecolor="darkred", linewidth=0.8
)
ax.scatter(
df1["Age"], df1["AnnualSalary"],
label="Comprou (1)", alpha=0.4,
color="skyblue", edgecolor="navy", linewidth=0.8
)
ax.set_title("Idade x Salário por Decisão de Compra")
ax.set_xlabel("Idade")
ax.set_ylabel("Salário Anual")
ax.grid(linestyle="--", alpha=0.6)
ax.legend()
buffer = BytesIO()
plt.savefig(buffer, format="svg", bbox_inches="tight")
buffer.seek(0)
print(buffer.getvalue().decode("utf-8"))
Info
A visualização deixa claro que idade e salário exercem influência relevante no comportamento de compra
O próximo gráfico apresenta a distribuição de clientes por gênero:
import pandas as pd
import matplotlib.pyplot as plt
from io import BytesIO
# Carregar dataset
url = "https://raw.githubusercontent.com/EnzoMalagoli/machine-learning/refs/heads/main/data/car_data.csv"
df = pd.read_csv(url)
# --- ETAPA 1: Data Cleaning
df["Gender"].fillna(df["Gender"].mode()[0], inplace=True)
counts = df["Gender"].value_counts()
# --- PLOT: Distribuição por Gênero ---
fig, ax = plt.subplots(1, 1, figsize=(6, 4))
ax.bar(
counts.index, counts.values,
color=["pink", "skyblue"], edgecolor="lightcoral"
)
ax.set_title("Distribuição por Gênero")
ax.set_xlabel("Gênero")
ax.set_ylabel("Quantidade")
ax.grid(axis="y", linestyle="--", alpha=0.6)
buffer = BytesIO()
plt.savefig(buffer, format="svg", bbox_inches="tight")
buffer.seek(0)
print(buffer.getvalue().decode("utf-8"))
Info
Observa-se que há uma leve predominância de mulheres no dataset.
O último gráfico apresenta a distribuição do salário anual dos clientes, permitindo visualizar a mediana, a dispersão dos valores e a presença de possíveis extremos:
import pandas as pd
import matplotlib.pyplot as plt
from io import BytesIO
# Carregar dataset
url = "https://raw.githubusercontent.com/EnzoMalagoli/machine-learning/refs/heads/main/data/car_data.csv"
df = pd.read_csv(url)
# --- ETAPA 1: Data Cleaning
df["AnnualSalary"].fillna(df["AnnualSalary"].median(), inplace=True)
# --- PLOT: Boxplot
fig, ax = plt.subplots(figsize=(7, 5))
bp = ax.boxplot(df["AnnualSalary"], patch_artist=True, widths=0.5)
for box in bp["boxes"]:
box.set(facecolor="skyblue", edgecolor="navy", linewidth=1.2)
for whisker in bp["whiskers"]:
whisker.set(color="navy", linewidth=1.2)
for cap in bp["caps"]:
cap.set(color="navy", linewidth=1.2)
for median in bp["medians"]:
median.set(color="darkred", linewidth=1.5)
ax.set_title("Distribuição do Salário Anual")
ax.set_ylabel("Salário Anual")
ax.set_xticks([])
ax.grid(axis="y", linestyle="--", alpha=0.6)
buffer = BytesIO()
plt.savefig(buffer, format="svg", bbox_inches="tight")
buffer.seek(0)
print(buffer.getvalue().decode("utf-8"))
Info
O gráfico evidencia que a maior parte dos salários está concentrada em uma faixa intermediária, entre aproximadamente 50 mil e 90 mil, com a mediana em torno de 70 mil.
Pré-processamento
Pré-processamento de dados brutos deve ser a primeira etapa ao lidar com datasets de todos tamanhos.
Data Cleaning
O processo de data cleaning garante que o conjunto utilizado seja confiável e esteja livre de falhas que possam distorcer os resultados. Consiste em identificar e corrigir problemas como valores ausentes, dados inconsistentes ou informações que não fazem sentido. Essa limpeza permite que a base seja mais fiel à realidade e forneça condições adequadas para a construção de modelos de Machine Learning.
No código, a limpeza foi feita dessa forma: possíveis valores vazios em idade, gênero e salário foram preenchidos com informações representativas, como a mediana ou o valor mais frequente.
Gender | Age | AnnualSalary |
---|---|---|
Male | 59 | 135500 |
Male | 55 | 39000 |
Female | 46 | 135500 |
Female | 47 | 42500 |
Male | 32 | 77500 |
Female | 53 | 90500 |
Male | 25 | 59500 |
Female | 36 | 63000 |
Female | 41 | 67500 |
Male | 41 | 73500 |
import pandas as pd
def preprocess(df):
df['Age'].fillna(df['Age'].median(), inplace=True)
df['Gender'].fillna(df['Gender'].mode()[0], inplace=True)
df['AnnualSalary'].fillna(df['AnnualSalary'].median(), inplace=True)
features = ['Gender', 'Age', 'AnnualSalary']
return df[features]
df = pd.read_csv('https://raw.githubusercontent.com/EnzoMalagoli/machine-learning/refs/heads/main/data/car_data.csv')
df = df.sample(n=10, random_state=42)
df = preprocess(df)
print(df.sample(n=10).to_markdown(index=False))
Encoding Categorical Variables
O processo de encoding de variáveis categóricas transforma informações em formato de texto em valores numéricos, permitindo que algoritmos de Machine Learning consigam utilizá-las em seus cálculos.
No código, o encoding foi aplicado à variável gênero, convertendo as categorias “Male” e “Female” em valores numéricos (1 e 0). Dessa forma, a base de dados mantém todas as colunas originais, mas agora com a variável categórica representada de maneira adequada para ser usada em algoritmos de classificação.
User ID | Gender | Age | AnnualSalary | Purchased |
---|---|---|---|---|
176 | 1 | 41 | 73500 | 0 |
448 | 1 | 59 | 135500 | 1 |
391 | 1 | 25 | 59500 | 0 |
623 | 0 | 47 | 42500 | 1 |
773 | 0 | 46 | 135500 | 0 |
413 | 0 | 53 | 90500 | 1 |
793 | 1 | 55 | 39000 | 1 |
836 | 0 | 36 | 63000 | 0 |
586 | 0 | 41 | 67500 | 0 |
651 | 1 | 32 | 77500 | 0 |
import pandas as pd
def preprocess(df):
# Limpeza
df['Age'].fillna(df['Age'].median(), inplace=True)
df['Gender'].fillna(df['Gender'].mode()[0], inplace=True)
df['AnnualSalary'].fillna(df['AnnualSalary'].median(), inplace=True)
# Encoding simples para Gender
df['Gender'] = df['Gender'].map({'Male': 1, 'Female': 0})
return df
df = pd.read_csv('https://raw.githubusercontent.com/EnzoMalagoli/machine-learning/refs/heads/main/data/car_data.csv')
df = df.sample(n=10, random_state=42)
df = preprocess(df)
print(df.to_markdown(index=False))
Normalização
A normalização é o processo de reescalar os valores numéricos de forma que fiquem dentro de um intervalo fixo, normalmente entre 0 e 1. Isso facilita a comparação entre variáveis que possuem unidades ou magnitudes diferentes, evitando que atributos com valores muito altos dominem a análise.
No código, a normalização foi aplicada às colunas idade e salário anual, transformando seus valores para a faixa de 0 a 1 por meio do método Min-Max Scaling. Dessa forma, ambas as variáveis passam a estar na mesma escala, tornando o conjunto de dados mais consistente e adequado para a modelagem.
User ID | Gender | Age | AnnualSalary | Purchased |
---|---|---|---|---|
870 | Female | 0.511111 | 0.730909 | 1 |
761 | Female | 0.755556 | 0.545455 | 1 |
876 | Male | 0.666667 | 0.214545 | 0 |
136 | Female | 0.8 | 0.149091 | 1 |
211 | Male | 0.466667 | 0.345455 | 0 |
139 | Male | 0.955556 | 0.207273 | 1 |
994 | Female | 0.444444 | 0.963636 | 1 |
783 | Male | 0.577778 | 0.432727 | 0 |
942 | Female | 0.666667 | 0.68 | 1 |
918 | Female | 0.355556 | 0.105455 | 0 |
import pandas as pd
from sklearn.preprocessing import MinMaxScaler
# Carregar dataset
url = "https://raw.githubusercontent.com/EnzoMalagoli/machine-learning/refs/heads/main/data/car_data.csv"
df = pd.read_csv(url)
# Selecionar colunas numéricas para normalizar
features_to_normalize = ['Age', 'AnnualSalary']
# Inicializar o scaler
scaler = MinMaxScaler()
# Aplicar normalização e substituir no DataFrame
df[features_to_normalize] = scaler.fit_transform(df[features_to_normalize])
# Mostrar amostra dos dados normalizados
print(df.sample(10).to_markdown(index=False))
Divisão dos Dados
Após o pré-processamento, o conjunto de dados precisa ser separado em duas partes: uma para treinamento e outra para teste. Essa divisão é fundamental para que o modelo de Machine Learning aprenda padrões a partir de um grupo de exemplos e, depois, seja avaliado em dados que ainda não foram vistos. Dessa forma, é possível medir a capacidade de generalização do modelo e evitar que ele apenas memorize os exemplos fornecidos.
No código, os atributos escolhidos como preditores foram gênero, idade e salário anual, enquanto a variável-alvo foi Purchased, que indica se o cliente comprou ou não o produto. A divisão foi feita em 70% para treino e 30% para teste, garantindo que a proporção de clientes que compraram e não compraram fosse preservada em ambos os subconjuntos.
Tamanho treino: 700 Tamanho teste: 300
import pandas as pd
from sklearn.model_selection import train_test_split
# Carregar dataset
url = "https://raw.githubusercontent.com/EnzoMalagoli/machine-learning/refs/heads/main/data/car_data.csv"
df = pd.read_csv(url)
# --- Data Cleaning
df["Age"].fillna(df["Age"].median(), inplace=True)
df["Gender"].fillna(df["Gender"].mode()[0], inplace=True)
df["AnnualSalary"].fillna(df["AnnualSalary"].median(), inplace=True)
# --- Encoding
df["Gender"] = df["Gender"].map({"Male": 1, "Female": 0})
# --- Normalização
for col in ["Age", "AnnualSalary"]:
cmin, cmax = df[col].min(), df[col].max()
df[col] = 0.0 if cmax == cmin else (df[col] - cmin) / (cmax - cmin)
# --- Separar variáveis preditoras
X = df[["Gender", "Age", "AnnualSalary"]]
y = df["Purchased"]
# --- Divisão em 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
)
print("Tamanho treino:", X_train.shape[0])
print("Tamanho teste:", X_test.shape[0])
Implementação KNN
From Scratch
Accuracy: 0.91
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
# ===================== KNN (seu classificador) =====================
class KNNClassifier:
def __init__(self, k=3):
self.k = k
def fit(self, X, y):
self.X_train = X
self.y_train = y
def predict(self, X):
return np.array([self._predict(x) for x in X])
def _predict(self, x):
# Distância Euclidiana
distances = np.sqrt(((self.X_train - x) ** 2).sum(axis=1))
# k vizinhos mais próximos
k_idx = np.argsort(distances)[:self.k]
k_labels = self.y_train[k_idx]
# Classe mais comum
vals, counts = np.unique(k_labels, return_counts=True)
return vals[np.argmax(counts)]
# ===================== Pré-processamento (3 etapas) =====================
def preprocess(df):
# 1) Data cleaning
df["Age"].fillna(df["Age"].median(), inplace=True)
df["Gender"].fillna(df["Gender"].mode()[0], inplace=True)
df["AnnualSalary"].fillna(df["AnnualSalary"].median(), inplace=True)
# 2) Encoding (Gender -> 0/1)
df["Gender"] = df["Gender"].map({"Female": 0, "Male": 1})
# 3) Normalização Min–Max (Age, AnnualSalary)
for col in ["Age", "AnnualSalary"]:
cmin, cmax = df[col].min(), df[col].max()
df[col] = 0.0 if cmax == cmin else (df[col] - cmin) / (cmax - cmin)
# Features finais e alvo
X = df[["Gender", "Age", "AnnualSalary"]].to_numpy(dtype=float)
y = df["Purchased"].to_numpy(dtype=int)
return X, y
# Carregar dados e preparar
url = "https://raw.githubusercontent.com/EnzoMalagoli/machine-learning/refs/heads/main/data/car_data.csv"
df = pd.read_csv(url)
X, y = preprocess(df)
# Split treino/teste
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.30, random_state=42, stratify=y
)
# Treino e avaliação (KNN)
knn = KNNClassifier(k=5) # ajuste k se quiser
knn.fit(X_train, y_train)
y_pred = knn.predict(X_test)
acc = accuracy_score(y_test, y_pred)
print(f"Accuracy: {acc:.2f}")
Usando Scikit-Learn
Accuracy: 0.91
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
# PREPROCESS
def preprocess(df):
df["Age"].fillna(df["Age"].median(), inplace=True)
df["Gender"].fillna(df["Gender"].mode()[0], inplace=True)
df["AnnualSalary"].fillna(df["AnnualSalary"].median(), inplace=True)
enc = LabelEncoder()
df["Gender"] = enc.fit_transform(df["Gender"])
for col in ["Age", "AnnualSalary"]:
cmin, cmax = df[col].min(), df[col].max()
df[col] = 0.0 if cmax == cmin else (df[col] - cmin) / (cmax - cmin)
X = df[["Gender", "Age", "AnnualSalary"]].to_numpy(dtype=float)
y = df["Purchased"].to_numpy(dtype=int)
return X, y
# LOAD DATA
url = "https://raw.githubusercontent.com/EnzoMalagoli/machine-learning/refs/heads/main/data/car_data.csv"
df = pd.read_csv(url)
X, y = preprocess(df)
# SPLIT
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y
)
# TRAIN KNN
knn = KNeighborsClassifier(n_neighbors=5, metric="minkowski", p=2) # k=5, distância Euclidiana
knn.fit(X_train, y_train)
# PREDICT & EVALUATE
y_pred = knn.predict(X_test)
print(f"Accuracy: {accuracy_score(y_test, y_pred):.2f}")
Accuracy: 0.90
A etapa de classificação foi conduzida utilizando o algoritmo K-Nearest Neighbors (KNN) em duas abordagens distintas. Primeiramente, foi implementado um classificador do zero, no qual as etapas de pré-processamento (limpeza, encoding e normalização) foram aplicadas antes da divisão em treino e teste. Esse modelo segue a lógica central do KNN: calcular distâncias entre os pontos, selecionar os vizinhos mais próximos e atribuir a classe mais frequente. Apesar de simples, essa versão manual ajudou a compreender os mecanismos internos do algoritmo.
Em seguida, a mesma tarefa foi realizada com o KNeighborsClassifier da biblioteca Scikit-Learn, que oferece uma implementação mais robusta e otimizada. A acurácia obtida em ambas as versões foi semelhante, demonstrando que o pré-processamento foi eficaz e que o modelo conseguiu identificar padrões relevantes nos dados. Além disso, a visualização da fronteira de decisão mostrou de forma clara como o KNN separa os clientes que compraram dos que não compraram, destacando sua capacidade de criar limites não lineares adaptados à distribuição real das variáveis de idade e salário.