Curso
Você vai implementar KNN no famoso conjunto de dados Iris.
Observação: vale a pena fazer o curso Machine Learning with Python ou, para entender o contexto da evolução do ML e muito mais, ler este post.
Introdução
Machine learning surgiu da ciência da computação e estuda principalmente o design de algoritmos que aprendem com a experiência. Para aprender, eles precisam de dados com certos atributos, a partir dos quais os algoritmos buscam padrões preditivos relevantes. De forma geral, as tarefas de ML podem ser categorizadas em aprendizagem de conceitos, clusterização, modelagem preditiva etc. O objetivo final dos algoritmos de ML é tomar decisões corretas sem intervenção humana. Prever preços de ações ou o clima são alguns exemplos de aplicação.
Existem vários algoritmos de machine learning, como árvores de decisão, Naive Bayes, random forest, support vector machine, K-nearest neighbor, k-means clustering, entre outros.
Do conjunto de algoritmos de ML, o que você vai usar hoje é o k-nearest neighbor.
Agora, o que exatamente é o algoritmo K-Nearest Neighbor? Vamos descobrir!
O que é o k-nearest neighbor?
O KNN, ou k-nearest neighbor, é um algoritmo de aprendizagem supervisionada — “supervisionada” porque ele usa os rótulos de classe dos dados de treinamento durante a fase de aprendizagem. É um algoritmo baseado em instâncias: novos pontos são classificados com base em instâncias rotuladas já armazenadas. KNN pode ser usado tanto para classificação quanto para regressão; porém, é mais comum em tarefas de classificação.
O k no KNN é uma variável crucial, também chamada de hiperparâmetro, que ajuda a classificar um ponto de dados com precisão. Em termos práticos, k é o número de vizinhos mais próximos dos quais você quer “pegar o voto” ao classificar um novo ponto.

