Skip to content

Projeto 1

Grupo 3 / Os Goats do SI

  1. José Longo Neto
  2. Pedro Almeida Maricate
  3. Martim Ponzio
  4. Pablo Dimitrof
  5. Enzo Malagoli
  6. Eduardo Gul

Introdução

O objetivo deste projeto é aplicar três algoritmos de Machine LearningÁrvore de Decisão, K-Nearest Neighbors (KNN) e K-Means (Clustering) — sobre a base de dados do kagle.
A base contém aproximadamente 130 mil registros de vinhos, com informações como país de origem, pontuação atribuída por especialistas, preço, tipo de uva, região produtora e descrição sensorial.

A proposta é explorar o uso de algoritmos de aprendizado supervisionado e não supervisionado para compreender padrões entre as variáveis e identificar possíveis relações entre características como origem, variedade e pontuação dos vinhos.

O roteiro segue a mesma estrutura aplicada em outros projetos da disciplina, dividindo o processo em etapas sequenciais e documentadas:

  • Exploração dos Dados (EDA): análise geral das variáveis, distribuição de valores e identificação de possíveis inconsistências;
  • Pré-processamento: limpeza, padronização e codificação de dados categóricos;
  • Divisão dos Dados: separação em conjuntos de treino e teste, quando houver variável-alvo definida;
  • Treinamento e Avaliação: aplicação e comparação dos modelos supervisionados (Árvore de Decisão e KNN);
  • Modelagem Não Supervisionada: uso do K-Means para identificar agrupamentos entre os vinhos sem utilizar o rótulo de pontuação;
  • Relatório Final e Comparação: discussão sobre os resultados obtidos e limitações de cada abordagem.

Observação:
O dataset não possui uma coluna explicitamente binária de qualidade, mas contém a variável points, que representa a nota do vinho atribuída por avaliadores.
Para fins de classificação, esta variável será transformada em uma variável-alvo derivada, categorizando os vinhos conforme sua pontuação (por exemplo, alta pontuação ≥ 90).
Assim, os modelos supervisionados trabalharão com essa classificação, enquanto o K-Means será utilizado para detectar padrões de agrupamento sem rótulos.

Exploração dos Dados

A etapa de Exploração dos Dados (EDA) tem como objetivo compreender a estrutura, o conteúdo e o significado das variáveis presentes na base wine.csv.
Essa análise inicial permite identificar padrões, outliers, distribuições e possíveis problemas de qualidade dos dados, como valores ausentes ou inconsistências.
As visualizações e descrições abaixo ajudam a construir uma visão geral do conjunto e a orientar as etapas seguintes de pré-processamento e modelagem.

A coluna country indica o país de origem do vinho. Essa variável é importante para observar a distribuição geográfica dos registros e entender a representatividade de cada país no conjunto.

2025-10-28T15:18:58.819361 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

A coluna designation informa a denominação ou nome específico do vinho, dentro da vinícola. É uma variável categórica de alta cardinalidade (muitos valores únicos) e pode indicar edições especiais ou lotes de produção.

2025-10-28T15:18:59.627953 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

A coluna points representa a pontuação do vinho atribuída por avaliadores especializados, geralmente variando entre 80 e 100 pontos. Essa variável é central no projeto, pois será usada para derivar a variável-alvo de qualidade que alimentará os modelos supervisionados.

2025-10-28T15:19:00.429319 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

A coluna price indica o preço do vinho em dólares. Ela é uma variável numérica contínua que pode apresentar assimetria devido à presença de vinhos muito caros (outliers). A relação entre preço e pontuação será uma das análises mais relevantes desta etapa.

2025-10-28T15:19:01.222979 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

A coluna province indica a província ou região produtora do vinho dentro de seu país. É uma variável categórica útil para observar a diversidade geográfica da produção.

2025-10-28T15:19:02.006819 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

A coluna region_1 representa uma sub-região produtora (como “Napa Valley” ou “Bordeaux”). Pode ser usada para análises mais detalhadas de terroir e diferenciação regional.

2025-10-28T15:19:02.801543 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

A coluna region_2 fornece informações complementares sobre uma segunda subdivisão geográfica, quando disponível. Nem todos os registros possuem este campo preenchido, portanto ele pode apresentar alta taxa de valores ausentes.

2025-10-28T15:19:03.589439 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

A coluna taster_name contém o nome do avaliador responsável pela nota e descrição do vinho. Ela permite explorar a distribuição de avaliações entre diferentes especialistas e identificar potenciais vieses.

2025-10-28T15:19:04.393425 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

A coluna variety indica o tipo de uva utilizada na produção (por exemplo: Pinot Noir, Chardonnay, Riesling). É uma das variáveis mais importantes do conjunto, pois reflete o perfil sensorial e o tipo do vinho.

