Lab-8 (k-NN Classification)¶

(Intro. to Data Science: IIT Delhi - Abu Dhabi)¶

The goal of this lab is to reinforce key concepts involved in a k-NN-based classification and classification in general. We will do this through real datasets. It follows a progression that builds both practical skill and conceptual understanding:

  1. A simple classification example
  2. Effect of scaling
  3. Choosing (k) by cross-validation
  4. Weighted k-NN
  5. Curse of dimensionality / irrelevant features

By the end of this lab, you should be able to:

  • train and evaluate a k-NN classifier,
  • explain why feature scaling matters for k-NN,
  • choose a reasonable value of (k) using validation,
  • compare ordinary k-NN with distance-weighted k-NN,
  • observe how irrelevant features can hurt k-NN in higher dimensions.

In the class, we coded a KNN classifier from scratch to gain a good understanding of the main steps involved and the issues related to efficiency (e.g., replacing loops with library calls where there is scope for parallelization). However, in this lab, we will use the open-source machine learning library Scikit-learn (also known as sklearn), a popular, free, and open-source machine learning library for Python that provides simple and efficient tools for data analysis and predictive modeling. It is built upon other core Python scientific libraries, including NumPy and SciPy. Let us make ourselves familiar with some of the available tools.

In [1]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.pyplot import subplots

# The sklearn.datasets module in the scikit-learn library provides 
# utilities to load various datasets, which are useful for demonstrating 
# machine learning algorithms and benchmarking.
from sklearn.datasets import load_iris, load_wine, load_breast_cancer, load_digits

# The sklearn.model_selection module in the scikit-learn library provides 
# tools for model selection (choosing the best model) and hyperparameter 
# tuning, primarily through cross-validation and various search methods.
# We will use this for creating a test-train split and for cross-validation
from sklearn.model_selection import train_test_split, cross_val_score

# The sklearn.pipeline.Pipeline class in scikit-learn is a powerful 
# utility that sequentially chains multiple data transformation steps 
# and an optional final machine learning estimator into a single, unified object.
from sklearn.pipeline import Pipeline

# The sklearn.preprocessing package in scikit-learn provides a variety of 
# functions and transformer classes to prepare raw data for machine 
# learning models. These tools handle common tasks such as scaling, 
# transforming, and encoding features.
from sklearn.preprocessing import StandardScaler, MinMaxScaler


# The sklearn.neighbors module in the scikit-learn library provides 
# functionality for various neighbors-based machine learning methods.
from sklearn.neighbors import KNeighborsClassifier

# The sklearn.metrics module provides comprehensive tools for evaluating 
# machine learning models, including classification, regression, and 
# clustering performance. Key metrics include accuracy, precision, 
# recall, and F1-score for classification; MSE/MAE for regression; 
# and specialized tools like ROC AUC, confusion matrices, and pairwise 
# distance metrics.
from sklearn.metrics import accuracy_score, confusion_matrix, classification_report, ConfusionMatrixDisplay

# initialize the pseudo-random number generator (PRNG)
np.random.seed(42)

8.1 Introduction¶

Loading and understanding data¶

We start with a simple educational dataset. The sklearn iris dataset is a classic dataset used in machine learning for multi-class classification tasks. It contains measurements from three different species of iris flowers (i.e., three labels/categories) and is built directly into the scikit-learn library, making it easy to load and use for educational and testing purposes.

In [2]:
# We can load this dataset using load_iris
iris = load_iris(as_frame=True)
X_iris = iris.data #The feature matrix
y_iris = iris.target # The class label vector

# You can see the description of the data set using the command below
#print(iris.DESCR)

# Some of the properties of the dataset can be seen using the following print commands
print("Feature names:", list(X_iris.columns))
print("Target names:", list(iris.target_names))
print("Shape of X:", X_iris.shape)
print("Shape of y:", y_iris.shape)
Feature names: ['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)', 'petal width (cm)']
Target names: [np.str_('setosa'), np.str_('versicolor'), np.str_('virginica')]
Shape of X: (150, 4)
Shape of y: (150,)

Training¶

