One question has stayed alive since the end of M10, and it's the one that moves the most money: how much stock should we order ahead of Sant Jordi? The previous lessons described the past (strong Saturdays, a runaway April, Faust the window-shopping champion); ordering stock demands projecting the future. That leap — from describing to predicting — is machine learning (ML), and scikit-learn is its reference library in Python. This lesson presents it for real and without hype: what it means for a machine to "learn", two complete cases with Papyrus's data, and the warnings marketing usually skips. Honest introductory level: a reliable map of the territory, not the whole territory.

Contents

  1. What ML really is: written rules vs learned patterns
  2. Types of learning: supervised and unsupervised
  3. The scikit-learn flow: split → fit → predict → evaluate
  4. Complete case 1 — regression: predicting daily sales
  5. The decision: the Sant Jordi order
  6. Complete case 2 — classification: will this member buy this month?
  7. Overfitting: memorizing is not learning
  8. What we have NOT covered (your next map)
  9. An ethical warning

What ML really is

The whole course up to here has been classic programming: you write the rules. M5's sell() deducted stock because you wrote self.stock -= units; Django's clean_price (M10) rejected negatives because you decided it; the member tariff is a formula Ana typed in. Explicit rules, data in, answers out.

ML flips the contract: you give it data along with its answers, and the machine deduces the rules.

Classic programming (M1-M10) Machine learning
You provide Rules + data Data + known answers
The machine produces Answers Rules (a model)
Papyrus example price * 1.04 * 0.95 "Saturdays sell double" — deduced from the CSV
When it shines The rule is known and exact The rule exists but nobody knows how to write it
When it fails Cases you didn't foresee A future unlike the past, scarce or dirty data

Nobody knows how to hand-write the exact formula for "how much Papyrus sells on a Tuesday in March". But the pattern is in sales_2026.csv, and a model can extract it. That — fitting functions to data — is the whole trick. There's no understanding or intent: there's applied statistics with good engineering.

Types of learning

Type What is learned Papyrus example
Supervised — regression Predicting a number from labelled examples How many units will we sell tomorrow? (case 1)
Supervised — classification Predicting a category Will this member buy this month: yes/no? (case 2)
Unsupervised Structure without labels (groups, anomalies) Grouping members by buying habits without telling it which groups to look for

This module practices the two supervised kinds. From unsupervised learning you take away the concept; and there's a fourth world — deep learning (neural networks: the engine behind chatbots and computer vision) — which honestly falls outside this course: it demands more maths, more data and more machine than Papyrus needs.

The scikit-learn flow

pip install scikit-learn

The best thing about scikit-learn is that all models are used the same way — the same API for a linear regression as for a random forest:

flowchart LR
    A["Data<br>X (features)<br>y (answer)"] --> B["train_test_split<br>80% train / 20% test"]
    B --> C["fit(X_train, y_train)<br>the model learns"]
    C --> D["predict(X_test)<br>predicts data it NEVER saw"]
    D --> E["evaluate<br>MAE, accuracy...<br>do we trust it?"]
    E -.->|no| A

The piece that changes everything is train_test_split: 20% of the data is set aside before training, and evaluation happens only on it. Why? Because evaluating on the training data is letting the student grade their own exam with the answers in front of them: high mark guaranteed, learning not guaranteed. We'll come back to this in overfitting.

Complete case 1 — regression: predicting daily sales

Question: how many units will Papyrus sell on a given day? Data: sales_2026.csv aggregated per day (181 days), with features the previous lessons already flagged as relevant: the month, whether it's a weekend (11-03's Saturday) and whether it's Sant Jordi (the 11-02/11-04 peak).

import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_absolute_error

df = pd.read_csv("data/sales_2026.csv", parse_dates=["date"])

# 1) From individual sales to a daily dataset (pandas, 11-03)
daily = df.groupby("date")["units"].sum().reset_index()
daily["month"] = daily["date"].dt.month
daily["is_weekend"] = (daily["date"].dt.dayofweek >= 5).astype(int)
daily["is_sant_jordi"] = (daily["date"] == "2026-04-23").astype(int)

X = daily[["month", "is_weekend", "is_sant_jordi"]]   # features
y = daily["units"]                                     # what we want to predict

# 2) Set the exam aside BEFORE studying
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 3) Learn and 4) predict on the never-seen
model = LinearRegression()
model.fit(X_train, y_train)
predictions = model.predict(X_test)

# 5) Evaluate
print(f"MAE: {mean_absolute_error(y_test, predictions):.2f} units/day")
# MAE: 1.10 units/day

The MAE (mean absolute error) says the model is off, on average, by ±1.1 units per day — against a real mean of about 2.9, that's not brilliant, but it's enough to size a monthly order, because daily errors cancel out when summed. Let's look at the rules it deduced:

for name, coef in zip(X.columns, model.coef_):
    print(f"{name:>15}: {coef:+.2f}")
print(f"{'intercept':>15}: {model.intercept_:+.2f}")
          month: +0.11
     is_weekend: +2.41
  is_sant_jordi: +70.93
      intercept: +1.92

