Lesson 19: Programming for Automation

Building ML models in Python using OOP — regression and neural networks

📅 April 2026 ⏱ ~87 min 🤖 Module 3 — Software Automation 🐍 Python + scikit-learn
← Back to Learning Hub
🧠

From theory to code

Last lesson you learned the concepts — supervised learning, decision trees, neural networks. This lesson you write working Python. The NESA syllabus specifically requires you to implement: linear regression, polynomial regression, logistic regression, and neural networks — all using OOP. That's exactly what this lesson covers.

0. Homework Review 5 min

Two tasks from Lesson 18. Think through your answers first, then reveal.

Task 1 — Spot ML in the wild: what did you find?

Good examples you might have spotted:

  • YouTube/TikTok autoplay — Recommendation system; supervised or reinforcement. Evidence: adapts after you skip or linger.
  • Spotify "Made for You" — Collaborative filtering. Evidence: improves as you like/skip tracks.
  • Google autocomplete — Supervised learning on billions of queries. Evidence: predicts what you mean before you finish typing.
  • Face ID — Image recognition (supervised). Evidence: trained on your face; fails on someone else's.

Key pattern: it adapts to data rather than following rules a programmer wrote.

Task 2 — Volcanic Pantheon ML design: what did you write?

Strong answers for each option:

  • Comment moderation: Supervised; training data = labeled "toxic / ok" comments; ethical risk = false positives silencing fans unfairly.
  • Character recommendations: Collaborative filtering; training data = visitor page-view logs; ethical risk = tracking behaviour without consent (GDPR concern).
  • Fanart auto-tagging: CNN (supervised); training data = labeled fanart images per character; ethical risk = misclassifying art or tagging content the creator didn't consent to.

These are real product decisions. The reasoning is what the HSC marks — not the answer itself.

1. ML Models Are OOP Objects 10 min

The NESA syllabus says to build ML models "using an OOP." Before you write any model code, notice that you've already been doing OOP every time you use scikit-learn — you just hadn't thought about it that way.

Every scikit-learn model follows the same OOP pattern. You instantiate an object, call its methods, and read its attributes:

from sklearn.linear_model import LinearRegression

# Instantiate — creates a LinearRegression object with chosen hyperparameters
model = LinearRegression()

# fit() — trains the model (modifies the object's internal state)
model.fit(X_train, y_train)

# Attributes set by fit() — the model's learned knowledge
print(model.coef_)        # slope(s)
print(model.intercept_)   # y-intercept

# predict() — uses learned knowledge to answer new questions
predictions = model.predict(X_test)

# score() — evaluates accuracy on unseen data
r2 = model.score(X_test, y_test)
__init__()

Set hyperparameters. No learning yet.

fit(X, y)

Train on data. Sets learned attributes.

predict(X)

Apply learned model to new data.

score(X, y)

Evaluate on unseen data.

This pattern is identical for every model in this lesson — LinearRegression, PolynomialFeatures, LogisticRegression, MLPClassifier. Learn the four methods once; apply them everywhere.

OOP concepts you already know — applied to ML

Everything scikit-learn does is OOP by design. Spot the principles:

OOP ConceptIn scikit-learn ML
Encapsulation The model stores its own learned state (coef_, intercept_). You don't touch it directly — the object manages it through fit().
Abstraction fit() hides all the linear algebra and gradient descent. You call one method; the maths happens inside.
Generalisation LinearRegression, LogisticRegression, and MLPClassifier all share the same interface. One function can train any of them.
Message passing model.fit(X, y) sends a message to the model object. The object acts on its own internal state.
Facade You'll build your own MLPipeline class later in this lesson — it wraps the scaler + model complexity behind a clean interface.

Train/test split — the rule you can't skip

Testing a model on the same data it trained on is like marking your own exam paper. You'd give yourself 100% — but that tells you nothing about whether you actually learned. The test set is data the model has never seen during training. Its score is the only honest measure of performance.

from sklearn.model_selection import train_test_split