We will create a 70:30 train-test split of the data using the model_selection library. When creating the test-train split, we will use the stratify feature. In scikit-learn, the stratify parameter in the train_test_split function is used to ensure that both the training and testing subsets have the same proportion of class labels as the original dataset. This is particularly useful for imbalanced datasets to prevent a minority class from being underrepresented or completely absent in either set. Finally, we use the accuracy_score function to calculate the fraction of correctly predicted categories on the test set.

In [3]:
# Create the train-test split. With test_size=0.30, it will create a 70:30 split, 
# with 70% of the data being used as training data.
X_train, X_test, y_train, y_test = train_test_split(
    X_iris, y_iris, test_size=0.30, random_state=42, stratify=y_iris
)

# We now set up the KNN model with k=3 and fit the test data.
# Note that in this implementation, we have not scaled the data.
# Sometimes, the KNN classifier behaves well even without scaling 
# (the iris dataset being one of the examples). Later, we'll see datasets
# on which the knn classifier does not behave well without scaling.
knn_iris = KNeighborsClassifier(n_neighbors=3)
knn_iris.fit(X_train, y_train)

# We use our trained KNN model to make the predictions on the test part of the 
# data that we held out.
y_pred = knn_iris.predict(X_test)
#print(y_pred)

# We can now calculate the fraction of correctly predicted labels.
acc = accuracy_score(y_test, y_pred)
print("Test accuracy:", acc)
Test accuracy: 0.9555555555555556

Evaluation¶

The accuracy score is a useful metric for evaluating a classifier. However, the accuracy score alone misses many fine-grained aspects of classifier evaluation. For example, we may be interested in which classes are typically confused with one another, and when a model predicts a class, how often is it correct? Many such questions can be answered using a confusion matrix.

Confusion Matrix, Precision, Recall, F1-Score, and Support¶

A confusion matrix is a table that shows how a classifier’s predictions compare with the true labels. Suppose we have a classification problem with 3 categories:

  • Cat
  • Dog
  • Rabbit and the confusion matrix is:

$$ \begin{array}{c|ccc} \text{Actual} \backslash \text{Predicted} & \text{Cat} & \text{Dog} & \text{Rabbit} \\ \hline \text{Cat} & 8 & 1 & 1 \\ \text{Dog} & 2 & 7 & 1 \\ \text{Rabbit} & 0 & 2 & 8 \end{array} $$

How to read the confusion matrix¶
  • Each row corresponds to the actual class.
  • Each column corresponds to the predicted class.
  • The entries on the main diagonal are the correct predictions.
  • The entries off the diagonal are the mistakes.

For example:

  • Among the 10 actual Cats, the classifier predicted: 8 as Cat, 1 as Dog, and 1 as Rabbit.
  • Among the 10 actual Dogs, the classifier predicted: 2 as Cat, 7 as Dog, and 1 as Rabbit.
  • Among the 10 actual Rabbits, the classifier predicted: 0 as Cat, 2 as Dog, and 8 as Rabbit.

This tells us not only how many predictions were correct, but also which classes are being confused with each other.

Precision, Recall, F1-Score, and Support¶

For each class, we can compute additional evaluation measures.

1. Precision¶

For a given class, $$\text{Precision} = \frac{\text{True Positives}}{\text{All predicted as that class}}$$

Precision answers the question: When the model predicts this class, how often is it correct?

2. Recall¶

For a given class, $$\text{Recall} = \frac{\text{True Positives}}{\text{All actual members of that class}}$$

Recall answers the question: Out of all the actual items in this class, how many did the model correctly find?

3. F1-score¶

The F1-score combines precision and recall into a single number: $$F_1 = 2 \cdot \frac{\text{Precision} \cdot \text{Recall}}{\text{Precision} + \text{Recall}}$$

It is the harmonic mean of precision and recall. A high F1-score means the classifier has a good balance of:

  • not making too many false positives, and
  • not missing too many true cases.

4. Support¶

Support is simply the number of actual examples in that class. So in this example:

  • Cat support = 10
  • Dog support = 10
  • Rabbit support = 10

Example calculations¶

