Skip to content

The baseline and the first model

Chapter 7 10 code blocks 2140 words

The code in this chapter uses these filesrun it on your own machine

The scene: 0.85, is that good?

A student runs up to you: "Teacher, my model got 85% accuracy!"

You ask one question: "What percentage of your data is 'yes'?"

They don't know.

If the answer is 85%, their model learned nothing. A model that says "yes to everyone" scores exactly the same.

This chapter is about one number: the pass mark. It is step 6 of the protocol from Chapter 1.

Column names

The main data is synthetic Tashkent rental listings, and its column names are Uzbek. tuman district, xonalar rooms, maydon_m2 area in m², qavat floor, qavatlar_soni floors in the building, metro_km distance to the metro, mebel furnished (0/1), qurilgan_yil year built, narx_usd monthly rent in USD. The target ijaraga_berildi_30k means rented within 30 days.

Without a thermometer, "hot" means nothing

accuracy = 0.85 means nothing on its own. It only means something in comparison.

Compared to what? To the dumbest possible model, one that learns nothing.

If you don't beat it, your complex model, your two weeks of work and your GPU time were wasted.

python11 lines
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.dummy import DummyClassifier
from sklearn.metrics import accuracy_score, f1_score

df = pd.read_csv(DATA / "uy_ijara.csv")
num = ["xonalar", "maydon_m2", "qavat", "qavatlar_soni", "metro_km", "mebel", "qurilgan_yil"]
X, y = df[num + ["tuman"]], df["ijaraga_berildi_30k"]
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.3, random_state=42, stratify=y)

print(f"Target ratio: {y.mean():.1%} 'yes'")
Output
Target ratio: 50.7% 'yes'

DummyClassifier: four dumb models

It comes ready-made in sklearn, with several kinds of "dumb".

python13 lines
strategies = {
    "most_frequent": "Always says the most common class",
    "stratified": "Guesses at random, following the class ratio",
    "uniform": "Flips a coin (equal odds)",
    "prior": "Predicts the same as most_frequent",
}
rows = []
for s, note in strategies.items():
    d = DummyClassifier(strategy=s, random_state=42).fit(X_tr[num], y_tr)
    b = d.predict(X_te[num])
    rows.append({"Strategy": s, "Accuracy": round(accuracy_score(y_te, b), 4),
                 "F1": round(f1_score(y_te, b, zero_division=0), 4), "What it does": note})
show(pd.DataFrame(rows))
StrategyAccuracyF1What it does
most_frequent0.5060.672Always says the most common class
stratified0.4940.494Guesses at random, following the class ratio
uniform0.5220.525Flips a coin (equal odds)
prior0.5060.672Predicts the same as most_frequent

Which one do we take? most_frequent, because it is the "strongest dummy". The other strategies come out lower, so it gives the strictest pass mark.

python5 lines
baseline = accuracy_score(y_te, DummyClassifier(strategy="most_frequent")
                          .fit(X_tr[num], y_tr).predict(X_te[num]))
print(f"BASELINE = {baseline:.4f}")
print()
print("Write this number in your notebook. Every later result is compared to it.")
Output
BASELINE = 0.5056

Write this number in your notebook. Every later result is compared to it.

Now a real model

python21 lines
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression

pipe = Pipeline([
    ("prep", ColumnTransformer([
        ("num", Pipeline([("impute", SimpleImputer(strategy="median")),
                          ("scale", StandardScaler())]), num),
        ("cat", OneHotEncoder(handle_unknown="ignore"), ["tuman"]),
    ])),
    ("model", LogisticRegression(max_iter=2000)),
]).fit(X_tr, y_tr)

model_acc = accuracy_score(y_te, pipe.predict(X_te))
print(f"BASELINE: {baseline:.4f}")
print(f"MODEL   : {model_acc:.4f}")
print(f"Gap     : {model_acc - baseline:+.4f}")
print()
print("Verdict:", "PASSED, the model really learned" if model_acc > baseline + 0.02 else "FAILED")
Output
BASELINE: 0.5056
MODEL   : 0.7778
Gap     : +0.2722