# 80% for training, 20% for testing
# random_state=42 makes the split the same every run (reproducible)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

You'll use this before training every model today.

2. Linear Regression — Predicting a Number 15 min

NESA Linear regression predicts a continuous numeric value by fitting a straight line through the data.

The line is: y = mx + b — where m (slope) and b (intercept) are what the model learns from your training data.

Classic question: "If I study for X hours, what exam score should I expect?"

import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split

# ── Data ──────────────────────────────────────────────────────────────
study_hours = np.array([1, 2, 2.5, 3, 4, 4.5, 5, 6, 6.5, 7, 8, 8.5])
exam_scores = np.array([42, 50, 53, 58, 64, 68, 73, 79, 82, 85, 90, 93])

# reshape(-1, 1): scikit-learn expects features as a 2D array (rows × columns)
X = study_hours.reshape(-1, 1)
y = exam_scores

# ── Split ─────────────────────────────────────────────────────────────
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)

# ── Train ─────────────────────────────────────────────────────────────
model = LinearRegression()
model.fit(X_train, y_train)

# ── Evaluate ──────────────────────────────────────────────────────────
print(f"R² score:      {model.score(X_test, y_test):.3f}")

# ── Predict ───────────────────────────────────────────────────────────
print(f"Predicted score for 5.5 hours: {model.predict([[5.5]])[0]:.1f}")

# ── Inspect what the model learned ────────────────────────────────────
print(f"Slope (m):     {model.coef_[0]:.2f}  — score gain per hour studied")
print(f"Intercept (b): {model.intercept_:.2f} — predicted score at 0 hours")

Visualise the result

Add this block after the code above. Training points are shown in blue, test points in orange, and the fitted line in red:

import matplotlib.pyplot as plt
import numpy as np

# ── Visualise ─────────────────────────────────────────────────────────────
x_line = np.linspace(X.min(), X.max(), 100).reshape(-1, 1)
y_line = model.predict(x_line)

plt.figure(figsize=(8, 5))
plt.scatter(X_train, y_train, color='steelblue', label='Training data', zorder=3)
plt.scatter(X_test,  y_test,  color='orange', marker='D', label='Test data', zorder=3)
plt.plot(x_line, y_line, color='crimson', linewidth=2,
         label=f'Fitted line  (R²={model.score(X_test, y_test):.3f})')
plt.xlabel('Study hours')
plt.ylabel('Exam score')
plt.title('Linear Regression — Study Hours vs Exam Score')
plt.legend()
plt.tight_layout()
plt.show()

Orange diamonds are test points — they were never seen during training. The closer they sit to the line, the higher the R².

What is R²?

R² (R-squared) answers: "How much of the variation in exam scores does my model explain?"

R² ValueMeaning
1.0Perfect — model explains all variation
0.85Strong — explains 85% of variation
0.5Moderate — other factors matter too
0.0Useless — no better than predicting the average every time
NegativeWorse than the average — model is actively wrong

Read the output

Suppose the code prints:

R² score:      0.981
Predicted score for 5.5 hours: 75.4
Slope (m):     6.48
Intercept (b): 39.71

Without running any code, answer three questions:

  • What does a slope of 6.48 mean in plain English?
  • What does an intercept of 39.71 mean?
  • Is 0.981 a good R² score?
Reveal answers

Slope 6.48: For every additional hour of study, the predicted exam score increases by 6.48 points.

Intercept 39.71: The model predicts a score of 39.71 if you studied 0 hours. Don't over-interpret intercepts — it's just where the line crosses the y-axis. It doesn't mean 0-hour students will actually score 39.

R² 0.981: Yes — excellent. The line explains 98.1% of the variation in exam scores. For a single-variable model, that's very strong.

Volcanic Pantheon tie-in: If you logged daily visitor counts and the number of new posts published, a linear regression could predict: "Each new post I publish is associated with X extra visitors." Small datasets still teach you to think like a data scientist.

3. Polynomial Regression — Predicting with a Curve 15 min

NESA Linear regression only fits straight lines. But real relationships are often curved.

