Paper 2 AI and ML Answers

These answers correspond to Paper 2 AI and ML Drills.

Verification note: ChatGPT-work reported executing the revised pure-Python and scikit-learn model blocks in isolated temporary processes. In this Codex runtime, the pure-Python blocks were re-executed; live scikit-learn execution requires scikit-learn to be installed. Fixed random_state controls and stable centre ordering are used where exact output depends on stochastic or label-order behaviour.

Answer 1: Absolute Distance

def distance(a, b):
    return abs(a - b)
 
 
print(distance(10, 3))
print(distance(4, 9))

Expected output:

7
5

Mark points:

  • defines distance(a, b);
  • subtracts the values;
  • returns the absolute value.

Answer 2: Nearest Training Example

The question assumes training is non-empty and that the nearest example is unique for the supplied tests.

def nearest_example(training, value):
    best_label = training[0][1]
    best_distance = abs(training[0][0] - value)
 
    for feature_value, label in training:
        current_distance = abs(feature_value - value)
        if current_distance < best_distance:
            best_distance = current_distance
            best_label = label
 
    return best_label
 
 
training = [(20, "not spam"), (25, "not spam"), (180, "spam")]
print(nearest_example(training, 30))
print(nearest_example(training, 170))

Expected output:

not spam
spam

Mark points:

  • initialises a best distance and label;
  • loops through training examples;
  • calculates distance from value;
  • updates the best example when a smaller distance is found;
  • returns the label;
  • produces both expected outputs.

Answer 3: k-NN Classification

The question assumes training is non-empty, 1 <= k <= len(training), no unresolved majority-label tie occurs, and original list order may be used if equal distances occur at the boundary.

def classify_knn(training, value, k):
    distances = []
 
    for feature_value, label in training:
        distances.append((abs(feature_value - value), label))
 
    distances.sort()
    neighbours = distances[:k]
 
    counts = {}
    for _, label in neighbours:
        counts[label] = counts.get(label, 0) + 1
 
    best_label = None
    best_count = -1
    for label, count in counts.items():
        if count > best_count:
            best_label = label
            best_count = count
 
    return best_label
 
 
training = [(20, "not spam"), (25, "not spam"), (180, "spam"), (200, "spam")]
print(classify_knn(training, 30, 3))
print(classify_knn(training, 190, 3))

Expected output:

not spam
spam

Sorting tuples such as (distance, label) orders by distance first, so distances[:k] selects the nearest k examples.

Mark points:

  • calculates distance for every training example;
  • stores distance with label;
  • sorts by distance;
  • selects the first k neighbours;
  • counts neighbour labels;
  • chooses the majority label;
  • returns the predicted label;
  • uses the supplied k;
  • works for both test values;
  • does not assume labels from the new item.

Answer 4: Accuracy

The question assumes predicted and actual have the same positive length.

def accuracy(predicted, actual):
    correct = 0
    for i in range(len(predicted)):
        if predicted[i] == actual[i]:
            correct += 1
    return correct / len(predicted)
 
 
print(accuracy(["spam", "not spam", "spam"], ["spam", "spam", "spam"]))

Expected output:

0.6666666666666666

Mark points:

  • loops through predictions;
  • compares each prediction with the actual label;
  • counts correct predictions;
  • divides by the number of examples;
  • returns the proportion.

Answer 5: Find Nearest Centre

The question assumes centres is non-empty. The strict < comparison keeps the earlier lower-index centre if distances are equal.

def nearest_centre(value, centres):
    best_index = 0
    best_distance = abs(value - centres[0])
 
    for i in range(1, len(centres)):
        current_distance = abs(value - centres[i])
        if current_distance < best_distance:
            best_index = i
            best_distance = current_distance
 
    return best_index
 
 
print(nearest_centre(3, [2, 10]))
print(nearest_centre(9, [2, 10]))

Expected output:

0
1

Mark points:

  • initialises nearest centre index;
  • compares distance to each centre;
  • updates the index when a closer centre is found;
  • returns the index;
  • passes both tests.

Answer 6: k-Means Assignment Step

def nearest_centre(value, centres):
    best_index = 0
    best_distance = abs(value - centres[0])
 
    for i in range(1, len(centres)):
        current_distance = abs(value - centres[i])
        if current_distance < best_distance:
            best_index = i
            best_distance = current_distance
 
    return best_index
 
 
def assign_clusters(points, centres):
    assignments = []
    for point in points:
        assignments.append(nearest_centre(point, centres))
    return assignments
 
 
print(assign_clusters([2, 3, 10, 11], [2, 10]))

Expected output:

[0, 0, 1, 1]

Mark points:

  • creates an assignment list;
  • loops through all points;
  • finds the nearest centre for each point;
  • appends the centre index;
  • returns the full assignment list;
  • matches the expected output.

Answer 7: Mean of a Cluster

def mean(values):
    return sum(values) / len(values)
 
 
print(mean([2, 3]))
print(mean([10, 11]))

Expected output:

2.5
10.5