Class: Cat¶

  • True Positives (TP) = 8
  • Total predicted as Cat = (8 + 2 + 0 = 10)
  • Total actual Cats = (8 + 1 + 1 = 10)

$$\text{Precision}_{\text{Cat}} = \frac{8}{10} = 0.80, \quad \text{Recall}_{\text{Cat}} = \frac{8}{10} = 0.80$$

$$F1_{\text{Cat}} = 2 \cdot \frac{0.80 \cdot 0.80}{0.80 + 0.80} = 0.80, \quad support = 10$$

Summary table¶

$$ \begin{array}{c|cccc} \text{Class} & \text{Precision} & \text{Recall} & \text{F1-score} & \text{Support} \\ \hline \text{Cat} & 0.80 & 0.80 & 0.80 & 10 \\ \text{Dog} & 0.70 & 0.70 & 0.70 & 10 \\ \text{Rabbit} & 0.80 & 0.80 & 0.80 & 10 \end{array} $$


Why these measures are useful¶

Accuracy gives only the overall fraction of correct predictions. But precision, recall, and F1-score give a more detailed picture.

  • Precision is useful when false positives are costly.
  • Recall is useful when missing true cases is costly.
  • F1-score is useful when we want a balance between precision and recall.
  • Support tells us how many actual examples each class has.

So, these measures help us understand not just how well the classifier performs, but also how it makes mistakes.

Let us now compute the confusion matrix and the classification report for our KNN-based classifier.

In [4]:
disp = ConfusionMatrixDisplay.from_estimator(knn_iris, X_test, y_test)
plt.show()

print(classification_report(y_test, y_pred, target_names=iris.target_names))
No description has been provided for this image
              precision    recall  f1-score   support

      setosa       1.00      1.00      1.00        15
  versicolor       0.88      1.00      0.94        15
   virginica       1.00      0.87      0.93        15

    accuracy                           0.96        45
   macro avg       0.96      0.96      0.96        45
weighted avg       0.96      0.96      0.96        45

Task 8.1 (10 points):¶

Let's do a simple exercise where we use a k-NN classifier (trained on the training set created above) for values of $k = 1, 3, 5, 7, 9, 11, 13, 15$ and test it (on the test set we have created above) and output the value of $k$ that gives the best accuracy score. You are asked to output a table of $k$ and test accuracy, as well as a plot of accuracy score-versus-$k$. Please note that, in practice, this is not the correct method for picking the right value of $k$ for prediction. We will see the method in a subsequent task.

In [5]:
# Use this code cell to write the code for the above task. You can create more code/markdown cells if required.

8.2 Relevance of scaling¶

In this section, we will work with the wine dataset included in the sklearn library to train two KNN models: one with scaling and another without scaling, and we will observe the difference in the accuracy of the KNN classifiers.

The sklearn wine dataset is a classic, small, built-in multi-class classification dataset used for machine learning examples and benchmarking. It contains results from a chemical analysis of three types of wines grown in the same region in Italy. There are 13 numeric, predictive attributes (features), and three distinct classes of wine origin.

In [6]:
# Let us first load the wine dataset and observe the numerical values of the features
wine = load_wine(as_frame=True)
X_wine = wine.data
y_wine = wine.target

print("Shape of X:", X_wine.shape)
print("Feature names:", list(X_wine.columns))
print("Target names:", list(wine.target_names))
X_wine.describe().T[["mean", "std", "min", "max"]]
Shape of X: (178, 13)
Feature names: ['alcohol', 'malic_acid', 'ash', 'alcalinity_of_ash', 'magnesium', 'total_phenols', 'flavanoids', 'nonflavanoid_phenols', 'proanthocyanins', 'color_intensity', 'hue', 'od280/od315_of_diluted_wines', 'proline']
Target names: [np.str_('class_0'), np.str_('class_1'), np.str_('class_2')]
Out[6]:
mean std min max
alcohol 13.000618 0.811827 11.03 14.83
malic_acid 2.336348 1.117146 0.74 5.80
ash 2.366517 0.274344 1.36 3.23
alcalinity_of_ash 19.494944 3.339564 10.60 30.00
magnesium 99.741573 14.282484 70.00 162.00
total_phenols 2.295112 0.625851 0.98 3.88
flavanoids 2.029270 0.998859 0.34 5.08
nonflavanoid_phenols 0.361854 0.124453 0.13 0.66
proanthocyanins 1.590899 0.572359 0.41 3.58
color_intensity 5.058090 2.318286 1.28 13.00
hue 0.957449 0.228572 0.48 1.71
od280/od315_of_diluted_wines 2.611685 0.709990 1.27 4.00
proline 746.893258 314.907474 278.00 1680.00