2025-10-28T15:19:05.200550 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

A coluna winery identifica a vinícola responsável pela produção. É uma variável categórica de alta cardinalidade e pode ser explorada futuramente em análises de desempenho médio por produtor.

2025-10-28T15:19:05.975190 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

Pré-processamento

Após a exploração inicial, aplicamos um conjunto de procedimentos para preparar a base wine.csv para modelagem:

  • Remoção de colunas irrelevantes: descartamos campos sem utilidade direta para o modelo ou que serão usados apenas como metadados na documentação (ex.: Unnamed: 0, title, description, taster_twitter_handle).
  • Conversão e limpeza de tipos: garantimos que price e points estejam em formato numérico; tratamos valores ausentes com imputações simples (mediana para numéricas; rótulo "Unknown" para categóricas).
  • Criação do alvo (quando aplicável): a partir de points, derivamos a variável quality_high (1 se points ≥ 90; 0 caso contrário). Para evitar vazamento de informação, points não será usada como feature nos modelos supervisionados.
  • Codificação de categóricas: para esta versão “base preparada” usada na documentação, aplicamos Label Encoding coluna a coluna (adequado para árvore; nas seções de treino do KNN/K-Means faremos scaling e codificações apropriadas nos pipelines).
  • Entrega de uma visão pronta para modelagem: apresentamos uma amostra da base já limpa e codificada, com quality_high disponível para as etapas supervisionadas e os demais campos prontos para uso em pipelines.

Dimensão (após pré-processamento): 129971 linhas x 10 colunas

country designation price province region_1 region_2 taster_name variety winery quality_high
2 35112 5 24 1027 15 8 125 952 0
15 29390 12 309 377 15 15 492 2718 0
37 12139 9 262 909 15 12 656 7019 0
40 23298 29 51 799 1 18 80 11074 0
40 37256 40 51 36 15 18 440 8338 0
40 35112 22 51 1013 0 7 125 10956 1
40 13155 33 268 1218 17 14 440 7730 1
22 30032 35 197 448 15 9 125 9739 0
40 35112 10 51 185 1 18 80 5345 0
15 7395 21 299 366 15 15 492 6439 1
40 37134 38 51 964 1 11 560 5574 0
15 29438 14 195 977 15 15 137 2639 0
15 7424 18 195 1210 15 15 137 3338 1
40 35112 20 51 911 12 18 703 11688 0
31 29965 19 7 1094 15 15 450 2411 1
22 5095 25 341 161 15 9 2 11693 0
40 9886 20 51 157 0 18 691 6891 0
17 10175 18 232 1094 15 1 479 1277 0
22 36799 20 341 855 15 18 459 586 0
40 8376 48 412 872 3 16 362 10668 1
import pandas as pd
import numpy as np
from sklearn.preprocessing import LabelEncoder

# ===== 1) Ler base =====
df = pd.read_csv("./src/wine.csv")

# ===== 2) Remover colunas irrelevantes (se existirem) =====
drop_cols = ["Unnamed: 0", "title", "description", "taster_twitter_handle"]
df = df.drop(columns=[c for c in drop_cols if c in df.columns], errors="ignore")

# ===== 3) Garantir tipos numéricos e tratar nulos =====
if "points" in df.columns:
    df["points"] = pd.to_numeric(df["points"], errors="coerce")
if "price" in df.columns:
    df["price"] = pd.to_numeric(df["price"], errors="coerce")

num_cols = df.select_dtypes(include=["number"]).columns.tolist()
cat_cols = df.select_dtypes(exclude=["number"]).columns.tolist()

for c in num_cols:
    med = df[c].median() if df[c].notna().any() else 0
    df[c] = df[c].fillna(med)

for c in cat_cols:
    df[c] = df[c].fillna("Unknown")

# ===== 4) Criar alvo supervisionado (se houver points) =====
if "points" in df.columns:
    df["quality_high"] = (df["points"] >= 90).astype(int)

# ===== 5) Codificação simples de categóricas (rastreável) =====
df_ready = df.copy()
for c in df_ready.select_dtypes(exclude=["number"]).columns:
    le = LabelEncoder()
    df_ready[c] = le.fit_transform(df_ready[c].astype(str))

# ===== 6) Evitar vazamento: retirar 'points' das features "base" =====
if "points" in df_ready.columns:
    df_ready = df_ready.drop(columns=["points"])

# ===== 7) Mostrar SOMENTE algumas linhas para não poluir o markdown =====
n_show = min(20, len(df_ready))
print(f"Dimensão (após pré-processamento): {df_ready.shape[0]} linhas x {df_ready.shape[1]} colunas\n")
print(df_ready.sample(n=n_show, random_state=42).to_markdown(index=False))

