Importante

Você está vendo a versão anterior da nova experiência da Alura que estamos preparando para você. Em breve, ela ganha uma identidade visual novinha totalmente pensada em potencializar seus estudos!

2
respostas

[Projeto] Mão na massa: construindo um classificador com o dataset Iris

Resultado de 100% é bem suspeito sempre, neste caso não significa necessariamente overfitting nem erro no código.

O Iris é um dataset muito pequeno 150 amostras, 50 de cada classe.
A classe Setosa é linearmente separável.
As classes Versicolor e Virginica possuem alguma mistura, mas pouca.
Algumas divisões treino/teste produzem 100%. usei seed 42 pode ter pegados amostras bem separadas
Talvez pegar amostra estratificada no treino e teste fosse mais generalista.

Na validação cruzada tivemos:
Média: 95%
Desvio padrão: 0.0521

# Passo 0: Importar as bibliotecas necessárias
from sklearn.tree import DecisionTreeClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from sklearn.preprocessing import StandardScaler
from sklearn.datasets import load_iris
import pandas as pd

# Passo 1: Carregar e explorar dados
iris_dataset = load_iris()

#converter o iris_dataset para dataframe do pandas 
#nota: data é o campo iris_dataset.data e as colunas do campo iris_dataset.feature_names
df_iris = pd.DataFrame(data=iris_dataset.data, columns=iris_dataset.feature_names)
df_iris['target'] = iris_dataset.target # target é o alvo ou classe 0,1,2 são 3 temos 150 amostras 50 de cada classe
df_iris['target_name'] = df_iris.target.apply(lambda x: iris_dataset.target_names[x])  #convertendo para nomes de classes para visualização
display(df_iris)

df_iris.shape

#estatisticas descritivas
df_iris.describe()

#descobrindo os tipos de dados
df_iris.info()

#Descobrindo quantas amostras de cada classe temos
df_iris.groupby('target_name').size()


df_iris.head()
# Passo 2: Pré-processar os dados e normalizar
#separando os astributos e os rótulos
X = df_iris.drop(columns=['target','target_name'])  # Características (comprimento e largura das pétalas e sépalas)
y = df_iris['target']  # Rótulos (espécies das flores)  lembrando que knn trabalho melhor com rótulos numéricos

#normalizando os dados
scaler = StandardScaler()  #Ao normalizar, retiramos um pouco da variação para melhorar o desempenho do algorítmo e acurácia no processo de treinamento.
X_scaled = scaler.fit_transform(X)
# Passo 3: Dividir os dados em treino e teste 80/20
X_train, X_test, y_train, y_test = train_test_split(X_scaled, y, test_size=0.2, random_state=42)

# Treinar
knn_model = KNeighborsClassifier()
knn_model.fit(X_train, y_train)
# Passo 4: Avaliar o modelo
knn_accuracy = accuracy_score(y_test, knn_model.predict(X_test))
print(f"Acurácia do KNN: {knn_accuracy * 100:.2f}%")
#predizer uma amostra nova
nova_amostra = [[5.1, 3.5, 1.4, 0.2]]  # Exemplo de uma nova amostra
nova_amostra_scaled = scaler.transform(nova_amostra)  # como escalei no treino na predição também precisa fazer

predicao = knn_model.predict(nova_amostra_scaled) # predizer com dados escalados

print("-" * 50)
print(f'Predição para a nova amostra: {df_iris.target_name[predicao[0]]}')
from sklearn.metrics import confusion_matrix

y_pred = knn_model.predict(X_test)

cm = confusion_matrix(y_test, y_pred)

print(cm)
from sklearn.metrics import classification_report

print(
    classification_report(
        y_test,
        y_pred,
        target_names=iris_dataset.target_names
    )
)
from sklearn.model_selection import cross_val_score

knn_model = KNeighborsClassifier()

scores = cross_val_score(
    knn_model,
    X_scaled,
    y,
    cv=10
)

print("Scores:")
print(scores)

print()
print(f"Média: {scores.mean():.4f}")
print(f"Desvio padrão: {scores.std():.4f}")
2 respostas

Olá, Marcelo. Como vai?

Excelente análise técnica! Você tocou no ponto mais importante ao avaliar modelos de Machine Learning: o ceticismo saudável. Ver uma acurácia de 100% em dados de teste deve sempre acender um alerta no cientista de dados, mas a sua leitura do cenário foi perfeita.