A visual inspection of the statistics of the numerical features shows that the units vary across features, and that distance computations will be dominated by a few features, whereas the others will make insignificant contributions. So, the accuracy of our nearest neighbour classifier may suffer if we do not scale. Let us check if this is indeed true.

In [7]:
# We shall first create a train-test split
X_train, X_test, y_train, y_test = train_test_split(
    X_wine, y_wine, test_size=0.30, random_state=42, stratify=y_wine
)

# Let's find the accuracy of our 5-NN classifier without scaling
knn_raw = KNeighborsClassifier(n_neighbors=5)
knn_raw.fit(X_train, y_train)
pred_raw = knn_raw.predict(X_test)
acc_raw = accuracy_score(y_test, pred_raw)

# We shall use the StandardScaler to scale the training dataset
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
# The transform method is then used to scale the test data using parameters
# obtained from the training data.
X_test_scaled = scaler.transform(X_test)
# We now classify and evaluate using the scaled data
knn_scaled = KNeighborsClassifier(n_neighbors=5)
knn_scaled.fit(X_train_scaled, y_train)
pred_scaled = knn_scaled.predict(X_test_scaled)
acc_scaled = accuracy_score(y_test, pred_scaled)

print('Accuracy without scaling: ', acc_raw)
print('Accuracy with standard scaling: ', acc_scaled)
Accuracy without scaling:  0.7222222222222222
Accuracy with standard scaling:  0.9444444444444444

There is a clear gap in the classifier's accuracy between scaled and unscaled data. Now that we have seen a clear case where scaling makes a huge difference in prediction, let us try out another scaling strategy. This is known as a min-max scaler. Instead of scaling the $i^{th}$ feature as $\frac{x - \mu_i}{\sigma_i}$, we will scale it as $\frac{x - min_i}{max_i - min_i}$, where $max_i, min_i$ are the maximum and and minimum values of the $i^{th}$ feature. Note that this scales the numerical feature to the fixed range $[0, 1]$ (note that the standard scaler does not map to a fixed range). In certain scenarios, this scaling can improve accuracy in KNN classification. You should consider the advantages and disadvantages of min-max scaling versus standard scaling, and identify scenarios where one would perform better than the other. Let us check whether min-max scaling indeed improves classification on the wine dataset.

In [8]:
# We shall use the MinMaxScaler to scale the training dataset
scaler = MinMaxScaler()
X_train_mmscaled = scaler.fit_transform(X_train)
# The transform method is then used to scale the test data using parameters
# obtained from the training data.
X_test_mmscaled = scaler.transform(X_test)
# We now classify and evaluate using the scaled data
knn_mmscaled = KNeighborsClassifier(n_neighbors=5)
knn_mmscaled.fit(X_train_mmscaled, y_train)
pred_mmscaled = knn_mmscaled.predict(X_test_mmscaled)
acc_mmscaled = accuracy_score(y_test, pred_mmscaled)

print('Accuracy with min-max scaling: ', acc_mmscaled)
Accuracy with min-max scaling:  0.9629629629629629
In [9]:
# Let us also see the confusion matrix for the classifier using min-max scaling
disp = ConfusionMatrixDisplay.from_estimator(knn_mmscaled, X_test_mmscaled, y_test)
plt.show()

print(classification_report(y_test, pred_mmscaled, target_names=wine.target_names))
No description has been provided for this image
              precision    recall  f1-score   support

     class_0       1.00      1.00      1.00        18
     class_1       1.00      0.90      0.95        21
     class_2       0.88      1.00      0.94        15

    accuracy                           0.96        54
   macro avg       0.96      0.97      0.96        54