Dimensão (base original): 129971 linhas x 11 colunas

Unnamed: 0 country designation points price province region_1 region_2 taster_name variety winery
77718 Australia nan 83 5 Australia Other South Eastern Australia nan Joe Czerwinski Chardonnay Banrock Station
67681 France Réserve 85 12 Rhône Valley Côtes du Rhône nan Roger Voss Rosé Cellier des Dauphins
69877 Spain Estate Grown & Bottled 86 9 Northern Spain Rueda nan Michael Schachner Verdejo-Viura Esperanza
46544 US Nebula 87 29 California Paso Robles Central Coast nan Cabernet Sauvignon Midnight
186 US Wiley Vineyard 88 40 California Anderson Valley nan nan Pinot Noir Harrington
73126 US nan 90 22 California Sonoma County-Monterey County-Santa Barbara County California Other Jim Gordon Chardonnay Meiomi
26800 US Five Faces 90 33 Oregon Willamette Valley Willamette Valley Paul Gregutt Pinot Noir Fullerton
80832 Italy Saten 89 35 Lombardy Franciacorta nan Kerin O’Keefe Chardonnay Lantieri de Paratico
86297 US nan 84 10 California Central Coast Central Coast nan Cabernet Sauvignon Cupcake
56015 France Clos du Château 92 21 Provence Côtes de Provence nan Roger Voss Rosé Domaine du Clos Gautier
78536 US White Hawk Vineyard 89 38 California Santa Barbara County Central Coast Matt Kettmann Syrah Deep Sea
41016 France Réserve des Vignerons 86 14 Loire Valley Saumur nan Roger Voss Chenin Blanc Cave de Saumur
81019 France Clos le Vigneau 90 18 Loire Valley Vouvray nan Roger Voss Chenin Blanc Château Gaudrelle
129762 US nan 87 20 California Russian River Valley Sonoma nan Zinfandel Novy
34634 Portugal Santos da Casa Reserva 91 19 Alentejano nan nan Roger Voss Portuguese Red Casca Wines
20284 Italy Caleno Oro 88 nan Southern Italy Campania nan Kerin O’Keefe Aglianico Nugnes
111397 US Dessert Wine 86 20 California California California Other nan White Blend Elkhorn Peak
26153 Germany Dom Off-Dry 89 18 Mosel nan nan Anna Lee C. Iijima Riesling Bischöfliche Weingüter Trier
110148 Italy Vriccio 86 20 Southern Italy Puglia nan nan Primitivo Antica Enotria
125453 US Crazy Mary 93 48 Washington Red Mountain Columbia Valley Sean P. Sullivan Mourvèdre Mark Ryan

Divisão dos Dados

Com a base pré-processada, realizou-se a divisão entre conjuntos de treinamento e teste.
O objetivo dessa etapa é garantir que o modelo seja avaliado em dados que ele nunca viu durante o treinamento, permitindo uma medida mais confiável de sua capacidade de generalização.

Foi utilizada a função train_test_split da biblioteca scikit-learn, com os seguintes critérios:
- 70% dos dados destinados ao treinamento, para que o modelo aprenda os padrões da base;
- 30% dos dados destinados ao teste, para avaliar o desempenho em novos exemplos;
- Estratificação pelo alvo (quality_high), garantindo que a proporção entre classes seja mantida em ambos os conjuntos;
- Random State fixado, assegurando reprodutibilidade na divisão.

import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder

# ===== 1) Ler base =====
df = pd.read_csv("./src/wine.csv")

# ===== 2) Remover colunas irrelevantes (se existirem) =====
drop_cols = ["Unnamed: 0", "title", "description", "taster_twitter_handle"]
df = df.drop(columns=[c for c in drop_cols if c in df.columns], errors="ignore")

# ===== 3) Conversões de tipo essenciais =====
if "points" in df.columns:
    df["points"] = pd.to_numeric(df["points"], errors="coerce")
if "price" in df.columns:
    df["price"] = pd.to_numeric(df["price"], errors="coerce")

# ===== 4) Criar alvo supervisionado a partir de 'points' =====
# quality_high = 1 (>= 90), 0 (< 90)
# Linhas sem 'points' não ajudam na classificação — removemos
df = df[df["points"].notna()].copy()
df["quality_high"] = (df["points"] >= 90).astype(int)

# ===== 5) Codificar variáveis categóricas (LabelEncoder simples) =====
cat_cols = [
    col for col in ["country", "province", "region_1", "region_2",
                    "taster_name", "designation", "variety", "winery"]
    if col in df.columns
]

for c in cat_cols:
    le = LabelEncoder()
    df[c] = df[c].fillna("Unknown").astype(str)
    df[c] = le.fit_transform(df[c])