Como você bem pontuou, o dataset Iris é pequeno (150 amostras) e apresenta características muito bem definidas, com a classe Setosa sendo linearmente separável das demais. Ao utilizar o random_state=42 com uma divisão clássica de 80/20, as 30 amostras selecionadas para o conjunto de teste deram a sorte de cair em regiões sem sobreposição, resultando no 100% de acurácia.

A sua decisão de aplicar a Validação Cruzada (Cross-Validation) com 10 folds foi a atitude mais madura e correta para validar essa suspeita. A média de 95% com um desvio padrão de 0.0521 nos dá a real dimensão da capacidade de generalização do KNN para este problema.

Para agregar ainda mais valor ao seu código e responder à sua excelente intuição sobre a amostragem estratificada, quero compartilhar duas sugestões de boas práticas no scikit-learn:

1. Garantindo a Estratificação na Divisão Treino/Teste

Sua intuição estava certíssima! Quando trabalhamos com datasets pequenos, corremos o risco de que a divisão aleatória monte um conjunto de teste desbalanceado (por exemplo, contendo muitas amostras de Setosa e poucas de Virginica).

Para resolver isso, a função train_test_split possui um parâmetro sensacional chamado stratify. Ao passar as suas etiquetas y para ele, o scikit-learn garante que a proporção das classes (50/50/50) seja mantida perfeitamente tanto no treino quanto no teste.

Veja como atualizar a sua linha de divisão:

# O parâmetro stratify=y garante a distribuição idêntica das espécies nos dois conjuntos
X_train, X_test, y_train, y_test = train_test_split(
    X_scaled, 
    y, 
    test_size=0.2, 
    random_state=42, 
    stratify=y
)

2. O Perigo do Data Leakage (Vazamento de Dados) no Pré-processamento

Olhando o seu pipeline, notei um detalhe sutil na ordem dos passos que é uma armadilha clássica em Machine Learning: você aplicou o scaler.fit_transform(X) na base inteira antes de fazer a divisão entre treino e teste.

Quando fazemos o fit do StandardScaler na base completa, o escalador calcula a média e o desvio padrão usando informações que pertencem ao conjunto de teste. Isso significa que o seu modelo de treino recebeu, de forma indireta, informações sobre a distribuição dos dados de teste. Esse fenômeno é chamado de Data Leakage (Vazamento de Dados) e costuma inflar artificialmente os resultados de acurácia.

A boa prática correta é dividir os dados brutos primeiro e, em seguida, calibrar o escalador apenas com os dados de treino. Veja o fluxo ideal:

# 1. Primeiro dividimos os dados brutos (com estratificação)
X_train_raw, X_test_raw, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# 2. Inicializamos o escalador
scaler = StandardScaler()

# 3. O 'fit_transform' calcula e aplica a média/desvio APENAS do treino
X_train = scaler.fit_transform(X_train_raw)

# 4. No teste, usamos APENAS 'transform' para não vazar informações
X_test = scaler.transform(X_test_raw)

# 5. Agora treinamos o modelo com dados perfeitamente isolados
knn_model = KNeighborsClassifier()
knn_model.fit(X_train, y_train)

Aplicar esse isolamento rigoroso garante que a acurácia medida no teste seja um reflexo fiel de como o KNN se comportará no mundo real com dados totalmente inéditos.

O seu projeto está excelente, com uma exploração rica via Pandas e uso correto das matrizes de confusão e relatórios de classificação. Continue com essa postura analítica e rigorosa!

Espero que possa ter lhe ajudado!

Obrigado pelo feedback, Versão com melhorias aplicadas:

  • evitar data leakage com escaler nos lugares certos e pipeline
  • gráfico de espalhamento para vizualizar separação de classes
  • teste vizinhos mais próximos nenhu, 3 e 5 mas não mudou a acurácia deixei 5
  • coloquei amostra nova no dataframe para evitar um warning que esta dando por ter usado as colunas
  • Melhorias de apresentação, parece estar mais realista 93,33% de acurácia e k-fold com 96% mostando uma generalização maior
# Passo 0: Importar as bibliotecas necessárias
from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import (train_test_split, cross_val_score, StratifiedKFold )
from sklearn.metrics import ( accuracy_score, confusion_matrix, classification_report )
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.datasets import load_iris