weighted avg       0.97      0.96      0.96        54

Task 8.2 (10 points):¶

For the task in this section, let us work with a larger wine dataset publicly available at https://archive.ics.uci.edu/dataset/186/wine+quality. There are 12 numerical predictor attributes, and the target attribute (quality) is a categorical variable with integer values in the range 0 to 10. For the purposes of our experiments, we will reduce the number of categories from 11 to 3 by mapping the quality scores of 0-3 to 0 (indicating "poor" quality), scores of 4-7 to 1 (indicating "regular" quality), and scores of 8-10 to 2 (indicating "high" quality). With these 3 class labels, we would like to repeat the analysis in this section to see the impact of scaling (both min-max and standard) on the accuracy of the k-NN classifier. For the experiments, we will fix the value of $k$ to be $5$ (as in the above analysis). The dataset has already been included for this task, and you can load it using the command below.

In [10]:
df = pd.read_csv('whitewinequality.csv')
df
Out[10]:
fixed acidity volatile acidity citric acid residual sugar chlorides free sulfur dioxide total sulfur dioxide density pH sulphates alcohol quality
0 7.0 0.27 0.36 20.7 0.045 45.0 170.0 1.00100 3.00 0.45 8.8 6
1 6.3 0.30 0.34 1.6 0.049 14.0 132.0 0.99400 3.30 0.49 9.5 6
2 8.1 0.28 0.40 6.9 0.050 30.0 97.0 0.99510 3.26 0.44 10.1 6
3 7.2 0.23 0.32 8.5 0.058 47.0 186.0 0.99560 3.19 0.40 9.9 6
4 7.2 0.23 0.32 8.5 0.058 47.0 186.0 0.99560 3.19 0.40 9.9 6
... ... ... ... ... ... ... ... ... ... ... ... ...
4893 6.2 0.21 0.29 1.6 0.039 24.0 92.0 0.99114 3.27 0.50 11.2 6
4894 6.6 0.32 0.36 8.0 0.047 57.0 168.0 0.99490 3.15 0.46 9.6 5
4895 6.5 0.24 0.19 1.2 0.041 30.0 111.0 0.99254 2.99 0.46 9.4 6
4896 5.5 0.29 0.30 1.1 0.022 20.0 110.0 0.98869 3.34 0.38 12.8 7
4897 6.0 0.21 0.38 0.8 0.020 22.0 98.0 0.98941 3.26 0.32 11.8 6

4898 rows × 12 columns

8.3 Choosing $k$ using cross-validation¶

Cross-validation is a method for estimating how well a machine learning model will perform on new, unseen data. The basic idea is to repeatedly split the dataset into two parts:

  • a training set, used to fit the model, and
  • a validation set, used to test how well the fitted model performs.

This is useful because a model may perform very well on the training data but not as well on new data. Cross-validation gives a more reliable estimate of real-world performance than a single train-test split.

m-Fold Cross-Validation¶

A common version is (m)-fold cross-validation. In (m)-fold cross-validation:

  1. The data is divided into (m) roughly equal parts, called folds.
  2. The model is trained on (m-1) folds.
  3. The remaining fold is used for validation.
  4. This process is repeated (m) times, each time using a different fold as the validation set.
  5. The performance scores are then averaged.

For example, in 5-fold cross-validation:

  • the data is split into 5 parts,
  • the model is trained 5 times,
  • each part is used once as the validation set.

This helps reduce the dependence of the evaluation on one particular split of the data.

Why it is useful¶

Cross-validation helps us:

  • estimate model performance more reliably,
  • compare different models,
  • choose hyperparameters such as the value of (k) in k-NN,
  • reduce the risk of overfitting to a single train-test split.

Example in k-NN¶

Suppose we want to choose the best number of neighbors in a k-NN classifier. We can try several values such as (k=1,3,5,7,9), use cross-validation to evaluate each one, and then select the value that gives the best average validation accuracy.

A note of caution¶

Cross-validation should be done using only the training data when a final test set is available. The test set should be kept separate until the very end, so that it provides an unbiased final evaluation.