# ===== 6) Definir X (features) e y (alvo), evitando vazamento de 'points' =====
feature_cols = []
feature_cols += [c for c in cat_cols]              # categóricas codificadas
if "price" in df.columns:
    feature_cols.append("price")                   # numérica útil
# NÃO usar 'points' como feature (origem do alvo)
X = df[feature_cols].copy()
y = df["quality_high"].copy()

# ===== 7) Divisão treino/teste com estratificação =====
X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.30,
    random_state=27,
    stratify=y
)

Treinamento do Modelo — Árvore de Decisão

Nesta etapa treinamos um DecisionTreeClassifier utilizando a base pré-processada.
Mantemos a mesma preparação usada na divisão (remoção de colunas irrelevantes, criação de quality_high a partir de points, codificação das categóricas) e realizamos o ajuste do modelo com 70/30 de treino/teste e random_state=27.

Precisão da Validação: 0.75
Importância das Features:

Feature Importância
price 0.343989
winery 0.205696
designation 0.169186
variety 0.091451
region_1 0.082287
taster_name 0.034639
province 0.034284
country 0.020755
region_2 0.017713
2025-10-28T15:19:09.320593 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn import tree
from sklearn.metrics import accuracy_score
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from io import StringIO
from sklearn.preprocessing import LabelEncoder
import numpy as np

# ===================== Ler base =====================
df = pd.read_csv("./src/wine.csv")

# ===================== Excluir colunas não desejadas =====================
# (mesma ideia do seu script antigo: tirar campos irrelevantes/verbosos)
drop_cols = ["Unnamed: 0", "title", "description", "taster_twitter_handle"]
df = df.drop(columns=[c for c in drop_cols if c in df.columns], errors="ignore")

# ===================== Conversões básicas =====================
# 'points' será usada para criar o alvo; por isso NÃO entra em X
df["points"] = pd.to_numeric(df["points"], errors="coerce")

# price pode ter nulos; converte e imputa mediana
if "price" in df.columns:
    df["price"] = pd.to_numeric(df["price"], errors="coerce")
    if df["price"].isna().any():
        df["price"] = df["price"].fillna(df["price"].median())

# manter somente linhas com 'points' (necessário para o alvo)
df = df[df["points"].notna()].copy()

# ===================== Criar alvo supervisionado =====================
# quality_high = 1 (>= 90), 0 (< 90)
df["quality_high"] = (df["points"] >= 90).astype(int)

# ===================== Label encoding das categóricas =====================
# (seguindo o espírito do seu antigo: LabelEncoder direto nas colunas de texto)
cat_cols = [c for c in ["country", "province", "region_1", "region_2",
                        "taster_name", "designation", "variety", "winery"]
            if c in df.columns]

for c in cat_cols:
    le = LabelEncoder()
    df[c] = df[c].fillna("Unknown").astype(str)
    df[c] = le.fit_transform(df[c])

# ===================== Definição de features (X) e alvo (y) =====================
# Evitar vazamento: NUNCA usar 'points' em X, pois dela deriva o alvo
feature_cols = []
feature_cols += cat_cols
if "price" in df.columns:
    feature_cols.append("price")

x = df[feature_cols].copy()
y = df["quality_high"].copy()

# ===================== Divisão treino/teste =====================
x_train, x_test, y_train, y_test = train_test_split(
    x, y, test_size=0.3, random_state=27, stratify=y
)

# ===================== Criar e treinar o modelo de árvore de decisão =====================
classifier = tree.DecisionTreeClassifier(random_state=27)
classifier.fit(x_train, y_train)

# ===================== Avaliar o modelo =====================
y_pred = classifier.predict(x_test)
accuracy = accuracy_score(y_test, y_pred)
print(f"Precisão da Validação: {accuracy:.2f}")

# Importância das features
feature_importance = pd.DataFrame({
    'Feature': classifier.feature_names_in_,
    'Importância': classifier.feature_importances_
}).sort_values(by='Importância', ascending=False)

print("<br>Importância das Features:")
print(feature_importance.to_html(index=False))

# ===================== Plot da árvore (até profundidade 5, como no seu padrão) =====================
plt.figure(figsize=(20, 10))
try:
    tree.plot_tree(classifier, max_depth=5, fontsize=10, feature_names=classifier.feature_names_in_)
except Exception:
    tree.plot_tree(classifier, max_depth=5, fontsize=10)

# Para imprimir na página HTML (MkDocs)
buffer = StringIO()
plt.savefig(buffer, format="svg", transparent=True)
print(buffer.getvalue())

Avaliação do Modelo — Árvore de Decisão