Verdict: PASSED, the model really learned

On imbalanced data the baseline fools you

Above, the target was 50/50, so the baseline was low (0.5) and easy to beat.

Now let's look at another dataset: whether students graduate.

Column names in talaba_bitirdi.csv

This synthetic student dataset also has Uzbek column names: kirish_bali entrance score, davomat_foiz attendance %, uy_vazifa_bali homework score, ishlaydi has a job (0/1), masofa_km distance from home in km. The target bitirdi means graduated.

python14 lines
td = pd.read_csv(DATA / "talaba_bitirdi.csv", keep_default_na=False, na_values=[""])
feats = ["kirish_bali", "davomat_foiz", "uy_vazifa_bali", "ishlaydi", "masofa_km"]
Xt, yt = td[feats], td["bitirdi"]
Xt_tr, Xt_te, yt_tr, yt_te = train_test_split(Xt, yt, test_size=0.3, random_state=42, stratify=yt)

baseline_t = accuracy_score(yt_te, DummyClassifier(strategy="most_frequent")
                            .fit(Xt_tr, yt_tr).predict(Xt_te))
model_t = LogisticRegression(max_iter=2000).fit(Xt_tr, yt_tr)
acc_t = accuracy_score(yt_te, model_t.predict(Xt_te))

print(f"Target ratio : {yt.mean():.1%} graduated")
print(f"BASELINE     : {baseline_t:.4f}")
print(f"MODEL        : {acc_t:.4f}")
print(f"Gap          : {acc_t - baseline_t:+.4f}")
Output
Target ratio : 88.6% graduated
BASELINE     : 0.8852
MODEL        : 0.8963
Gap          : +0.0111

This is the trap your student falls into.

The model gives 0.8963. You could write "almost 90 percent!" in a report. But the baseline is 0.8852, so the model's entire contribution is 0.011: three rows out of 270.

When the baseline is high, drop accuracy

If the baseline is above 0.85, using accuracy as your measure is almost meaningless, because the noise is at the same level.

What to do: switch to F1, recall or PR-AUC. Chapter 9 is about this.

python7 lines
print("The same model, with other metrics:")
b = model_t.predict(Xt_te)
print(f"  Accuracy : {accuracy_score(yt_te, b):.4f}   (baseline {baseline_t:.4f}, so little gain)")
print(f"  F1       : {f1_score(yt_te, b):.4f}")
from sklearn.metrics import recall_score, precision_score
print(f"  Precision: {precision_score(yt_te, b, zero_division=0):.4f}")
print(f"  Recall   : {recall_score(yt_te, b, zero_division=0):.4f}")
Output
The same model, with other metrics:
  Accuracy : 0.8963   (baseline 0.8852, so little gain)
  F1       : 0.9447
  Precision: 0.8951
  Recall   : 1.0000

The baseline in regression

For regression there is DummyRegressor. It always predicts the mean (or the median).

python22 lines
from sklearn.dummy import DummyRegressor
from sklearn.linear_model import Ridge
from sklearn.metrics import root_mean_squared_error, r2_score

yn = df["narx_usd"]
Xn_tr, Xn_te, yn_tr, yn_te = train_test_split(X, yn, test_size=0.3, random_state=42)

d_reg = DummyRegressor(strategy="mean").fit(Xn_tr[num], yn_tr)
rmse_baseline = root_mean_squared_error(yn_te, d_reg.predict(Xn_te[num]))

pipe_reg = Pipeline([
    ("prep", ColumnTransformer([
        ("num", Pipeline([("impute", SimpleImputer(strategy="median")),
                          ("scale", StandardScaler())]), num),
        ("cat", OneHotEncoder(handle_unknown="ignore"), ["tuman"]),
    ])),
    ("model", Ridge()),
]).fit(Xn_tr, yn_tr)
pred_reg = pipe_reg.predict(Xn_te)