Interpretation with humility, which is how coefficients are always interpreted:

  • Baseline weekday: ~1.9 + 0.11·month ≈ 2-3 units. Consistent with what we observed.
  • Weekend: +2.4 units/day. 11-03's Saturday, now quantified as a rule.
  • Sant Jordi: +71 units. Careful: that coefficient was learned from a single day in the entire history. The model doesn't "understand" Sant Jordi; it memorized one point. With two or three years of data it would be a rule; with one, it's a reliable but fragile annotation.
  • And the general warning: a coefficient is not a cause. is_weekend doesn't cause sales; it captures that people stroll about on Saturdays. If the town hall pedestrianizes the street on a Tuesday, the model has no idea.

The decision: the Sant Jordi order

The model predicts any future day by building its features. April 2027: 30 days, 9 of them weekend, and Sant Jordi falls on a Friday:

import numpy as np

april_2027 = pd.DataFrame({
    "month": [4] * 30,
    "is_weekend": [1 if d in april_2027_weekend_days else 0 for d in range(1, 31)],
    "is_sant_jordi": [1 if d == 23 else 0 for d in range(1, 31)],
})
daily_pred = model.predict(april_2027)
print(f"April 2027 prediction: {daily_pred.sum():.0f} units")
# April 2027 prediction: 172 units

172 units — the same neighbourhood as the real April 2026 (168), which is what's reasonable with only one year of history. And here comes the most important part of the lesson: the model doesn't decide; Ana decides. Ana takes the prediction, adds a safety margin (~10%, because running out of books on 23 April costs more than overstocking a few copies) and splits it according to each title's share of April (11-03):

Title April share Order (out of 190)
Don Quixote 35% 67
The Odyssey 30% 57
Hamlet 23% 44
Faust 12% 22

Final order: 190 units. The question that opened the module, closed: not with a hunch, but with a clean history (11-03), a visualized peak (11-04) and a projection with a known error (±1.1/day). The margin criterion, the split and the final word remain human — as they should.

Complete case 2 — classification: will this member buy this month?

Marta wants to know which members to send the book club newsletter to. A classification question: member × month → buys (1) or not (0)? The members_months.csv dataset comes from crossing the member records (LUIS-001, MARTA-002, PAU-003...) with sales_2026.csv and the web logs: 40 members × 6 months = 240 rows, with bought_last_month, web_visits_month, months_as_member and the label buys — which is 1 in only 36 rows (15%): most months, most people don't buy. That imbalance is the key to everything that follows.

from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, recall_score

members = pd.read_csv("data/members_months.csv")
X = members[["bought_last_month", "web_visits_month", "months_as_member"]]
y = members["buys"]

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2,
                                                    random_state=42, stratify=y)
clf = LogisticRegression()        # classification despite the name; same API: fit/predict
clf.fit(X_train, y_train)
pred = clf.predict(X_test)

print(f"Accuracy: {accuracy_score(y_test, pred):.2f}")   # Accuracy: 0.88

88% correct! Before celebrating, the accuracy trap: since 85% of the rows are "doesn't buy", a "model" that always answers no gets 85% right without learning anything. Our 88% is barely better than that rock. The metric that exposes the con:

print(f"Recall: {recall_score(y_test, pred):.2f}")       # Recall: 0.43

Of the members who did buy, the model only detected 43%. For Marta's newsletter — where missing a likely buyer is what's expensive — this model isn't fit for use as is. Permanent lesson: with imbalanced classes, accuracy lies; always ask for recall/precision (and their remedies: class_weight="balanced", more data, better features). DecisionTreeClassifier trains identically (same API) and, on top of that, can be read as if-then rules — a good next experiment.

Overfitting: memorizing is not learning

Two students facing the M9 exam: one memorized the solutions to the worked exercises; the other understood the concepts. With the exact questions from the book, the memorizer scores a perfect 10; with new questions, they sink — and the one who understood passes. Overfitting is the memorizer model: it nails the training data by fitting even its noise, and fails on new data, which is the only thing that matters.

  • How it's detected: a big gap between performance on train and on test (that's why train_test_split exists). A decision tree with no depth limit can score 100% on train and collapse on test.
  • What encourages it: very flexible models + little data. Our Sant Jordi coefficient, learned from a single day, is a small overfit — acknowledged and documented.
  • What mitigates it: more data, simpler models, and the techniques in the next table.

What we have NOT covered: your next map

Closing honesty — this lesson is the door, not the house:

Topic What it solves Look it up as
Cross-validation Evaluating without depending on the luck of one split cross_val_score, k-fold
Feature engineering Good variables are worth more than good models feature engineering, OneHotEncoder
Scaling and pipelines Chaining preprocessing + model without data leakage StandardScaler, Pipeline
Hyperparameter tuning Choosing the model's configuration methodically GridSearchCV
Ensemble models More accuracy by combining trees RandomForest, gradient boosting
Deep learning Images, text, speech — another scale of data and compute neural networks, PyTorch/TensorFlow