Perceba que, à medida que o valor de k aumenta de 1 para 7, a fronteira de decisão entre duas classes se torna mais suave.
Como essa “mágica” acontece? Como todo novo ponto é classificado com base nos pontos armazenados?
Vamos entender rapidamente, passo a passo:
- Primeiro, você carrega todos os dados e define o valor de k;
- Depois, calcula a distância entre os pontos armazenados e o novo ponto que deseja classificar, usando métricas como distância Manhattan (L1), distância Euclidiana (L2), similaridade do cosseno, distância Bhattacharyya, distância Chebyshev, etc.;
- Em seguida, ordena os valores de distância em ordem crescente ou decrescente e determina os k vizinhos mais próximos (maiores ou menores, conforme a métrica);
- Coleta os rótulos desses k vizinhos e usa uma votação majoritária ou ponderada para classificar o novo ponto. O rótulo atribuído é o da classe com maior pontuação;
- Por fim, retorna a classe prevista para a nova instância.
A previsão pode ser de dois tipos: classificação, na qual é atribuído um rótulo de classe, ou regressão, na qual é atribuído um valor numérico. Diferente da classificação, na regressão costuma-se atribuir ao novo ponto a média dos valores dos k vizinhos.
Desvantagens do KNN: primeiro, a complexidade de buscar os vizinhos mais próximos para cada novo ponto. Segundo, escolher o valor de k pode ser trabalhoso. Por fim, nem sempre é claro qual métrica de distância usar.
Chega de teoria, certo? Vamos carregar, analisar e entender os dados que você vai usar neste tutorial.
Carregando os dados do Iris
O conjunto Iris tem 150 amostras de três classes: Iris-Setosa, Iris-Versicolor e Iris-Virginica. Quatro atributos usados para identificar cada uma das três classes são sepal-length, sepal-width, petal-length e petal-width.
Fique à vontade para usar outro dataset público ou um conjunto de dados próprio.
Sklearn é uma biblioteca de machine learning em Python amplamente usada em tarefas de data science. Ela oferece diversos algoritmos de classificação, regressão e clusterização, incluindo support vector machines, random forests, gradient boosting, k-means, KNN etc. Dentro do sklearn há o módulo datasets com vários conjuntos prontos para uso, incluindo o Iris. É bem intuitivo. Vamos carregar o iris.
from sklearn.datasets import load_iris
O load_iris traz os dados e os rótulos de classe de cada amostra. Vamos extrair tudo rapidamente.
data = load_iris().data
A variável data será um array numpy de shape (150,4), com 150 amostras e quatro atributos cada. Cada classe tem 50 amostras.
data.shape
(150, 4)
Vamos extrair os rótulos de classe.
labels = load_iris().target
labels.shape
(150,)
Agora você vai combinar dados e rótulos usando a excelente biblioteca NumPy. O NumPy oferece suporte a arrays e matrizes multidimensionais, além de uma coleção extensa de funções matemáticas de alto nível para operar nesses arrays. Vamos importar:
import numpy as np
Como data é um array 2D, você também precisa reshapar labels para 2D.
labels = np.reshape(labels,(150,1))
Agora use a função concatenate do numpy, com axis=-1, para concatenar pela segunda dimensão.
data = np.concatenate([data,labels],axis=-1)
data.shape
(150, 5)
Em seguida, importe a biblioteca de análise de dados pandas, útil para organizar dados em formato tabular e fazer operações e manipulações. Em especial, ela oferece estruturas de dados e operações para trabalhar com tabelas numéricas e séries temporais.
Neste tutorial, você vai usar bastante o pandas.
import pandas as pd
names = ['sepal-length', 'sepal-width', 'petal-length', 'petal-width', 'species']
dataset = pd.DataFrame(data,columns=names)
Pronto: o data frame dataset agora tem os dados e os rótulos de classe que você precisa!
Antes de avançar, lembre que a variável labels tem rótulos numéricos. Vamos convertê-los para os nomes das flores (espécies).
Para isso, selecione apenas a coluna class e substitua os três valores numéricos pelas espécies correspondentes. Use inplace=True para modificar o data frame dataset diretamente.
dataset['species'].replace(0, 'Iris-setosa',inplace=True)
dataset['species'].replace(1, 'Iris-versicolor',inplace=True)
dataset['species'].replace(2, 'Iris-virginica',inplace=True)
Vamos imprimir as cinco primeiras linhas de dataset para ver o resultado:
dataset.head(5)
| sepal-length | sepal-width | petal-length | petal-width | species | |
|---|---|---|---|---|---|
| 0 | 5.1 | 3.5 | 1.4 | 0.2 | Iris-setosa |
| 1 | 4.9 | 3.0 | 1.4 | 0.2 | Iris-setosa |
| 2 | 4.7 | 3.2 | 1.3 | 0.2 | Iris-setosa |
| 3 | 4.6 | 3.1 | 1.5 | 0.2 | Iris-setosa |
| 4 | 5.0 | 3.6 | 1.4 | 0.2 | Iris-setosa |
Analise seus dados
Vamos ver como as três flores ficam quando visualizadas e quão diferentes são entre si — não só em números, mas também na prática!
(Fonte)Agora, vamos visualizar os dados carregados acima com um scatterplot para entender quanto uma variável influencia a outra — ou seja, qual é a correlação entre elas.
Você vai usar a biblioteca matplotlib para criar o scatterplot.
import matplotlib.pyplot as plt
Dica: quer aprender diferentes formas de visualizar dados em Python? Então confira o curso Introduction to data visualization with matplotlib.
plt.figure(4, figsize=(10, 8))
plt.scatter(data[:50, 0], data[:50, 1], c='r', label='Iris-setosa')
plt.scatter(data[50:100, 0], data[50:100, 1], c='g',label='Iris-versicolor')
plt.scatter(data[100:, 0], data[100:, 1], c='b',label='Iris-virginica')
plt.xlabel('Sepal length',fontsize=20)
plt.ylabel('Sepal width',fontsize=20)
plt.xticks(fontsize=20)
plt.yticks(fontsize=20)
plt.title('Sepal length vs. Sepal width',fontsize=20)
plt.legend(prop={'size': 18})
plt.show()

Pelo gráfico acima, fica claro que há alta correlação para as flores Iris setosa em relação ao comprimento e à largura da sépala. Por outro lado, há menor correlação entre Iris versicolor e Iris virginica. Os pontos de versicolor e virginica estão mais espalhados, enquanto os de setosa são mais densos.
Vamos também plotar o gráfico para petal-length e petal-width.
plt.figure(4, figsize=(8, 8))
plt.scatter(data[:50, 2], data[:50, 3], c='r', label='Iris-setosa')
plt.scatter(data[50:100, 2], data[50:100, 3], c='g',label='Iris-versicolor')
plt.scatter(data[100:, 2], data[100:, 3], c='b',label='Iris-virginica')
plt.xlabel('Petal length',fontsize=15)
plt.ylabel('Petal width',fontsize=15)
plt.xticks(fontsize=15)
plt.yticks(fontsize=15)
plt.title('Petal length vs. Petal width',fontsize=15)
plt.legend(prop={'size': 20})
plt.show()