Consider: how many hours of sleep gives you the best exam performance? Too few and you're exhausted. Too many and you're groggy. The relationship peaks somewhere in the middle — that's a curve, not a line.

Linear regression

Assumes a straight line

y = mx + b

Works when the relationship really is linear. Fails badly on curved data.

Polynomial regression

Fits a curve

y = ax² + bx + c  (degree 2)

Captures peaks, troughs, and non-linear patterns. Same OOP interface.

How it works: the PolynomialFeatures trick

There's no separate "polynomial regression" class. Instead, you use PolynomialFeatures to transform your input first — then pass the transformed features into an ordinary LinearRegression.

For degree 2: it takes your feature [x] and expands it to [1, x, x²]. Now a linear model fitting those three columns is actually fitting a curve.

import numpy as np
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split

# ── Data — sleep hours vs exam performance ────────────────────────────
sleep_hours  = np.array([3, 4, 5, 5.5, 6, 6.5, 7, 7.5, 8, 8.5, 9, 10, 11])
performance  = np.array([42, 50, 60, 65, 72, 78, 85, 87, 84, 80, 74, 62, 50])

X = sleep_hours.reshape(-1, 1)
y = performance

# ── Split ─────────────────────────────────────────────────────────────
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)

# ── Transform features — expand [x] to [1, x, x²] ────────────────────
poly = PolynomialFeatures(degree=2)          # degree=2 fits a parabola
X_train_poly = poly.fit_transform(X_train)  # fit_transform on training data
X_test_poly  = poly.transform(X_test)       # transform (NOT fit_transform) on test data

# ── Train — ordinary LinearRegression on the expanded features ────────
model = LinearRegression()
model.fit(X_train_poly, y_train)

# ── Evaluate ──────────────────────────────────────────────────────────
print(f"R² score: {model.score(X_test_poly, y_test):.3f}")

# ── Predict — must transform new input with the same poly object ──────
new_hours = poly.transform([[7.5]])
print(f"Predicted performance for 7.5h sleep: {model.predict(new_hours)[0]:.1f}")

Visualise the result — curve vs straight line

Add this block after the code above. It plots both a straight line and the polynomial curve on the same chart so you can see exactly why degree=2 wins here:

import matplotlib.pyplot as plt
import numpy as np
from sklearn.linear_model import LinearRegression

# ── Visualise — linear vs polynomial fit ─────────────────────────────────
x_range = np.linspace(X.min(), X.max(), 200).reshape(-1, 1)

# Polynomial curve (uses the poly and model objects from above)
y_curve = model.predict(poly.transform(x_range))

# Straight line for comparison — fit a plain linear model
lin = LinearRegression().fit(X_train, y_train)
y_line = lin.predict(x_range)

plt.figure(figsize=(8, 5))
plt.scatter(X_train, y_train, color='steelblue', label='Training data', zorder=3)
plt.scatter(X_test,  y_test,  color='orange', marker='D', label='Test data', zorder=3)
plt.plot(x_range, y_line,  color='gray',   linestyle='--', linewidth=1.5,
         label=f'Linear fit  (degree=1)')
plt.plot(x_range, y_curve, color='crimson', linewidth=2,
         label=f'Polynomial fit  (degree=2, R²={model.score(X_test_poly, y_test):.3f})')
plt.xlabel('Sleep hours')
plt.ylabel('Performance')
plt.title('Polynomial Regression — Sleep Hours vs Performance')
plt.legend()
plt.tight_layout()
plt.show()

The dashed grey line is what degree=1 would give you — flat and wrong. The red curve captures the peak around 7–8 hours then the drop-off.

Common mistake — fitting the test data

Notice the code uses poly.fit_transform(X_train) but only poly.transform(X_test) — not fit_transform. The fit step "learns" how to expand the features from training data only. If you fit on the test set too, you leak information. Same rule applies when predicting new values.

Degree matters — what happens if you change it?

Predict what would happen in each scenario, then reveal:

What if you use degree=1 on the sleep/performance data?

A straight line would fit the data poorly because the relationship is curved (peaks around 7–8 hours then drops). The R² score would be low — maybe 0.3 or less. The line would have to compromise between the rising section and the falling section, fitting neither well.

What if you use degree=10?

Overfitting. A degree-10 polynomial has so many terms that it can wiggle through every single training point perfectly — memorising the data rather than learning the pattern. The training R² would approach 1.0, but the test R² would be terrible. This is the same overfitting problem from linear regression, just more dramatic.

Rule of thumb: start with degree=2 or 3. Only go higher if you have strong reason and lots of data.

Volcanic Pantheon tie-in: Site engagement often follows a curve — too little content and visitors don't return; too much posted at once and they feel overwhelmed. A polynomial model could find the sweet spot for posting frequency.

4. Logistic Regression — Predicting a Probability 15 min

NESA Despite the name, logistic regression is a classification algorithm — not regression. It predicts the probability that something belongs to a category.

Instead of predicting "score = 74," it predicts "probability of passing = 0.87" — and then classifies based on that probability (usually: above 0.5 → class 1, below → class 0).

The key difference from linear regression: the output is always squashed between 0 and 1 using a function called the sigmoid.

Sigmoid output — maps any number to a probability between 0 and 1

0.0–0.2
Confident: No
0.2–0.4
Likely: No
0.5
50/50
0.6–0.8
Likely: Yes
0.8–1.0
Confident: Yes

Default threshold: > 0.5 → predict class 1

Example — predicting pass/fail

We have two features: hours studied and attendance percentage. The label is pass (1) or fail (0).

import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

# ── Data — [hours_studied, attendance_%] → pass/fail ─────────────────
X = np.array([
    [1,  40], [1,  55], [2,  50], [2,  70], [3,  60],
    [3,  75], [4,  65], [4,  85], [5,  70], [5,  90],
    [6,  75], [6,  95], [7,  80], [8,  85], [8,  95],
])
y = np.array([0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1])

# ── Split ─────────────────────────────────────────────────────────────
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# ── Scale — logistic regression performs better with scaled features ──
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled  = scaler.transform(X_test)

# ── Train ─────────────────────────────────────────────────────────────
model = LogisticRegression()
model.fit(X_train_scaled, y_train)

# ── Evaluate ──────────────────────────────────────────────────────────
print(f"Accuracy: {model.score(X_test_scaled, y_test):.1%}")

# ── Predict — hard label and probability ─────────────────────────────
student = scaler.transform([[5, 72]])   # 5 hours study, 72% attendance
label = model.predict(student)[0]
proba = model.predict_proba(student)[0]

print(f"Prediction: {'Pass' if label == 1 else 'Fail'}")
print(f"P(Fail) = {proba[0]:.3f}   P(Pass) = {proba[1]:.3f}")

Visualise the result — decision boundary

Logistic regression has two features, so the chart is a 2D scatter plot. The shaded region shows the model's probability gradient — red = likely fail, blue = likely pass. The dashed black line is the decision boundary where P(pass) = 0.5:

import matplotlib.pyplot as plt
import numpy as np

# ── Visualise — scatter + decision boundary ───────────────────────────────
fails  = X[y == 0]
passes = X[y == 1]

# Build a grid covering the feature space
h_range = np.linspace(X[:, 0].min() - 0.5, X[:, 0].max() + 0.5, 300)
a_range = np.linspace(X[:, 1].min() - 5,   X[:, 1].max() + 5,   300)
hh, aa = np.meshgrid(h_range, a_range)
grid_scaled = scaler.transform(np.c_[hh.ravel(), aa.ravel()])
Z = model.predict_proba(grid_scaled)[:, 1].reshape(hh.shape)

