Implementing k Nearest Neighbours from scratch in Python

This post is a follow up from the previous post. I will work through an implementation from scratch in python.

The code is saved at my github.

The data

The data is a well known dataset related to features of Iris flowers, from the Iris Plants Database. It might be the best known dataset in classification literature, related to several classic papers. The data is here, and more info on the dataset is here.

The code

First, importing the dataset and having a look at the data.

# import required packages
from csv import reader
from math import sqrt

#create a simple function for loading csvs

def load_csv(filename):
    dataset = list()
    with open(filename, 'r') as file:
        csv_read = reader(file)
        for row in csv_read:
            if not row:
                continue
            dataset.append(row)
    return dataset

#identify the file name and load the data

filename = 'iris.csv'
dataset = load_csv(filename)

#view the data

#note it is all in string format and needs to be reformatted

dataset
First view of the data.

Then cleaning the data, converting strings to the correct formats.

# function to convert string column to float

#strip removed trailing and leading blanks
#float function converts to float

def str_to_float(dataset, col):
    for row in dataset:
        row[col] = float(row[col].strip())

#apply the formula to all but the last column

for i in range(len(dataset[0])-1):
    str_to_float(dataset, i)

#now need to clean up the categorical variable, convert it to an integer.

def str_to_int(dataset, col):
    #identify classes
    classes = [row[col] for row in dataset]
    #get distinct classes
    distinct = set(classes)
    #initialise dictionary of class values
    lookup = dict()
    #fill dict with integer for each class value
    for i, value in enumerate(distinct):
        lookup[value] = i
        print('[%s] = %d' % (value, i))
    #lookup into the dictionary, based on the string
    for row in dataset:
        row[col] = lookup[row[col]]
    return lookup 

 #apply formula to last column
    
str_to_int(dataset, len(dataset[0])-1)

Cleaner!

The algorithm

Functions to calculate euclidean distance, identify neighbours, and apply the algorithm to new data.


# Calculate the Euclidean distance
def euclid_dist(r1, r2):
    #initialise distance
    distance = 0.0
    #loop through rows
    for i in range(len(r1)-1):
        distance += (r1[i] - r2[i])**2
    return sqrt(distance)

# Locate the most similar neighbors
def get_neighbours(train, test_row, num_neighbours):
    #train is the training dataset
    #test_row is a single observation
    #num_neighbours is the predefined k value
    
    #initialise list
    distances = list()
    #loop through train rows
    for train_row in train:
        #measure distance betweeen test row and train row
        dist = euclid_dist(test_row, train_row)
        #add train row and distance to distances dataset
        distances.append((train_row, dist))
    #sort by distance
    distances.sort(key=lambda tup: tup[1])
    neighbours = list()
    #identify top k neighbours
    for i in range(num_neighbours):
        neighbours.append(distances[i][0])
    return neighbours
 

# Make a prediction

def predict_class(train, test_row, num_neighbours):
    #identify neighbours using neighbour function
    neighbours = get_neighbours(train, test_row, num_neighbours)
    #keep the class variable
    output = [row[-1] for row in neighbours]
    #assign the most common category as the prediction
    prediction = max(set(output), key=output.count)
    return prediction
 

and then applying it to a single new input!

# define k
num_neighbours = 5
# define a new record
row = [4.3,1.1,2.2,1.1]
# predict the class
label = predict_class(dataset, row, num_neighbours)
print('Data=%s, Predicted: %s' % (row, label))
It runs!

Testing the accuracy of the algorithm

import random

random.shuffle(dataset)

train_data = dataset[:120]
test_data = dataset[120:]

num_neighbours = 5

correct = 0
for i in range(len(test_data)):
    y = predict_class(train_data, test_data[i][0:4], num_neighbours)
    if y == test_data[i][4]:
        correct += 1
    print(y, test_data[i][4])

29 out of 30 correct classifications in the test dataset. Not bad!

Algorithm Overview: k-Nearest Neighbours

k-Nearest Neighbours (k-NN) is a non-parametric classification algorithm that has been around since the 50’s. It is typically used for classification, but can also be used for evaluation. With classification, a data point is compared to the k observations closest to it, and the classes of those observations infer the class of the data point. With regression, the same approach is taken, but the mean of the regression variable is used as the inferred value for the data point. Classification is the more common use.

The measure of distance used to identify neighbours is important, as is the scales of the variables involved – if they vary significantly then standardisation can help with accuracy. Finally, weights are sometimes applied to the neighbours, with nearer data points carrying more significance.

How The Algorithm Works

The training dataset consists of multidimensional vectors, each with a class label.

The classification dataset is an unlabeled vector. With a user-defined k, the unlabeled vector is compared to its k nearest neighbours. For classification, the unlabeled vector gets the mode label from the k nearest neighbours. For regression, it gets the average of the k nearest labels.

Distance Metrics

The most commonly used distance metric for continuous variables is Euclidean Distance. In n-dimensional space the formula is:

Formula from linked Wikipedia page.

For discrete variables, including text classification, metrics like the Hamming Distance can work.

Choosing K

Choosing k depends on the data. Larger values of k can reduce noise, but the boundaries between classes can become complicated. Choice of features can also introduce significant noise: having irrelevant or unimportant features in the data degrades the algorithm’s performance. Feature selection and feature scaling can help with optimisation.

Running the algorithm at different values of k will allow for error rate inspection and optimal parameter choice.

Examples

Here’s a simple example that does calculations by hand.

Here’s a blog post with a from-scratch implementation in python.

Here’s a great blog post from analyticsvidhya.com with python and R implementation, and some nice graphical examples.

Graphs from analyticsvidhya’s post.