import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns


# Passo 1: Carregar e explorar dados
iris_dataset = load_iris()

# converter o iris_dataset para dataframe do pandas
df_iris = pd.DataFrame( data=iris_dataset.data, columns=iris_dataset.feature_names )

# adicionar coluna target
df_iris['target'] = iris_dataset.target

# adicionar nome das classes
df_iris['target_name'] = df_iris.target.apply(lambda x: iris_dataset.target_names[x] )

display(df_iris)

print("\nShape:")
print(df_iris.shape)

print("\nEstatísticas Descritivas:")
display(df_iris.describe())

print("\nInformações do Dataset:")
df_iris.info()

print("\nQuantidade de Amostras por Classe:")
print(df_iris.groupby('target_name').size())

print("\nPrimeiras Linhas:")
display(df_iris.head())


# Passo 1.1: Melhoria gráfico para Visualizar a separação das classes
plt.figure(figsize=(8,6))

sns.scatterplot(
    data=df_iris,
    x='petal length (cm)',
    y='petal width (cm)',
    hue='target_name',
    s=100
)

plt.title("Distribuição das Classes - Iris")
plt.xlabel("Comprimento da Pétala")
plt.ylabel("Largura da Pétala")
plt.grid(True)
plt.show()

# Observação:
# Setosa fica completamente separada.
# Versicolor e Virginica possuem alguma sobreposição.

# Passo 2: Separar atributos e rótulos
X = df_iris.drop(columns=['target', 'target_name'])
y = df_iris['target']

# Passo 3: Dividir treino e teste 
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2,  random_state=42, stratify=y) # melhoria estratificado

# Passo 4: Normalizar os dados
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train) # calcula média e desvio apenas usando treino
X_test_scaled = scaler.transform(X_test) # aplica a mesma transformação no teste


# Passo 5: Treinar o modelo
knn_model = KNeighborsClassifier( n_neighbors=5) # melhoria adicionado numero de vizinho próximos
knn_model.fit( X_train_scaled, y_train )

# Passo 6: Avaliar o modelo
y_pred = knn_model.predict(X_test_scaled)
knn_accuracy = accuracy_score( y_test, y_pred )

print("-" * 50)
print(f"Acurácia do KNN: {knn_accuracy * 100:.2f}%")

# Passo 6.1: Matriz de Confusão
cm = confusion_matrix( y_test, y_pred )

print("\nMatriz de Confusão:")
print(cm)

plt.figure(figsize=(6,5))

sns.heatmap(
    cm,
    annot=True,
    cmap="Blues",
    fmt="d",
    xticklabels=iris_dataset.target_names,
    yticklabels=iris_dataset.target_names
)

plt.ylabel("Classe Real")
plt.xlabel("Classe Prevista")
plt.title("Matriz de Confusão")
plt.show()

# Passo 6.2: Relatório de Classificação
print("\nRelatório de Classificação:")

print(
    classification_report(
        y_test,
        y_pred,
        target_names=iris_dataset.target_names
    )
)


# Passo 7: Predizer uma nova amostra
nova_amostra = pd.DataFrame([[5.1, 3.5, 1.4, 0.2]],  columns=X.columns )
#nova_amostra = pd.DataFrame([[6.5,	3.0,	5.2,	2.0]],  columns=X.columns )	

# aplicar mesma normalização usada no treino
nova_amostra_scaled = scaler.transform( nova_amostra )
predicao = knn_model.predict(nova_amostra_scaled)

print("-" * 50)
print(f"Predição para a nova amostra: "
      f"{iris_dataset.target_names[predicao[0]]}"
     )

# Passo 8: Validação Cruzada Estratificada
# aqui pra evitar o leakage coloquei em um pipeline que já faz escala nas amostras
pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('knn', KNeighborsClassifier(n_neighbors=5))  #mesma vizinhança selecionada antes
])

cv = StratifiedKFold( n_splits=10, shuffle=True, random_state=42 )

scores = cross_val_score( pipeline, X, y, cv=cv, scoring='accuracy' )

print("-" * 50)
print("Validação Cruzada Estratificada")

print(f"Scores: {scores}")
print(f"Média: {scores.mean()*100:.2f}%")
print(f"Desvio padrão: {scores.std():.2f}")
print(f"Acurácia Média: {scores.mean()*100:.2f}%")