plt.figure(figsize=(8, 6))
plt.contourf(hh, aa, Z, levels=20, cmap='RdBu', alpha=0.35)
plt.contour( hh, aa, Z, levels=[0.5], colors='black', linewidths=1.5, linestyles='--')
plt.colorbar(label='P(Pass)')
plt.scatter(fails[:,  0], fails[:,  1], color='crimson',   label='Fail (0)', s=80, zorder=3)
plt.scatter(passes[:, 0], passes[:, 1], color='steelblue', label='Pass (1)', s=80, zorder=3)
plt.xlabel('Hours studied')
plt.ylabel('Attendance %')
plt.title('Logistic Regression — Decision Boundary')
plt.legend()
plt.tight_layout()
plt.show()

Anything above and to the right of the dashed line → the model predicts Pass. Everything below → Fail. Students close to the line are the uncertain cases where predict_proba returns a value near 0.5.

Linear vs Logistic — what's the same, what's different

Linear RegressionLogistic Regression
PredictsA number (continuous)A probability → a class
Output rangeAny value (e.g. −∞ to +∞)Always 0.0 – 1.0
EvaluationR² scoreAccuracy / predict_proba
Use whenAnswer is a numberAnswer is a category
OOP patternfit() → predict() → score()fit() → predict() → score()

Spot the mistake

A student writes this code and is confused about why the output looks wrong:

model = LogisticRegression()
model.fit(X_train, y_train)

new_student = [[5, 72]]
print(model.predict(new_student))        # prints [1]
print(model.predict_proba(new_student))  # error or unexpected values

What did they forget to do, and why does it matter for logistic regression?

Reveal the answer

They forgot to scale the features with StandardScaler.

Look at the two features:

  • hours_studied — values like 1, 2, 3 ... 8
  • attendance_% — values like 40, 55, 72 ... 95

Attendance numbers are roughly 10× bigger. Logistic regression learns by adjusting the weight of each feature — nudging them up or down based on how wrong the prediction was. When one feature's values are much larger, the model's weight-adjustment maths gets pushed around by the scale, not the actual importance of the feature. Attendance ends up dominating just because its numbers are bigger.

StandardScaler fixes this by rescaling both features to mean 0, standard deviation 1 — so they sit on the same playing field:

  • hours_studied → roughly −1.5 to +1.5
  • attendance_% → roughly −1.5 to +1.5

Now neither feature dominates just because of its units.

The fix — and a critical rule:

scaler = StandardScaler()
X_train_s = scaler.fit_transform(X_train)  # learns mean/std from training data, then scales it
X_test_s  = scaler.transform(X_test)       # uses the SAME mean/std — never re-learns on test data

You must always call transform() on test data, never fit_transform(). Re-fitting on test data would be "peeking" — the scaler would learn from test values and make the split dishonest, the same problem as training on test data.

Volcanic Pantheon tie-in: A logistic regression could predict whether a visitor will click through to a character page (1) or leave (0) based on their entry point and time on site. That probability score could drive what content you show them on the homepage.

5. Neural Network Models — MLPClassifier 12 min

NESA In Lesson 18 you learned that a neural network transforms data through layers of connected nodes. The MLPClassifier (Multi-Layer Perceptron) is scikit-learn's implementation of this — and it uses the exact same OOP interface you've been using all lesson.

Architecture — what hidden_layer_sizes means

The key hyperparameter is hidden_layer_sizes — a tuple where each number is the count of nodes in that hidden layer:

  • (4,) → one hidden layer with 4 nodes
  • (8, 4) → two hidden layers: 8 nodes, then 4
  • (16, 8, 4) → three hidden layers: gets deeper and more complex

More layers = more capacity to find complex patterns, but also more risk of overfitting on small datasets.

Applying a neural network to the same pass/fail problem

Using the same data as Section 4 — notice how similar the code is. This is the point of the OOP pattern.

import numpy as np
from sklearn.neural_network import MLPClassifier
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

# Same data as Section 4
X = np.array([
    [1,40],[1,55],[2,50],[2,70],[3,60],[3,75],[4,65],[4,85],
    [5,70],[5,90],[6,75],[6,95],[7,80],[8,85],[8,95],
])
y = np.array([0,0,0,0,0,1,0,1,1,1,1,1,1,1,1])