Após o treinamento, a árvore foi avaliada no conjunto de teste (30%), garantindo que as métricas reflitam a capacidade de generalização do modelo. As saídas principais consideradas são:

  • Acurácia (teste): proporção de acertos sobre o total de amostras de teste;
  • Matriz de Confusão: distribuição de acertos/erros por classe (0 = qualidade comum, 1 = alta qualidade), útil para inspecionar erros assimétricos;
  • Classification Report: métricas por classe (precision, recall e F1), evidenciando se o modelo está sacrificando uma classe em detrimento da outra;
  • Importância das Features: ranking de variáveis mais influentes na decisão (ex.: variety, province/country, price), auxiliando na interpretação.

Observações e interpretação

  • Desbalanceamento: é comum a classe “alta qualidade” (≥ 90 pontos) ser minoritária, o que pode reduzir o recall dessa classe mesmo com acurácia global razoável.
  • Overfitting: árvores muito profundas tendem a memorizar o treino. Se a diferença entre desempenho de treino e teste for alta, recomenda-se restringir max_depth, min_samples_leaf e/ou min_samples_split.
  • Variáveis proxy: atributos geográficos (country, province, region_*) e variety podem capturar padrões de estilo/qualidade; price costuma aparecer relevante, mas atenção a correlações espúrias e viés de seleção.
  • Sem vazamento: a variável points foi usada apenas para gerar o alvo (quality_high) e não entrou como feature nas previsões.

Treinamento do Modelo - KNN

Nesta seção foi implementado o algoritmo KNN de forma manual, a partir do zero, para consolidar o entendimento do funcionamento do método.
A implementação considera a distância euclidiana entre os pontos, identifica os vizinhos mais próximos e atribui a classe com maior frequência.
Esse exercício é importante para compreender a lógica por trás do KNN antes de utilizar bibliotecas prontas.

Acurácia (KNN manual, k=5): 0.716 Tamanho treino/teste: 3500 / 1500

Acurácia (KNN manual, k=5): 0.716 Tamanho treino/teste: 3500 / 1500

Usando Scikit-Learn

Aqui repetimos a preparação e treinamos o KNeighborsClassifier do scikit-learn. Para fins de visualização, usamos PCA (2D) apenas para projetar os dados e exibir a fronteira de decisão do KNN no plano, junto de um gráfico de dispersão dos pontos de treino.

Acurácia (KNN sklearn c/ PCA 2D, k=5): 0.709 2025-10-28T15:19:47.446237 image/svg+xml Matplotlib v3.10.7, https://matplotlib.org/

import numpy as np
import pandas as pd

import matplotlib
matplotlib.use("Agg")  # backend headless para rodar no mkdocs
import matplotlib.pyplot as plt

from io import StringIO
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score

# 1) Carregar base
df = pd.read_csv("./src/wine.csv")

# 2) Limpeza básica e alvo
drop_cols = ["Unnamed: 0", "title", "description", "taster_twitter_handle"]
df = df.drop(columns=[c for c in drop_cols if c in df.columns], errors="ignore")

df["points"] = pd.to_numeric(df.get("points"), errors="coerce")
if "price" in df.columns:
    df["price"] = pd.to_numeric(df["price"], errors="coerce")

df = df[df["points"].notna()].copy()
df["quality_high"] = (df["points"] >= 90).astype(int)

# 3) Amostragem estratificada cedo (antes de dummies) para visualização
MAX_VIS = 4000
if len(df) > MAX_VIS:
    frac = MAX_VIS / len(df)
    df = (
        df.groupby("quality_high", group_keys=False)
          .apply(lambda g: g.sample(frac=frac, random_state=42))
          .reset_index(drop=True)
    )

y = df["quality_high"].values

# 4) Features (evitar cardinalidade absurda)
num_cols = [c for c in ["price"] if c in df.columns]
cat_cols = [c for c in ["country", "province", "region_1", "variety"] if c in df.columns]

for c in num_cols:
    df[c] = df[c].fillna(df[c].median())
for c in cat_cols:
    df[c] = df[c].fillna("Unknown").astype(str).str.strip()

X_cat = pd.get_dummies(df[cat_cols], drop_first=False, dtype=int) if cat_cols else pd.DataFrame(index=df.index)
X_num = df[num_cols].copy() if num_cols else pd.DataFrame(index=df.index)

if not X_num.empty:
    scaler = StandardScaler()
    X_num[num_cols] = scaler.fit_transform(X_num[num_cols])

X = pd.concat([X_num, X_cat], axis=1).values

# 5) Split (70/30), manter consistência
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.30, random_state=27, stratify=y
)

# 6) PCA (2D) apenas para visualização/fronteira
pca = PCA(n_components=2, random_state=27)
X_train_2d = pca.fit_transform(X_train)
X_test_2d  = pca.transform(X_test)