Choosing the best $k$ on Breast Cancer Dataset¶

We shall perform cross-validation to select the best $k$ on the breast cancer dataset available in the sklearn library. There are 30 predictive features and 2 class labels (0/1 for benign/malignant).

In [11]:
cancer = load_breast_cancer(as_frame=True)
X_cancer = cancer.data
y_cancer = cancer.target

print("Shape of X:", X_cancer.shape)
print("Class names:", list(cancer.target_names))
print(y_cancer.value_counts())
Shape of X: (569, 30)
Class names: [np.str_('malignant'), np.str_('benign')]
target
1    357
0    212
Name: count, dtype: int64

We'll keep aside a test set that we will use to test the final model (i.e., after choosing the best $k$ through cross-validation on the remaining training set). We'll go with the standard 70-30 split.

In [12]:
# We shall first create a train-test split and use the training set for cross-validation
X_train, X_test, y_train, y_test = train_test_split(
    X_cancer, y_cancer, test_size=0.30, random_state=42, stratify=y_cancer
)

Instead of implementing the cross-validation method from scratch, we shall use the in-built library function cross_val_score that is described below:

The sklearn.model_selection.cross_val_score function in scikit-learn provides a simple way to evaluate a model's performance using cross-validation. It fits the model on different training subsets of the data and returns an array of scores, one for each cross-validation fold.

Key Concepts:

  • Purpose: cross_val_score estimates how well a model is expected to generalize to unseen data by mitigating the bias of a single train-test split.
  • Process: The dataset is split into $m$ "folds" (subsets), where $m$ is determined by the cv parameter (default is 5 in recent versions). For each iteration, one fold is held out as the test set, and the model is trained on the remaining $m-1$ folds. The trained model is evaluated on the held-out test fold, and a score is recorded. This process repeats $m$ times, ensuring every sample in the dataset is used as part of the test set exactly once (in standard $m$-fold).
  • Output: The function returns an ndarray of floating-point scores, which can then be averaged to get a robust estimate of the model's performance.

We must give the model as input to the cross_val_score function. What should we give as input? If we simply give KNeighborsClassifier(n_neighbors=k) (for some value of $k$) as the model, then the cross-validation will happen without scaling, which we have seen is not a good idea. We would want to include scaling as part of the model. This can be done through the pipeline utility.

Pipeline: The sklearn Pipeline is a powerful utility in the scikit-learn library that allows you to chain multiple data processing steps and an optional final machine learning estimator into a single, cohesive object.

So, instead of our KNN model being defined as model = KNeighborsClassifier(n_neighbors=k), we shall define it using pipeline as

model = Pipeline([
        ("scaler", StandardScaler()),
        ("knn", KNeighborsClassifier(n_neighbors=k))
    ])

The code for running a 5-fold cross-validation for $k=5$ is given below:

In [13]:
model = Pipeline([
        ("scaler", StandardScaler()),
        ("knn", KNeighborsClassifier(n_neighbors=5))
    ])
scores = cross_val_score(model, X_train, y_train, cv=5, scoring="accuracy")
print('The scores are:', scores)
print('The average score is: ', scores.mean())
The scores are: [0.9625     0.975      0.95       0.97468354 0.97468354]
The average score is:  0.967373417721519

Task 8.3 (10 points):¶

Use the above code to find the value of $k$ that gives the best average accuracy. Also, plot average-accuracy-versus-k to visualize. Then use that value of $k$ in the k-NN classifier trained on the training data (i.e., X_train) to evaluate its performance on the test data (i.e., X_test). Give the detailed classification report as we did earlier.

In [14]:
# Use this code cell for the above task. Create more cells if needed.

8.4 Weighted k-NN versus ordinary k-NN¶

In ordinary k-NN, each of the (k) neighbors gets the same vote. In distance-weighted k-NN, closer neighbors get more influence than farther ones. In scikit-learn:

  • weights="uniform" means ordinary k-NN
  • weights="distance" means weighted k-NN

Task 8.4 (10 points):¶

In this task, you are asked to find out about the distance-weighted k-NN functionality in scikit-learn and compare the performance of ordinary versus weighted k-NN on the wine dataset. The train-test split has been created for you to start working. Check if weighted k-NN gives better accuracy than ordinary k-NN for various values of $k$?