# ── Split ─────────────────────────────────────────────────────────────
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# ── Scale — neural networks require scaled features ───────────────────
scaler = StandardScaler()
X_train_s = scaler.fit_transform(X_train)
X_test_s  = scaler.transform(X_test)

# ── Train ─────────────────────────────────────────────────────────────
mlp = MLPClassifier(
    hidden_layer_sizes=(8, 4),   # two hidden layers
    max_iter=1000,               # maximum training passes
    random_state=42
)
mlp.fit(X_train_s, y_train)

# ── Evaluate ──────────────────────────────────────────────────────────
print(f"Accuracy: {mlp.score(X_test_s, y_test):.1%}")

# ── Predict with confidence ────────────────────────────────────────────
student = scaler.transform([[5, 72]])
label   = mlp.predict(student)[0]
proba   = mlp.predict_proba(student)[0]

print(f"Prediction: {'Pass' if label == 1 else 'Fail'}")
print(f"P(Fail)={proba[0]:.3f}  P(Pass)={proba[1]:.3f}")

When to use a neural network vs logistic regression? For this small 15-row dataset, logistic regression will likely perform just as well (or better) than the neural network. Neural networks shine on large datasets and complex problems — images, audio, text. For small tabular data, simpler models often win because they're less likely to overfit.

Up next — Section 6 covers how to wrap all of this inside your own OOP class. That's where the four principles in the table above come together in code you design yourself.

6. OOP Design for AI — Building Your Own Pipeline 12 min

NESA The syllabus asks you to build ML models "using an OOP." Using scikit-learn objects satisfies this — but designing your own wrapper class goes further and demonstrates that you understand OOP, not just the library.

The problem without a wrapper: every time you use a neural network you have to remember to scale the data, use transform() (not fit_transform()) on test data, and repeat those three steps every time. Forget one step and the model gives wrong answers silently.

Solution: wrap the model + scaler inside one class. The caller never thinks about scaling — they just call train() and predict().

The MLPipeline class

Each part maps directly to an OOP concept:

from sklearn.preprocessing import StandardScaler
from sklearn.neural_network import MLPClassifier
from sklearn.linear_model import LogisticRegression

class MLPipeline:

    def __init__(self, model):
        # Encapsulation — the object owns its scaler and model.
        # The caller doesn't touch these directly.
        self._model  = model
        self._scaler = StandardScaler()
        self._trained = False

    def train(self, X_train, y_train):
        # Abstraction — caller just says "train".
        # Scaling + fitting happens inside; the caller doesn't know or care.
        X_scaled = self._scaler.fit_transform(X_train)
        self._model.fit(X_scaled, y_train)
        self._trained = True

    def predict(self, X):
        # Message passing — pipeline.predict(X) sends a message to the object.
        # The object acts on its own internal state (_scaler, _model).
        X_scaled = self._scaler.transform(X)   # transform only, NOT fit_transform
        return self._model.predict(X_scaled)

    def evaluate(self, X_test, y_test):
        X_scaled = self._scaler.transform(X_test)
        return self._model.score(X_scaled, y_test)

    def is_trained(self):
        return self._trained

Using the class — one interface, any model

This is generalisation in action. The MLPipeline accepts any scikit-learn model because they all share the same interface. You swap the model; none of the surrounding code changes:

import numpy as np
from sklearn.model_selection import train_test_split

X = np.array([
    [1,40],[1,55],[2,50],[2,70],[3,60],[3,75],[4,65],[4,85],
    [5,70],[5,90],[6,75],[6,95],[7,80],[8,85],[8,95],
])
y = np.array([0,0,0,0,0,1,0,1,1,1,1,1,1,1,1])

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# ── Try a neural network ──────────────────────────────────────────────────
nn_pipeline = MLPipeline(MLPClassifier(hidden_layer_sizes=(8, 4), max_iter=1000, random_state=42))
nn_pipeline.train(X_train, y_train)
print(f"Neural network accuracy : {nn_pipeline.evaluate(X_test, y_test):.1%}")