Mark points:

  • sums the values;
  • counts the values;
  • divides sum by count;
  • returns a numeric average.

Answer 8: Update k-Means Centres

The question assumptions exclude empty clusters, so mean(cluster_points) is always called with a non-empty list.

def mean(values):
    return sum(values) / len(values)
 
 
def update_centres(points, assignments, k):
    new_centres = []
 
    for cluster_index in range(k):
        cluster_points = []
        for i in range(len(points)):
            if assignments[i] == cluster_index:
                cluster_points.append(points[i])
        new_centres.append(mean(cluster_points))
 
    return new_centres
 
 
points = [2, 3, 10, 11]
assignments = [0, 0, 1, 1]
print(update_centres(points, assignments, 2))

Expected output:

[2.5, 10.5]

Mark points:

  • loops over cluster indexes;
  • collects points assigned to each cluster;
  • uses matching positions in points and assignments;
  • calculates mean for each cluster;
  • appends each new centre;
  • returns the new centre list;
  • uses k to control number of clusters;
  • matches the expected output.

Answer 9: k-NN Parameter Tuning

from sklearn.neighbors import KNeighborsClassifier
 
 
X_train = [[1], [2], [3], [8], [9], [10]]
y_train = ["low risk", "low risk", "high risk", "high risk", "high risk", "high risk"]
 
X_valid = [[3.2], [7.8]]
y_valid = ["low risk", "high risk"]
 
 
scores = {}
 
for k in [1, 3]:
    model = KNeighborsClassifier(n_neighbors=k)
    model.fit(X_train, y_train)
    score = model.score(X_valid, y_valid)
    scores[k] = score
    print(f"k = {k} accuracy: {score}")
 
better_k = max(scores, key=scores.get)
print(f"better k: {better_k}")

Expected output:

k = 1 accuracy: 0.5
k = 3 accuracy: 1.0
better k: 3

The better value is k = 3 because it has higher validation accuracy. Comparing parameter values such as n_neighbors is parameter tuning. A final untouched test set would be preferable for a fair final estimate after tuning.

Mark points:

  • imports or correctly uses KNeighborsClassifier;
  • creates and fits a model with n_neighbors=1;
  • creates and fits a model with n_neighbors=3;
  • obtains the validation score for both models;
  • prints or records both scores correctly;
  • compares the two scores;
  • selects k = 3;
  • identifies the comparison as parameter tuning.

Answer 10: scikit-learn Integration

Part A: k-NN prediction [4]

from sklearn.neighbors import KNeighborsClassifier
 
 
X = [[20], [25], [180], [200]]
y = ["not spam", "not spam", "spam", "spam"]
 
knn = KNeighborsClassifier(n_neighbors=3)
knn.fit(X, y)
 
print(knn.predict([ [30] ])[0])
print(knn.predict([ [190] ])[0])

Expected output:

not spam
spam

fit(X, y) trains or stores the labelled examples for the k-NN classifier.

Mark points:

  • imports or uses KNeighborsClassifier;
  • creates KNeighborsClassifier(n_neighbors=3);
  • fits the model with X and y;
  • predicts and extracts both labels correctly.

Part B: k-means centres [3]

from sklearn.cluster import KMeans
 
 
X = [[2], [3], [10], [11]]
 
kmeans = KMeans(n_clusters=2, random_state=0, n_init=10)
kmeans.fit(X)
 
centres = sorted(round(centre[0], 1) for centre in kmeans.cluster_centers_)
print(centres)

Expected output:

[2.5, 10.5]

fit(X) performs clustering. The cluster labels may be assigned in either order, so sorting the centre values gives a stable output for checking.

Mark points:

  • creates KMeans(n_clusters=2, random_state=0, n_init=10);
  • fits the model using X;
  • extracts, rounds, sorts, and prints the centre values.

Part C: train/test split and cross-validation [5]

from sklearn.model_selection import train_test_split, cross_validate
from sklearn.neighbors import KNeighborsClassifier
 
X = [[1], [2], [3], [10], [11], [12]]
y = ["A", "A", "A", "B", "B", "B"]
 
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.33, random_state=0, stratify=y
)
 
model = KNeighborsClassifier(n_neighbors=1)
model.fit(X_train, y_train)
 
print(model.score(X_test, y_test))
 
results = cross_validate(KNeighborsClassifier(n_neighbors=1), X, y, cv=3)
print([round(score, 2) for score in results["test_score"]])

Expected output:

1.0
[1.0, 1.0, 1.0]

.score(X_test, y_test) returns the classifier accuracy on the held-out test data. results["test_score"] contains the accuracy from each cross-validation fold.

Mark points:

  • uses train_test_split(...) with the specified arguments;
  • trains and scores KNeighborsClassifier(n_neighbors=1);
  • prints the correct test accuracy;
  • uses cross_validate(...) with cv=3 and prints the rounded scores;
  • explains what .score(...) and results["test_score"] represent.

Common weak answer:

  • manually calculating the centres but not using the required library classes.