In [15]:
X_train, X_test, y_train, y_test = train_test_split(
    X_wine, y_wine, test_size=0.30, random_state=44, stratify=y_wine
)
# Write the rest of the code for the above task here. Create more cells if required.

8.5 Curse of dimensionality: adding irrelevant features¶

As discussed in class, k-NN often suffers when the number of features grows, especially when many of those features are irrelevant noise. To see this, we will work with the Iris data. We will add random noise features and see the impact on the performance of the k-NN classifier.

In [16]:
# Let us create a copy of the IRIS data that we'll be working with
X_base = X_iris.copy()
y_base = y_iris.copy()
In [17]:
# This is a helper function that adds normally distributed noise
# The number of normally distributed features is given as input
def add_noise_features(X, num_noise_features, random_state=42):
    rng = np.random.default_rng(random_state)
    noise = rng.normal(size=(X.shape[0], num_noise_features))
    noise_cols = [f"noise_{i+1}" for i in range(num_noise_features)]
    noise_df = pd.DataFrame(noise, columns=noise_cols, index=X.index)
    return pd.concat([X, noise_df], axis=1)

# Let us use this function to create a dataframe with 5 extra columns with noise
X_aug = add_noise_features(X_base, 5, random_state=42)
X_aug
Out[17]:
sepal length (cm) sepal width (cm) petal length (cm) petal width (cm) noise_1 noise_2 noise_3 noise_4 noise_5
0 5.1 3.5 1.4 0.2 0.304717 -1.039984 0.750451 0.940565 -1.951035
1 4.9 3.0 1.4 0.2 -1.302180 0.127840 -0.316243 -0.016801 -0.853044
2 4.7 3.2 1.3 0.2 0.879398 0.777792 0.066031 1.127241 0.467509
3 4.6 3.1 1.5 0.2 -0.859292 0.368751 -0.958883 0.878450 -0.049926
4 5.0 3.6 1.4 0.2 -0.184862 -0.680930 1.222541 -0.154529 -0.428328
... ... ... ... ... ... ... ... ... ...
145 6.7 3.0 5.2 2.3 1.461179 0.282587 0.767317 -1.140161 -1.119536
146 6.3 2.5 5.0 1.9 0.447814 0.058274 0.548739 -0.187671 0.278144
147 6.5 3.0 5.2 2.0 0.158119 0.777767 0.807008 -1.619872 -2.247269
148 6.2 3.4 5.4 2.3 1.001745 1.187725 -1.020623 -1.859835 0.099035
149 5.9 3.0 5.1 1.8 0.930838 1.797595 0.516298 -0.371717 -0.893131

150 rows × 9 columns

Task 8.5 (10 points):¶

In this task, you are asked to add various numbers of noisy features ($0, 2, 5, 10, 20, 50, 100, 200$) using the function above and evaluate the accuracy of the k-NN classifier with $k = 5$. For evaluation, create a 70:30 train-test split (after adding the noisy features). Draw the plot of the accuracy versus the number of noisy features and make a conclusion about the impact of noisy features to the accuracy of the k-NN classifier.

In [18]:
# Write the code for the above task here. You may create more cells if needed.

8.6 A minor project on k-NN classification¶

Task 8.6 (50 points):¶

You are asked to use all the concepts covered in this lab to design a k-NN classifier for a smaller version (with 20000 points) of the covertype dataset. One of the aspects on which your classifier will be graded is its accuracy. You also need to show and explain all your work on how you decided on your final k-NN classifier. Your understanding of various issues will determine your score on this minor project.

In [19]:
from sklearn.datasets import fetch_covtype

X, y = fetch_covtype(return_X_y=True, shuffle=True, random_state=42)

# create a smaller subset of 20,000 examples
X_small, _, y_small, _ = train_test_split(
    X, y, train_size=20000, stratify=y, random_state=42
)
# For this project you are asked to consider (X_small, y_small) as the entire labeled dataset available to you.
# Please feel free to create as many cells as required for this project
In [ ]: