ML Workflow and Simple Programs
This note shows how to turn an ML problem into a small Python program.
The goal is not to build industrial AI. The goal is to understand the syllabus workflow, connect each stage to code, and interpret the result of a simple model.
In practical work, H2 students may be expected to use existing Python libraries such as scikit-learn, often in JupyterLab. The hand-coded examples in this note expose the algorithmic logic. They are not meant to replace library-based practical work when the exam or lesson provides a library environment.
Start With the Problem, Not the Model
A common mistake is to choose an algorithm before defining the task.
Before coding, answer these questions:
| Question | Example answer |
|---|---|
| What real problem must be solved? | classify a message as spam or not spam |
| What data is available? | past messages with labels |
| What is one data item? | one message |
| What features will be used? | message length and number of links |
| Are labels available? | yes |
| What output is required? | spam or not spam |
| Which learning type fits? | supervised |
| Which model may fit? | k-NN |
| How will success be measured? | accuracy on test messages |
The formulation should also check whether ML is necessary. If a complete and reliable fixed rule already exists, ordinary programming may be more suitable.
ML Workflow
gather data
prepare data
choose a model
train or fit the model
evaluate the model
tune parameters
make predictionsThese stages are connected rather than completely separate. Evaluation may reveal a problem with the features or data, causing the developer to return to an earlier stage.
1. Gather data
Collect examples relevant to the task.
Questions to ask:
- Are there enough examples?
- Do the examples represent the situations the model will face?
- Are supervised labels available and reliable?
2. Prepare data
Preparation can include:
- correcting invalid or inconsistent values;
- handling missing values;
- choosing useful features;
- converting categories into a suitable representation;
- separating features from labels;
- dividing supervised data into training and testing sets.
For distance-based methods such as k-NN and k-means, feature scale can affect which points count as nearest.
3. Choose a model
Match the model to the task:
| Task | Suitable syllabus model |
|---|---|
| predict a known label from labelled examples | k-NN |
| discover clusters in unlabelled data | k-means |
4. Train or fit the model
Training has different meanings for different algorithms.
- For k-NN, fitting mainly stores the labelled training examples.
- For k-means, fitting repeatedly assigns points and updates cluster centres.
5. Evaluate the model
For supervised classification, compare predictions with known labels on data not used to fit the model.
For clustering, evaluation is less direct because the data has no correct class labels. At syllabus level, inspect whether the clusters are sensible for the problem and compare results produced by different parameter choices when instructed.
6. Tune parameters
A parameter is a setting chosen before or during fitting.
Examples:
n_neighborsfor k-NN;n_clustersfor k-means.
Try suitable values and compare results using validation or training-stage evidence. Do not repeatedly choose values based on the final test set, because that makes the test result less fair.
7. Make predictions or use clusters
After the model has been selected and evaluated:
- a k-NN model can predict labels for new examples;
- a k-means model can assign data to learned clusters.
How the Workflow Appears in scikit-learn
When using a library such as scikit-learn, the same workflow still applies.
| Workflow step | What it looks like in code |
|---|---|
| prepare data | create feature data X and labels y where needed |
| choose a model | create KNeighborsClassifier(...) or KMeans(...) |
| fit | call .fit(...) |
| evaluate | call .score(...), compare predictions, or use cross-validation |
| tune parameters | change values such as n_neighbors or n_clusters |
| make predictions | call .predict(...) |
The important exam skill is not memorising every library detail. It is understanding what each call represents in the ML workflow.
Representing Feature Data
In scikit-learn, X is usually a two-dimensional collection:
X = [
[20, 0],
[25, 1],
[180, 4],
[200, 5],
]Each inner list is one data item. Each position in that list is one feature.
For supervised learning, y stores one label for each row of X:
y = ["not spam", "not spam", "spam", "spam"]Therefore, len(X) and len(y) should match.
Simple Hand-Coded k-NN
This hand-coded example uses one numeric feature: length. It shows the idea behind k-NN.
def distance(a, b):
return abs(a - b)
def classify_knn(training, new_length, k):
if not 1 <= k <= len(training):
raise ValueError("k must be from 1 to the number of training examples")
distances = []
for length, label in training:
gap = distance(length, new_length)
distances.append((gap, 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 in sorted(counts):
count = counts[label]
if count > best_count:
best_label = label
best_count = count
return best_label
training_data = [
(20, "not spam"),
(25, "not spam"),
(180, "spam"),
(200, "spam"),
]
print(classify_knn(training_data, 30, 3))
print(classify_knn(training_data, 190, 3))Expected output:
not spam
spamTrace the first prediction
For new_length = 30:
| Training length | Distance | Label |
|---|---|---|
| 25 | 5 | not spam |
| 20 | 10 | not spam |
| 180 | 150 | spam |
| 200 | 170 | spam |
With , the labels are not spam, not spam, and spam, so the majority prediction is not spam.
Beginner checkpoint: The new item has no known label. The label is predicted from the labelled neighbours.
Limitations of this simple function
Equal-distance examples are secondarily ordered by their label because Python sorts the (distance, label) tuples. A tied vote is resolved alphabetically because the code iterates through sorted labels. These rules are deterministic but arbitrary; a real specification should define its tie policy. Choosing odd only reduces some two-class vote ties and does not resolve equal-distance ambiguity.
k-NN With scikit-learn
This example performs the same kind of classification using a library.
from sklearn.neighbors import KNeighborsClassifier
X = [[20], [25], [180], [200]]
y = ["not spam", "not spam", "spam", "spam"]
model = KNeighborsClassifier(n_neighbors=3)
model.fit(X, y)
print(model.predict([ [30] ])[0])
print(model.predict([ [190] ])[0])Expected output:
not spam
spamIn this example:
Xstores the input features;ystores the labels;KNeighborsClassifier(n_neighbors=3)chooses a k-NN model with ;fit(X, y)trains or stores the labelled examples;predict(...)makes predictions for new examples.[0]selects the first predicted label from the returned collection.
Notice the double brackets in [ [30] ]. The outer list contains examples; the inner list contains the features of one example.
Why Training and Testing Data Should Be Separate
If a model is evaluated only on examples it already used during fitting, the score may give an unrealistically favourable view.
A training set is used to fit the model. A validation set, or cross-validation performed within training data, guides model and parameter choices. A final test set stays untouched until those choices are finished and is used once to estimate performance on unseen examples.
all labelled data -> development/training data + final test data
training data -> fit model
validation or cross-validation within training data -> choose/tune
final test data -> one final independent evaluationWith very small datasets, one split can give an unstable result. Cross-validation repeats evaluation across several different splits.
Train/Test Split and Cross-Validation
The reference guide includes train_test_split and cross_validate. These help evaluate a model instead of only training and predicting on the same examples.
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_train, y_train, cv=2
)
print([round(score, 2) for score in results["test_score"]])Expected output:
1.0
[1.0, 1.0]In this example:
train_test_split(...)separates data for training and testing;stratify=ykeeps the class balance in the split;.score(...)calculates accuracy for the test set;cross_validate(...)evaluates the model across several splits of the training data; it does not reuse the held-out test examples.
Do not assume every example must produce 1.0. This tiny, deliberately separated dataset demonstrates API flow only; it is not convincing evidence that the classifier generalises. The score depends on the data, split, model, and parameter values.
Accuracy
Accuracy measures the fraction of predictions that are correct:
def accuracy(predictions, actual):
if len(predictions) == 0 or len(predictions) != len(actual):
raise ValueError("lists must have the same non-zero length")
correct = 0
for i in range(len(predictions)):
if predictions[i] == actual[i]:
correct += 1
return correct / len(predictions)
print(accuracy(["spam", "not spam", "spam"], ["spam", "spam", "spam"]))Expected output:
0.6666666666666666Two out of three predictions are correct, so the accuracy is .
Accuracy is easy to understand, but a high value does not by itself prove that the data was representative or that the model will work well in every situation. If 95 of 100 messages are ordinary, a model that always predicts ordinary has 95% accuracy while detecting none of the five rare cases. Interpret accuracy with class balance and the kinds of errors being made.
Simple Hand-Coded k-Means Assignment
This hand-coded example performs one assignment step using one-dimensional data.
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
points = [2, 3, 10, 11]
centres = [2, 10]
assignments = []
for point in points:
assignments.append(nearest_centre(point, centres))
print(assignments)Expected output:
[0, 0, 1, 1]This means:
- points
2and3are nearest to centre0; - points
10and11are nearest to centre1.
The numbers 0 and 1 are cluster identifiers. They are not known class labels.
Updating the Centres
The new centre is the mean of the points assigned to the cluster.
def mean(values):
return sum(values) / len(values)
cluster_0 = [2, 3]
cluster_1 = [10, 11]
print(mean(cluster_0))
print(mean(cluster_1))Expected output:
2.5
10.5These new centres would be used in the next assignment step.
A complete hand-coded version also needs to handle an empty cluster. The small example avoids that case so that the main assignment-and-update idea remains clear.
k-Means With scikit-learn
This example uses a library to cluster the same one-dimensional points into two clusters.
from sklearn.cluster import KMeans
X = [[2], [3], [10], [11]]
model = KMeans(
n_clusters=2,
random_state=0,
n_init=10,
)
model.fit(X)
centres = sorted(
round(centre[0], 1)
for centre in model.cluster_centers_
)
print(centres)Expected output:
[2.5, 10.5]In this example:
n_clusters=2means k-means should form two clusters;random_state=0makes the random starting process reproducible;n_init=10tries several starting arrangements;fit(X)performs the clustering process;cluster_centers_stores the learned centre positions;- the centres are sorted only so the printed output is stable for checking.
Cluster numbering can be reversed between valid runs. For example, one run may call the left group cluster 0, while another calls it cluster 1. The grouping can still be equivalent.
Predicting Cluster Membership
After fitting, k-means can assign new points to the learned clusters:
print(model.predict([ [2.2], [10.8] ]))The result contains cluster identifiers. Interpret them by comparing the points with the learned centres rather than assuming that a particular number has a fixed meaning.
Tuning Parameters
Tuning means trying different parameter values and evaluating which works better.
Examples:
| Algorithm | Parameter to tune | Example |
|---|---|---|
| k-NN | number of neighbours | try n_neighbors=1 and 3 on this small dataset |
| k-means | number of clusters | try n_clusters=2 and 3 |
A safe introductory tuning pattern uses cross-validation on the training data:
from sklearn.model_selection import cross_val_score
for k in [1, 3]:
scores = cross_val_score(
KNeighborsClassifier(n_neighbors=k), X_train, y_train, cv=2
)
print(k, scores.mean())After choosing , fit that model on the available development/training data and evaluate it once on the untouched final test set. Once test outcomes influence a choice, the test score is optimistically biased and no longer an independent estimate.
Overfitting occurs when a model fits peculiarities or noise in the training data but performs worse on unseen data. A very small in k-NN can be noise-sensitive, although this is a tendency rather than an absolute rule. High training performance alone is therefore insufficient.
Testing ML Programs
Useful checks:
- verify that the number of feature rows matches the number of labels;
- test a new point close to one known group;
- test a new point close to another known group;
- test boundary or tie cases when the specification defines them;
- test whether changing affects the result as expected;
- check that prediction input has the correct number of features;
- compare actual output with expected output;
- confirm that evaluation data was not accidentally used for fitting.
Supporting precision: preprocessing learned from data—such as scaling values, selecting features, or choosing missing-value replacements—must also be fitted using training data only and then applied unchanged to validation, test and new data. Otherwise information leaks from evaluation data into training.
Reading and Explaining ML Code
For a Paper 2-style program, be ready to explain statements such as:
model = KNeighborsClassifier(n_neighbors=3)
model.fit(X_train, y_train)
predictions = model.predict(X_test)A clear explanation is:
- create a k-NN classifier using three neighbours;
- fit it using the training features and training labels;
- predict labels for the test features.
Avoid descriptions such as “the code runs AI.” State the role of the data, model, fitting, and prediction steps precisely.
Final Workflow Check
For any simple ML program, verify that you can identify:
- the problem;
- each data item;
- the features;
- the labels, if any;
- the learning type;
- the model and parameter values;
- the training or fitting step;
- the evaluation method;
- the prediction or clustering output;
- at least one limitation of the result.
Return to Artificial Intelligence and Machine Learning.