# 7) KNN no espaço 2D projetado
knn = KNeighborsClassifier(n_neighbors=5)
knn.fit(X_train_2d, y_train)
pred = knn.predict(X_test_2d)
acc = accuracy_score(y_test, pred)
print(f"Acurácia (KNN sklearn c/ PCA 2D, k=5): {acc:.3f}")

# 8) Fronteira de decisão em 2D (grid menos denso pra não travar)
plt.figure(figsize=(8, 6))
h = 0.25

x_min, x_max = X_train_2d[:, 0].min() - 1, X_train_2d[:, 0].max() + 1
y_min, y_max = X_train_2d[:, 1].min() - 1, X_train_2d[:, 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()]).reshape(xx.shape)

plt.contourf(xx, yy, Z, alpha=0.3)
plt.scatter(X_train_2d[:, 0], X_train_2d[:, 1], c=y_train, s=16, edgecolors="k", alpha=0.8)

plt.xlabel("PCA 1")
plt.ylabel("PCA 2")
plt.title("KNN — Fronteira de decisão (PCA 2D)")
plt.tight_layout()

buf = StringIO()
plt.savefig(buf, format="svg", transparent=True)
print(buf.getvalue())
plt.close()

Avaliação do Modelo - KNN

Após o treinamento, avaliamos a acurácia em teste nas duas abordagens (manual e com scikit-learn). Como o alvo é quality_high (derivado de points), o desempenho pode ser afetado por desbalanceamento (vinhos com nota ≥ 90 tendem a ser minoria). Além disso, o KNN é sensível à escala e à escolha de k, o que pode gerar variação de resultados. A visualização em PCA 2D tende a mostrar sobreposição entre classes, reforçando a dificuldade de separação perfeita nesse domínio.

Treinamento do Modelo - K-Means

O modelo K-Means foi treinado com 3 clusters como referência pedagógica para segmentar perfis de vinhos (ex.: baixa/média/alta qualidade percebida).
Após o pré-processamento (padronização de variáveis numéricas e one-hot para categóricas selecionadas), aplicamos o K-Means e projetamos os dados em PCA (2D) para visualização dos grupos e de seus centróides.

KMeans clustering (Wine)

import base64
from io import BytesIO
import numpy as np
import pandas as pd

import matplotlib
matplotlib.use("Agg")  # backend headless p/ rodar no mkdocs
import matplotlib.pyplot as plt

from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA

# 1) Carregar base
df = pd.read_csv("./src/wine.csv")

# 2) Remover campos verbosos/irrelevantes (se existirem)
drop_cols = ["Unnamed: 0", "title", "description", "taster_twitter_handle"]
df = df.drop(columns=[c for c in drop_cols if c in df.columns], errors="ignore")

# 3) Conversões e imputações mínimas
df["points"] = pd.to_numeric(df.get("points"), errors="coerce")
if "price" in df.columns:
    df["price"] = pd.to_numeric(df["price"], errors="coerce")
    df["price"] = df["price"].fillna(df["price"].median())

# 4) AMOSTRAGEM cedo (para não pesar no build)
MAX_ROWS = 5000
if len(df) > MAX_ROWS:
    df = df.sample(MAX_ROWS, random_state=27).reset_index(drop=True)

# 5) Seleção de features (evitar cardinalidade extrema)
num_cols = [c for c in ["price"] if c in df.columns]
cat_cols = [c for c in ["country", "province", "region_1", "variety"] if c in df.columns]  # sem taster_name

for c in cat_cols:
    df[c] = df[c].fillna("Unknown").astype(str).str.strip()

X_cat = pd.get_dummies(df[cat_cols], drop_first=False, dtype=int) if cat_cols else pd.DataFrame(index=df.index)
X_num = df[num_cols].copy() if num_cols else pd.DataFrame(index=df.index)

if not X_num.empty:
    scaler = StandardScaler()
    X_num[num_cols] = scaler.fit_transform(X_num[num_cols])

X = pd.concat([X_num, X_cat], axis=1).values

# 6) PCA 2D para visualização
pca = PCA(n_components=2, random_state=27)
X_pca = pca.fit_transform(X)

# 7) K-Means no espaço 2D (k=3)
kmeans = KMeans(n_clusters=3, init="k-means++", max_iter=300, n_init=10, random_state=27)
labels = kmeans.fit_predict(X_pca)

# 8) Plot (matplotlib puro) e saída em base64 (HTML <img>)
plt.figure(figsize=(8, 6))
plt.scatter(X_pca[:, 0], X_pca[:, 1], c=labels, s=18, alpha=0.85)
plt.scatter(
    kmeans.cluster_centers_[:, 0], kmeans.cluster_centers_[:, 1],
    c="red", marker="*", s=200, label="Centróides"
)
plt.title("K-Means (Wine) — PCA 2D, k=3")
plt.xlabel("PCA 1")
plt.ylabel("PCA 2")
plt.legend(loc="best")