Também para petal-length e petal-width, o gráfico indica uma forte correlação para as flores setosa, que ficam densamente agrupadas.
Para reforçar como petal-length e petal-width se correlacionam, vamos plotar uma matriz de correlação.
dataset.iloc[:,2:].corr()
| petal-length | petal-width | |
|---|---|---|
| petal-length | 1.000000 | 0.962865 |
| petal-width | 0.962865 | 1.000000 |
A tabela acima mostra uma forte correlação de 0.96 entre petal-length e petal-width quando combinamos as três espécies.
Vamos analisar a correlação separadamente por espécie.
dataset.iloc[:50,:].corr() #setosa
| sepal-length | sepal-width | petal-length | petal-width | |
|---|---|---|---|---|
| sepal-length | 1.000000 | 0.742547 | 0.267176 | 0.278098 |
| sepal-width | 0.742547 | 1.000000 | 0.177700 | 0.232752 |
| petal-length | 0.267176 | 0.177700 | 1.000000 | 0.331630 |
| petal-width | 0.278098 | 0.232752 | 0.331630 | 1.000000 |
dataset.iloc[50:100,:].corr() #versicolor
| sepal-length | sepal-width | petal-length | petal-width | |
|---|---|---|---|---|
| sepal-length | 1.000000 | 0.525911 | 0.754049 | 0.546461 |
| sepal-width | 0.525911 | 1.000000 | 0.560522 | 0.663999 |
| petal-length | 0.754049 | 0.560522 | 1.000000 | 0.786668 |
| petal-width | 0.546461 | 0.663999 | 0.786668 | 1.000000 |
dataset.iloc[100:,:].corr() #virginica
| sepal-length | sepal-width | petal-length | petal-width | |
|---|---|---|---|---|
| sepal-length | 1.000000 | 0.457228 | 0.864225 | 0.281108 |
| sepal-width | 0.457228 | 1.000000 | 0.401045 | 0.537728 |
| petal-length | 0.864225 | 0.401045 | 1.000000 | 0.322108 |
| petal-width | 0.281108 | 0.537728 | 0.322108 | 1.000000 |
Pelas três tabelas, fica claro que a correlação entre petal-length e petal-width é de 0.33 para setosa e 0.32 para virginica, enquanto para versicolor é 0.78.
Agora, vamos visualizar a distribuição das features com histogramas:
fig = plt.figure(figsize = (8,8))
ax = fig.gca()
dataset.hist(ax=ax)
plt.show()