print(f"BASELINE (always the mean): RMSE = {rmse_baseline:.1f}  R2 = {r2_score(yn_te, d_reg.predict(Xn_te[num])):.4f}")
print(f"MODEL (Ridge)             : RMSE = {root_mean_squared_error(yn_te, pred_reg):.1f}  R2 = {r2_score(yn_te, pred_reg):.4f}")
Output
BASELINE (always the mean): RMSE = 228.6  R2 = -0.0044
MODEL (Ridge)             : RMSE = 71.2  R2 = 0.9025

R2 already has the baseline built in

R2 = 0 means "the same as predicting the mean". R2 < 0 means "worse than the mean".

So in regression R2 already measures against the baseline. But comparing RMSE with the baseline is useful: it answers how much better in business language.

The business baseline: the most important one

The dummy model is the statistical baseline. But a real project has a third baseline, and it matters most:

Who or what does this job right now?

At the agency, an experienced employee guesses "this flat will go fast" and is right 70% of the time. Your model gives 0.7778.

You are well above the statistical baseline (0.5056). But above the business baseline (0.70) by only 0.08. That second number is the one the manager cares about.

python8 lines
show(pd.DataFrame([
    {"Baseline": "Dummy (most_frequent)", "What it shows": "Whether the model learned anything at all",
     "Who asks": "You"},
    {"Baseline": "A simple rule (if price < X)", "What it shows": "Whether ML's complexity is needed",
     "Who asks": "A senior engineer"},
    {"Baseline": "The current process (a person/old system)", "What it shows": "Whether the project pays off",
     "Who asks": "The manager: the most important question"},
]))
BaselineWhat it showsWho asks
Dummy (most_frequent)Whether the model learned anything at allYou
A simple rule (if price < X)Whether ML's complexity is neededA senior engineer
The current process (a person/old system)Whether the project pays offThe manager: the most important question

Let's try a simple rule too; sometimes it makes ML unnecessary.

python8 lines
import numpy as np
cutoff = X_tr["maydon_m2"].median()
rule = (X_te["maydon_m2"] < cutoff).astype(int)   # a guess that smaller flats go faster
print(f"Rule: if area < {cutoff:.0f} m2, 'goes fast'")
print(f"  Accuracy: {accuracy_score(y_te, rule):.4f}")
print(f"  F1      : {f1_score(y_te, rule, zero_division=0):.4f}")
print()
print(f"Comparison -> dummy {baseline:.4f} | rule {accuracy_score(y_te, rule):.4f} | model {model_acc:.4f}")
Output
Rule: if area < 55 m2, 'goes fast'
  Accuracy: 0.7694
  F1      : 0.7701

Comparison -> dummy 0.5056 | rule 0.7694 | model 0.7778

What we learned today

python8 lines
show(pd.DataFrame([
    {"Concept": "Why a baseline", "In one sentence": "Without it, an accuracy number means nothing"},
    {"Concept": "DummyClassifier", "In one sentence": "most_frequent is the strictest pass mark"},
    {"Concept": "DummyRegressor", "In one sentence": "Always the mean; that is the point where R2=0"},
    {"Concept": "A high baseline", "In one sentence": "Above 0.85, drop accuracy and move to F1"},
    {"Concept": "The simple-rule baseline", "In one sentence": "Answers whether you need ML at all"},
    {"Concept": "The business baseline", "In one sentence": "Who does this now, and how well"},
]))
ConceptIn one sentence
Why a baselineWithout it, an accuracy number means nothing
DummyClassifiermost_frequent is the strictest pass mark
DummyRegressorAlways the mean; that is the point where R2=0
A high baselineAbove 0.85, drop accuracy and move to F1
The simple-rule baselineAnswers whether you need ML at all
The business baselineWho does this now, and how well

Next chapter: accuracy throws two very different mistakes into one number. We take it apart into the four cells of the confusion matrix.

Questions about this chapter?

The course, homework review and the other five books are in Uzbek. Questions in English are welcome on Telegram.

Write on Telegram Contents