buf = BytesIO()
plt.savefig(buf, format="png", transparent=True, bbox_inches="tight")
plt.close()
buf.seek(0)
img_b64 = base64.b64encode(buf.read()).decode("utf-8")
print(f'<img src="data:image/png;base64,{img_b64}" alt="KMeans clustering (Wine)"/>')

Avaliação do Modelo

Para avaliar o clustering, mapeamos os clusters para o rótulo derivado quality_high (1 se points ≥ 90, 0 caso contrário) por voto majoritário em cada grupo. Assim, obtemos métricas de classificação mesmo em um cenário originalmente não supervisionado, incluindo acurácia e matriz de confusão.

Acurácia (mapeamento por voto, treino): 62.90%
Matriz de Confusão (treino):

Classe Pred 0 Classe Pred 1
Classe Real 0 2614 1
Classe Real 1 1557 28

import numpy as np
import pandas as pd
from sklearn.metrics import accuracy_score, confusion_matrix
from sklearn.model_selection import train_test_split
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA

# 1) Carregar base
df = pd.read_csv("./src/wine.csv")

# 2) Limpeza e alvo derivado
drop_cols = ["Unnamed: 0", "title", "description", "taster_twitter_handle"]
df = df.drop(columns=[c for c in drop_cols if c in df.columns], errors="ignore")

df["points"] = pd.to_numeric(df.get("points"), errors="coerce")
df = df[df["points"].notna()].copy()  # necessário para quality_high

if "price" in df.columns:
    df["price"] = pd.to_numeric(df["price"], errors="coerce")
    df["price"] = df["price"].fillna(df["price"].median())

df["quality_high"] = (df["points"] >= 90).astype(int)
y = df["quality_high"].values

# 3) AMOSTRAGEM estratificada cedo (para não pesar no build)
MAX_ROWS = 6000
if len(df) > MAX_ROWS:
    frac = MAX_ROWS / len(df)
    df = (
        df.groupby("quality_high", group_keys=False)
          .apply(lambda g: g.sample(frac=frac, random_state=27))
          .reset_index(drop=True)
    )
y = df["quality_high"].values

# 4) Features (evitar cardinalidade extrema)
num_cols = [c for c in ["price"] if c in df.columns]
cat_cols = [c for c in ["country", "province", "region_1", "variety"] if c in df.columns]  # sem taster_name

for c in cat_cols:
    df[c] = df[c].fillna("Unknown").astype(str).str.strip()

X_cat = pd.get_dummies(df[cat_cols], drop_first=False, dtype=int) if cat_cols else pd.DataFrame(index=df.index)
X_num = df[num_cols].copy() if num_cols else pd.DataFrame(index=df.index)

if not X_num.empty:
    scaler = StandardScaler()
    X_num[num_cols] = scaler.fit_transform(X_num[num_cols])

X = pd.concat([X_num, X_cat], axis=1).values

# 5) Split (70/30) + PCA no treino
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.30, random_state=27, stratify=y
)

pca = PCA(n_components=2, random_state=27)
X_train_pca = pca.fit_transform(X_train)

# 6) K-Means em treino (k=3) e mapeamento cluster->classe por voto majoritário
kmeans = KMeans(n_clusters=3, init="k-means++", max_iter=300, n_init=10, random_state=27)
labels_train = kmeans.fit_predict(X_train_pca)

cluster_map = {}
classes = np.unique(y_train)
for c in np.unique(labels_train):
    mask = labels_train == c
    if mask.sum() == 0:
        cluster_map[c] = classes[0]
        continue
    counts = np.bincount(y_train[mask], minlength=classes.max() + 1)
    cluster_map[c] = counts.argmax()

# 7) Acurácia e matriz de confusão (TREINO), como no seu exemplo
y_pred_train = np.array([cluster_map[c] for c in labels_train])
acc = accuracy_score(y_train, y_pred_train)
cm = confusion_matrix(y_train, y_pred_train, labels=np.sort(classes))

cm_df = pd.DataFrame(
    cm,
    index=[f"Classe Real {cls}" for cls in np.sort(classes)],
    columns=[f"Classe Pred {cls}" for cls in np.sort(classes)]
)

print(f"Acurácia (mapeamento por voto, treino): {acc*100:.2f}%")
print("<br>Matriz de Confusão (treino):")
print(cm_df.to_html(index=True))

Conclusão Geral do Projeto