An ethical warning

Case 2 predicts the behaviour of people, and there the bar rises: a model that decides who gets contacted, who gets offered a discount or — in serious domains — who gets granted credit, inherits the biases of its data and gets things wrong about someone with a name and a face. The rule at Papyrus and beyond: models recommend, people decide; any decision with effects on people requires human oversight, and whoever is affected deserves to be able to ask why. Marta reviewing the list before sending the newsletter isn't red tape: it's the difference between using a tool and delegating to it.

Common Mistakes and Tips

  • Evaluating on the training data. The foundational mistake: an inflated mark, guaranteed. The split goes before any fit, always.
  • Trusting accuracy with imbalanced classes. The 88% hiding a 43% recall. Always compare against the dumb baseline (predicting the majority class every time) and look at recall/precision.
  • Reading coefficients as causes. is_weekend: +2.41 describes an association in this data; it's not a physical law or a mechanism. Change the context and the coefficient changes.
  • Extrapolating without saying so. Predicting April 2027 with data from a single year is a reasonable bet declared as such. Predicting December (a Christmas campaign the model never saw) would be making things up.
  • Forgetting random_state. Without pinning it, every run gives a different split and your results aren't reproducible — the ML equivalent of M9's non-deterministic tests.
  • Starting with the model. 80% of the result lies in the question and the data (11-01 to 11-03). A LinearRegression with good features beats a sophisticated model with dirty data.

Exercises

  1. Add the is_saturday feature to case 1 (separate from is_weekend, which becomes is_sunday), retrain and compare the MAE. Given what you know from 11-03 (Saturday 130 vs Sunday 97), what do you expect to happen to the coefficients?
  2. For case 2, compute the baseline: the accuracy of a "model" that always predicts "doesn't buy", on the same y_test. Write it without scikit-learn, with pure pandas or NumPy. How much does LogisticRegression really improve?
  3. Conceptual: Ana proposes training the regression model on ALL the data (no test set aside) "so it learns from more examples" and using it directly for the 2027 order. Give one argument in favour, the argument against, and the standard practice that reconciles both.

Solutions

  1. daily["is_saturday"] = (daily["date"].dt.dayofweek == 5).astype(int)
    daily["is_sunday"] = (daily["date"].dt.dayofweek == 6).astype(int)
    X = daily[["month", "is_saturday", "is_sunday", "is_sant_jordi"]]
    # ... same split/fit/evaluation
    
    The MAE drops slightly (≈1.10 → ≈1.02): separating Saturday and Sunday gives the model the freedom to assign them different weights, and the data takes advantage — the Saturday coefficient (~+3.1) larger than Sunday's (~+1.8), exactly the 130/97 asymmetry groupby showed in 11-03. A good new feature = business knowledge encoded.
  2. baseline = (y_test == 0).mean()
    print(f"'Always no' baseline: {baseline:.2f}")   # 0.85
    
    y_test == 0 is a boolean mask (11-02) and its mean is the proportion of True. The logistic model scores 0.88 against 0.85: 3 points of real improvement, not 88. Reporting models alongside their baseline should be the law.
  3. In favour: with scarce data, every row counts, and a 20% set aside is information wasted for the final model. Against: without a test set you have no honest measure of the error — you wouldn't know whether the order rests on a decent model or an overfitted one; flying without instruments. Standard practice: evaluate with a split (or cross-validation) to know the expected error and, once the approach is validated, retrain on all the data to produce the final model. You get the best of both: the measure and the data.

Conclusion

Module 11 kept its promise: the questions the M10 website left hanging now have answers backed by data. NumPy provided the engine (vectorization and boolean masks), pandas turned it into answers — Saturday as king of the week with 130 units, The Odyssey reigning on the web, Faust with 210 visits and an 11.9% conversion pointing at its price —, Matplotlib made it visible on the back-room dashboard with the arrow pinned on Sant Jordi, and scikit-learn closed the circle: a regression with a MAE of ±1.1 units/day projected 172 units for April 2027, and Ana — not the model: Ana — signed an order for 190, split by shares, with a margin and with judgement. You also take the vaccines with you: accuracy that lies with imbalanced classes, the memorizer's overfitting, coefficients that aren't causes, and the golden rule that models recommend and people decide. Now look at the complete shelf: a papyrus package with models, errors and tests (M1-M9), a website with Flask and Django serving it to the world (M10) and a data lab that understands it (M11). No piece is missing; what's left is assembling them into a system that holds together end to end, from the first line to the final report. That's the final project, and it's exactly module 12.

Python Programming Course

Module 1: Introduction to Python

Module 2: Control Structures

Module 3: Functions and Modules

Module 4: Data Structures

Module 5: Object-Oriented Programming

Module 6: File Handling

Module 7: Error and Exception Handling

Module 8: Advanced Topics

Module 9: Testing and Debugging

Module 10: Web Development with Python

Module 11: Data Science with Python

Module 12: Final Project

© Copyright 2026. All rights reserved