# ── Swap in logistic regression — same code, different model ─────────────
lr_pipeline = MLPipeline(LogisticRegression())
lr_pipeline.train(X_train, y_train)
print(f"Logistic regression acc : {lr_pipeline.evaluate(X_test, y_test):.1%}")

# ── Predict with any pipeline — same call ────────────────────────────────
new_student = np.array([[5, 72]])
print(f"NN prediction      : {'Pass' if nn_pipeline.predict(new_student)[0] == 1 else 'Fail'}")
print(f"LogReg prediction  : {'Pass' if lr_pipeline.predict(new_student)[0] == 1 else 'Fail'}")

The key insight: nn_pipeline.train() and lr_pipeline.train() are identical calls. The pipeline doesn't care which model is inside. That's because both MLPClassifier and LogisticRegression respond to the same messages — fit(), predict(), score(). This is generalisation.

Extend the class

Add a method called compare(models, X_train, X_test, y_train, y_test) that takes a list of models, wraps each one in an MLPipeline, trains it, and prints each model's accuracy.

Reveal answer
def compare(models, X_train, X_test, y_train, y_test):
    for model in models:
        pipeline = MLPipeline(model)
        pipeline.train(X_train, y_train)
        acc = pipeline.evaluate(X_test, y_test)
        print(f"{type(model).__name__:30} {acc:.1%}")

# Usage
compare(
    [LogisticRegression(), MLPClassifier(hidden_layer_sizes=(8, 4), max_iter=1000, random_state=42)],
    X_train, X_test, y_train, y_test
)

type(model).__name__ returns the class name as a string — so it prints "LogisticRegression" or "MLPClassifier" automatically regardless of which model you pass in. This only works because all models share the same OOP interface.

7. Summary & Roadmap 5 min

Regression 1

Linear Regression

Fits a straight line. Predicts a continuous number. Evaluated with R².

Use when: relationship is roughly linear.

Regression 2

Polynomial Regression

Fits a curve using PolynomialFeatures + LinearRegression. Still predicts a number.

Use when: relationship has a peak, trough, or bend.

Regression 3

Logistic Regression

Predicts a probability → a class. Uses sigmoid to keep output 0–1.

Use when: answer is a category (pass/fail, yes/no).

What you can explain right now

Why scikit-learn models are OOP objects and what fit(), predict(), score() each do

Why you must use a train/test split and what overfitting looks like

How to build a linear regression model and what R² means

How PolynomialFeatures transforms inputs so a linear model can fit a curve

How logistic regression differs from linear regression — probability output, sigmoid, classification

How to build an MLPClassifier and what hidden_layer_sizes controls

How to wrap a model in your own OOP class

18

Lesson 18 — Overview (done)

Automation spectrum, AI vs ML, types of learning, key algorithms, applications

19

Lesson 19 — This lesson: Programming for Automation (done)

OOP ML models, train/test split, linear regression, polynomial regression, logistic regression, neural networks

20

Lesson 20 — Impact, Ethics & Exam Prep

Bias, employment, disability, economy, psychological effects, cultural protocols. Full HSC exam question practice.

Homework — Two Tasks

Task 1 — Run the logistic regression code

Install scikit-learn if needed (pip install scikit-learn), then run the Section 4 logistic regression example.

1. What accuracy does it print?

2. Change the student being predicted to [3, 55] (3 hours study, 55% attendance). What does the model predict, and with what confidence?

3. Try [7, 90]. Does the result make sense?

Task 2 — Design question (no code needed)

A music streaming app wants to predict a user's satisfaction score (1–10) after a listening session based on: session length (minutes), number of songs skipped, and whether a recommended playlist was played.

1. Is this linear regression, polynomial regression, logistic regression, or a neural network problem? Why?

2. What would overfitting look like for this model?

3. What would you need to know before choosing degree=3 polynomial instead of degree=1?

Bring both tasks to next lesson. For Task 1, screenshots of the output are fine.


← Back to Learning Hub