O projeto teve como objetivo aplicar e comparar três abordagens clássicas de Machine LearningÁrvore de Decisão, KNN (K-Nearest Neighbors) e K-Means — sobre o dataset kagle de vinhos, que contém informações como país, região, variedade, provador, preço e pontuação (variável points), entre outras.
A proposta foi compreender como diferentes técnicas de aprendizado supervisionado e não supervisionado lidam com o problema de prever ou agrupar vinhos de alta qualidade (definidos como aqueles com pontuação ≥ 90).


Sobre a Base de Dados

A base apresenta alta cardinalidade categórica (muitos valores distintos em colunas como variety e region_1) e distribuição desigual entre países e faixas de pontuação.
Por esse motivo, foi necessário aplicar pré-processamentos cuidadosos, como: - normalização das variáveis numéricas (price); - one-hot encoding para variáveis categóricas; - e amostragem estratificada para reduzir o custo computacional mantendo representatividade.

Essa estrutura favoreceu a análise, mas também revelou as limitações naturais do conjunto — com poucos atributos diretamente relacionados à qualidade sensorial do vinho, as previsões tendem a capturar mais o perfil geral do mercado do que a avaliação crítica de especialistas.


Árvore de Decisão

O modelo de Árvore de Decisão apresentou uma acurácia de 75%, sendo o melhor desempenho entre os métodos supervisionados testados.
A análise das importâncias das variáveis mostrou que: - o preço é o principal preditor (forte correlação com a pontuação),
- seguido por winery e designation, que representam o produtor e o rótulo do vinho.

O modelo conseguiu estruturar regras compreensíveis — como faixas de preço associadas à qualidade —, reforçando o caráter interpretável das árvores.
Ainda assim, parte do resultado pode refletir overfitting leve, já que a árvore aprende padrões muito específicos de produtores e regiões.


KNN (K-Nearest Neighbors)

O KNN manual alcançou ≈71,6% de acurácia, enquanto a versão com Scikit-Learn e projeção via PCA (2D) obteve cerca de 70,8%.
O gráfico de fronteira de decisão mostrou uma sobreposição significativa entre classes, especialmente nas regiões de média qualidade.
Isso reforça o fato de que os atributos disponíveis não separam de forma nítida os vinhos de alta e baixa pontuação.

Apesar disso, o KNN apresentou comportamento consistente e intuitivo: - bons resultados para amostras com padrões similares de região e variedade; - sensibilidade a escala e densidade de vizinhança, exigindo scaling e escolha adequada de k.


K-Means (Clustering)

O K-Means, aplicado com 3 clusters, buscou agrupar vinhos de maneira não supervisionada, representando diferentes perfis de qualidade.
Os grupos formados mostraram tendência de separação, mas com fronteiras difusas, o que é esperado dado que os dados não possuem rótulos explícitos de classes distintas.
O mapeamento por voto majoritário atingiu ≈62,9% de acurácia, com confusão entre vinhos medianos e de alta pontuação.

O resultado indica que, embora o K-Means consiga identificar padrões estruturais (como faixas de preço e origem), ele não captura com precisão o conceito subjetivo de “qualidade” — que depende de fatores sensoriais não representados na base.


Considerações Finais

Comparando os modelos:

Modelo Tipo de Aprendizado Acurácia Observações
Árvore de Decisão Supervisionado 0.75 Melhor desempenho; interpretável; sensível a overfitting
KNN Supervisionado ~0.71 Bom em padrões locais; alta sobreposição entre classes
K-Means Não supervisionado ~0.63 Agrupamento coerente, mas difuso; útil para segmentação exploratória

Em síntese, o projeto mostrou que: - modelos supervisionados (Árvore e KNN) se beneficiam do conhecimento prévio das classes e alcançam resultados mais robustos;
- o K-Means, embora menos preciso, é útil para análises exploratórias e descoberta de padrões; - e que a qualidade do dado é tão ou mais determinante do que o algoritmo escolhido — atributos objetivos como preço e origem são insuficientes para explicar completamente uma variável subjetiva como “pontuação de degustação”.

Assim, o estudo reforça a importância do pré-processamento, seleção de variáveis e análise crítica dos resultados em qualquer projeto de Machine Learning, especialmente em domínios complexos como o enológico, onde os dados quantitativos capturam apenas parte da realidade avaliada por especialistas.


Reflexão Final

O desenvolvimento deste projeto proporcionou uma visão prática do ciclo completo de Machine Learning: desde a limpeza e transformação de dados até a avaliação e comparação de modelos.
Ficou evidente que compreender o contexto do problema e a natureza dos dados é essencial para interpretar resultados e extrair conclusões relevantes.
Mais do que alcançar a maior acurácia possível, o aprendizado principal foi entender como cada modelo oferece uma lente diferente sobre os mesmos dados, revelando tanto suas potencialidades quanto suas limitações.