petal-length, petal-width e sepal-length mostram distribuição unimodal, enquanto sepal-width se aproxima de uma distribuição Gaussiana. Tudo isso ajuda a escolher um algoritmo que funcione bem com essas distribuições.
Em seguida, vamos analisar se os quatro atributos estão na mesma escala — algo essencial em ML. O data frame do pandas tem a função describe, que retorna count, mean, max, min etc. em formato tabular.
dataset.describe()
| sepal-length | sepal-width | petal-length | petal-width | |
|---|---|---|---|---|
| count | 150.000000 | 150.000000 | 150.000000 | 150.000000 |
| mean | 5.843333 | 3.057333 | 3.758000 | 1.199333 |
| std | 0.828066 | 0.435866 | 1.765298 | 0.762238 |
| min | 4.300000 | 2.000000 | 1.000000 | 0.100000 |
| 25% | 5.100000 | 2.800000 | 1.600000 | 0.300000 |
| 50% | 5.800000 | 3.000000 | 4.350000 | 1.300000 |
| 75% | 6.400000 | 3.300000 | 5.100000 | 1.800000 |
| max | 7.900000 | 4.400000 | 6.900000 | 2.500000 |
Perceba que os quatro atributos têm escala semelhante entre 0 e 8, em centímetros. Se quiser, você ainda pode reescalá-los para o intervalo entre 0 e 1.
Embora saibamos que há 50 amostras por classe (cerca de 33,3% do total), vamos confirmar:
print(dataset.groupby('species').size())
species
Iris-setosa 50
Iris-versicolor 50
Iris-virginica 50
dtype: int64
Pré-processando seus dados
Depois de carregar e analisar bem os dados, é hora de prepará-los para alimentar seu modelo de ML. Nesta etapa, você vai normalizar os dados e dividir em conjuntos de treino e teste.
Normalizando seus dados
Existem duas formas comuns de normalização:
- Normalização por exemplo, em que você normaliza cada amostra individualmente;
- Normalização por feature, em que você normaliza cada atributo de forma consistente em todas as amostras.
Por que e quando normalizar? E é necessário padronizar o Iris?
De modo geral, quase sempre é uma boa prática. Normalizar coloca tudo na mesma escala e faixa, o que é crucial quando os dados não são consistentes. Você pode checar inconsistências com describe(), que mostra max e min. Se uma feature tem faixas muito maiores que outra, normalizar ambas é importante.
Imagine X com faixa maior e Y com faixa menor: a influência de Y pode ser ofuscada pela de X. Nesses casos, normalize X e Y.
No Iris, a normalização não é necessária.
Vamos imprimir novamente describe() para ver por que não precisamos normalizar.
dataset.describe()
| sepal-length | sepal-width | petal-length | petal-width | |
|---|---|---|---|---|
| count | 150.000000 | 150.000000 | 150.000000 | 150.000000 |
| mean | 5.843333 | 3.057333 | 3.758000 | 1.199333 |
| std | 0.828066 | 0.435866 | 1.765298 | 0.762238 |
| min | 4.300000 | 2.000000 | 1.000000 | 0.100000 |
| 25% | 5.100000 | 2.800000 | 1.600000 | 0.300000 |
| 50% | 5.800000 | 3.000000 | 4.350000 | 1.300000 |
| 75% | 6.400000 | 3.300000 | 5.100000 | 1.800000 |
| max | 7.900000 | 4.400000 | 6.900000 | 2.500000 |
O atributo sepal-length varia de 4,3 a 7,9; sepal-width, de 2 a 4,4; petal-length, de 1 a 6,9; e petal-width, de 0,1 a 2,5. Todas as features ficam entre 0,1 e 7,9 — aceitável. Logo, não é necessário normalizar o Iris.
Dividindo os dados
Este é outro ponto importante em ML: seu objetivo é treinar um modelo capaz de tomar decisões ou classificar dados em ambiente de teste sem intervenção humana. Antes de colocar o modelo em produção, garanta que ele generaliza bem nos dados de teste.
Para isso, você precisa de conjunto de treino e de teste. No Iris, com 150 amostras, vamos treinar em 80% e testar nos 20% restantes.
Em data science, você vai ouvir muito sobre overfitting: quando o modelo aprende muito bem os dados de treino, mas vai mal nos de teste. Dividir em treino e teste (ou validação) ajuda a detectar esse problema.
Para fazer o split, use a função train_test_split da biblioteca sklearn. Vamos dividir:
from sklearn.model_selection import train_test_split
train_data, test_data, train_label, test_label = train_test_split(dataset.iloc[:,:3], dataset.iloc[:,3], test_size=0.2, random_state=42)
Note que random_state é uma semente. Se você mudar o número, a divisão muda. Mantendo o mesmo random_state, a divisão permanece igual em execuções repetidas.
Vamos imprimir o shape dos conjuntos de treino e teste e seus rótulos.
train_data.shape,train_label.shape,test_data.shape,test_label.shape
((120, 3), (120,), (30, 3), (30,))
Agora sim: é hora de alimentar o algoritmo k-nearest neighbor!
O modelo KNN
Depois de carregar, analisar e pré-processar os dados, vamos alimentar o modelo KNN. Para isso, use a função neighbors do sklearn, que tem a classe KNeigborsClassifier.
Comece importando o classificador.
from sklearn.neighbors import KNeighborsClassifier
Observação: o parâmetro k (n_neighbors) costuma ser ímpar para evitar empates na votação.
Para escolher o melhor k, vamos fazer um grid-search simples: treinar e testar o modelo com 10 valores diferentes de k e ficar com o que tiver melhor desempenho.
Vamos inicializar neighbors(k) com valores de 1 a 9 e duas matrizes de zeros do numpy, train_accuracy e test_accuracy, para armazenar as acurácias de treino e teste. Depois, vamos plotar um gráfico e escolher o melhor neighbor.
neighbors = np.arange(1,9)
train_accuracy =np.zeros(len(neighbors))
test_accuracy = np.zeros(len(neighbors))
No próximo trecho acontece a mágica: vamos enumerate sobre os nove valores de vizinhos e, para cada um, prever em treino e teste. Por fim, armazenar as acurácias em train_accuracy e test_accuracy.
for i,k in enumerate(neighbors):
knn = KNeighborsClassifier(n_neighbors=k)
#Fit the model
knn.fit(train_data, train_label)
#Compute accuracy on the training set
train_accuracy[i] = knn.score(train_data, train_label)
#Compute accuracy on the test set
test_accuracy[i] = knn.score(test_data, test_label)
Agora vamos plotar as acurácias de treino e teste com matplotlib. Com o gráfico de accuracy vs. número de vizinhos, você escolhe o melhor valor de k.
plt.figure(figsize=(10,6))
plt.title('KNN accuracy with varying number of neighbors',fontsize=20)
plt.plot(neighbors, test_accuracy, label='Testing Accuracy')
plt.plot(neighbors, train_accuracy, label='Training accuracy')
plt.legend(prop={'size': 20})
plt.xlabel('Number of neighbors',fontsize=20)
plt.ylabel('Accuracy',fontsize=20)
plt.xticks(fontsize=20)
plt.yticks(fontsize=20)
plt.show()

Olhando o gráfico, parece que com n_neighbors=3 o modelo tem o melhor desempenho. Vamos usar n_neighbors=3 e treinar novamente.
knn = KNeighborsClassifier(n_neighbors=3)
#Fit the model
knn.fit(train_data, train_label)
#Compute accuracy on the training set
train_accuracy = knn.score(train_data, train_label)
#Compute accuracy on the test set
test_accuracy = knn.score(test_data, test_label)
Avaliando seu modelo
No último trecho do tutorial, vamos avaliar o modelo nos dados de teste usando técnicas como confusion_matrix e classification_report.
Primeiro, verifique a acurácia no conjunto de teste.
test_accuracy
0.9666666666666667
Uau! Parece que o modelo classificou corretamente 96,66% dos dados de teste. Incrível, né? Com poucas linhas de código, você treinou um modelo que consegue dizer o nome da flor usando apenas quatro atributos, com 96,66% de acerto. Quem sabe ele até supera um humano nessa tarefa.
Matriz de confusão
A matriz de confusão descreve o desempenho do modelo no conjunto de teste, para o qual os rótulos verdadeiros são conhecidos.
O Scikit-learn fornece uma função que calcula a matriz de confusão para você.
prediction = knn.predict(test_data)
A função plot_confusion_matrix() abaixo foi adaptada desta fonte.
import itertools
def plot_confusion_matrix(cm, classes,
normalize=False,
title='Confusion matrix',
cmap=plt.cm.Blues):
plt.imshow(cm, interpolation='nearest', cmap=cmap)
plt.title(title)
plt.colorbar()
tick_marks = np.arange(len(classes))
plt.xticks(tick_marks, classes, rotation=45)
plt.yticks(tick_marks, classes)
fmt = '.2f' if normalize else 'd'
thresh = cm.max() / 2.
for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):
plt.text(j, i, format(cm[i, j], fmt),
horizontalalignment="center",
color="white" if cm[i, j] > thresh else "black")
plt.ylabel('True label',fontsize=30)
plt.xlabel('Predicted label',fontsize=30)
plt.tight_layout()
plt.xticks(fontsize=18)
plt.yticks(fontsize=18)
class_names = load_iris().target_names
# Compute confusion matrix
cnf_matrix = confusion_matrix(test_label, prediction)
np.set_printoptions(precision=2)
# Plot non-normalized confusion matrix
plt.figure(figsize=(10,8))
plot_confusion_matrix(cnf_matrix, classes=class_names)
plt.title('Confusion Matrix',fontsize=30)
plt.show()

Pela confusion_matrix acima, dá para ver que o modelo classificou todas corretamente, exceto uma virginica, que foi prevista como versicolor.
Relatório de classificação
O classification report ajuda a identificar classes mal classificadas com mais detalhes, fornecendo precision, recall e F1 score por classe. Vamos usar o sklearn para visualizar o relatório.
from sklearn.metrics import classification_report
print(classification_report(test_label, prediction))
precision recall f1-score support
Iris-setosa 1.00 1.00 1.00 10
Iris-versicolor 0.90 1.00 0.95 9
Iris-virginica 1.00 0.91 0.95 11
micro avg 0.97 0.97 0.97 30
macro avg 0.97 0.97 0.97 30
weighted avg 0.97 0.97 0.97 30
Vá além!
Parabéns a quem chegou até aqui! Mas isso é só o começo — ainda tem muita estrada pela frente.
Este tutorial tratou dos fundamentos de machine learning e da implementação de um algoritmo, o KNN, em Python. O dataset Iris é pequeno e relativamente simples.
Se este conteúdo despertou seu interesse, experimente outros conjuntos de dados ou estude mais algoritmos de ML e aplique no Iris para observar o impacto na acurácia. Dessa forma, você aprende muito mais do que apenas a teoria!
Se você já praticou bastante com o que foi apresentado aqui e com outros algoritmos de ML, talvez seja hora de se aprofundar em Python e análise de dados.

