Dictionary of Applied Machine Learning · baseline

baseline — Python demo

Numerical companion to the entry baseline: it recomputes what the entry states and prints one line per check

Numerical companion to the glossary entry 'baseline'. The GeoSphere Austria weather station Krems (station id 3805, 48.42 N, 15.62 E) records the minimum and maximum air temperature of each day; this script downloads the records for all 366 days of 2024 from the GeoSphere data hub (dataset klima-v2-1d) and writes them to baseline_weather.csv, so the exact numbers behind the figure stay on record. Checks pin the downloaded values to the 2024 archive, so a change on the server side is caught rather than silently absorbed.

Run it with python3 baseline.py, from any directory — it writes its output files into the current directory. Requires NumPy and Matplotlib only, and uses fixed seeds, so the printed numbers reproduce exactly. Download baseline.py · Notebook · Open in Colab

The script, block by block

One cell per block of the script: the code, and what that code printed when it last ran here

setup

"""A fundamental limit for the achievable loss, read off a weather record:
daily temperatures at Krems an der Donau, 2024.

Purpose
-------
Numerical companion to the glossary entry 'baseline'.  The GeoSphere
Austria weather station Krems (station id 3805, 48.42 N, 15.62 E)
records the minimum and maximum air temperature of each day; this
script downloads the records for all 366 days of 2024 from the
GeoSphere data hub (dataset klima-v2-1d) and writes them to
baseline_weather.csv, so the exact numbers behind the figure stay on
record.  Checks pin the downloaded values to the 2024 archive, so a
change on the server side is caught rather than silently absorbed.

Each day is one data point: its feature is the minimum temperature of
the day, its label the maximum temperature.  The 15 days whose minimum
temperature lies between 11.5 and 12.5 degrees C carry maximum
temperatures from 13.2 to 33.4 degrees C: for (almost) the same
feature value, the label spreads over 20 degrees.  Every hypothesis
maps (almost) the same feature value to (almost) the same prediction,
so this spread bounds the achievable average loss from below.  The
band-wise variance of the label, averaged over all 1-degree bands with
at least five days, estimates that bound as 19.34 (squared degrees C);
a hypothesis learned by linear regression reaches an average squared
error loss of 19.17 on the 366 days -- within one percent of the
estimated bound, so the learned hypothesis is already close to
optimal.  That comparison against a baseline is exactly what the entry
is about.

Deterministic: no randomness (the linear hypothesis is the closed-form
least-squares solution).  Self-contained: numpy + matplotlib only
(stdlib urllib for the download).

Blocks
------
[B-fetch]  Download the daily minimum and maximum temperature at Krems
           for 2024 from the GeoSphere data hub; write the 366 records
           to baseline_weather.csv and check them against the archive.
[B-spread] The 15 days with minimum temperature in [11.5, 12.5): their
           maximum temperatures range from 13.2 to 33.4 degrees C, and
           two of them share the exact feature value 11.8 with labels
           13.2 and 29.9 -- no hypothesis can predict both correctly.
           Write the band days and the remaining days to separate CSVs.
[B-limit]  Estimate the smallest achievable average squared error loss
           by the band-wise variance of the label (19.34); learn a
           linear hypothesis and check that its average squared error
           loss (19.17) is within one percent of that estimate.

Outputs
-------
baseline_weather.csv : date, tmin, tmax of the 366 downloaded days
baseline_band.csv    : tmin, tmax of the 15 days in the band [11.5, 12.5)
baseline_points.csv  : tmin, tmax of the remaining 351 days
baseline_fit.csv     : tmin, tmax along the learned linear hypothesis
baseline.png         : preview (checking only) -- the 366 data points,
                       the band, its label spread, and the learned
                       linear hypothesis
"""

import json
import urllib.request
from pathlib import Path

import numpy as np
import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt

OUT_DIR = Path(__file__).parent

report = []                         # collects (check name, pass/fail) pairs


def check(name, ok):                # records and prints one verification
    report.append((name, bool(ok)))
    print(f"  [{'ok' if ok else 'FAIL'}] {name}")

B-fetch

Download the daily minimum and maximum temperature at Krems for 2024 from the GeoSphere data hub; write the 366 records to baseline_weather.csv and check them against the archive.

URL = ("https://dataset.api.hub.geosphere.at/v1/station/historical/"
       "klima-v2-1d?parameters=tlmin,tlmax&station_ids=3805"
       "&start=2024-01-01&end=2024-12-31")
with urllib.request.urlopen(URL, timeout=120) as resp:
    payload = json.load(resp)
params = payload["features"][0]["properties"]["parameters"]
records = [(stamp[:10], lo, hi)
           for stamp, lo, hi in zip(payload["timestamps"],
                                    params["tlmin"]["data"],
                                    params["tlmax"]["data"])]
with open(OUT_DIR / "baseline_weather.csv", "w") as f:
    f.write("date,tmin,tmax\n")
    for day, lo, hi in records:
        f.write(f"{day},{lo},{hi}\n")
check("[B-fetch] 366 days downloaded for 2024", len(records) == 366)
check("[B-fetch] no day is missing a temperature",
      all(lo is not None and hi is not None for _, lo, hi in records))
check("[B-fetch] the record matches the archive (Jan 1: 2.6 to 9.6)",
      records[0] == ("2024-01-01", 2.6, 9.6))
  [ok] [B-fetch] 366 days downloaded for 2024
  [ok] [B-fetch] no day is missing a temperature
  [ok] [B-fetch] the record matches the archive (Jan 1: 2.6 to 9.6)

B-spread

The 15 days with minimum temperature in [11.5, 12.5): their maximum temperatures range from 13.2 to 33.4 degrees C, and two of them share the exact feature value 11.8 with labels 13.2 and 29.9 -- no hypothesis can predict both correctly. Write the band days and the remaining days to separate CSVs.

temps = np.array([[lo, hi] for _, lo, hi in records])
tmin, tmax = temps[:, 0], temps[:, 1]
band = (tmin >= 11.5) & (tmin < 12.5)

check("[B-spread] 15 days have their minimum temperature in [11.5, 12.5)",
      int(band.sum()) == 15)
check("[B-spread] their maximum temperatures range from 13.2 to 33.4",
      round(float(tmax[band].min()), 1) == 13.2
      and round(float(tmax[band].max()), 1) == 33.4)
check("[B-spread] two days share the feature value 11.8, labels 13.2 / 29.9",
      sorted(tmax[tmin == 11.8].tolist()) == [13.2, 29.9])

header = "tmin,tmax"
np.savetxt(OUT_DIR / "baseline_band.csv", temps[band], delimiter=",",
           header=header, comments="", fmt="%.1f")
np.savetxt(OUT_DIR / "baseline_points.csv", temps[~band], delimiter=",",
           header=header, comments="", fmt="%.1f")
  [ok] [B-spread] 15 days have their minimum temperature in [11.5, 12.5)
  [ok] [B-spread] their maximum temperatures range from 13.2 to 33.4
  [ok] [B-spread] two days share the feature value 11.8, labels 13.2 / 29.9
  learned hypothesis: tmax = 1.099 * tmin + 10.022
  average squared error loss 19.17, estimated bound 19.34 (over 349 days)

B-limit

Estimate the smallest achievable average squared error loss by the band-wise variance of the label (19.34); learn a linear hypothesis and check that its average squared error loss (19.17) is within one percent of that estimate.

floor_sum, floor_days = 0.0, 0      # bound estimate: band-wise variance
for center in range(-10, 25):
    in_band = (tmin >= center - 0.5) & (tmin < center + 0.5)
    if int(in_band.sum()) >= 5:
        floor_sum += float(tmax[in_band].var(ddof=1)) * int(in_band.sum())
        floor_days += int(in_band.sum())
floor = floor_sum / floor_days

A = np.stack([tmin, np.ones(len(tmin))], 1)
w, *_ = np.linalg.lstsq(A, tmax, rcond=None)
mse = float(np.mean((tmax - A @ w) ** 2))
print(f"  learned hypothesis: tmax = {w[0]:.3f} * tmin + {w[1]:.3f}")
print(f"  average squared error loss {mse:.2f}, "
      f"estimated bound {floor:.2f} (over {floor_days} days)")

check("[B-limit] the bound estimate is 19.34 squared degrees C",
      round(floor, 2) == 19.34)
check("[B-limit] the learned hypothesis reaches average loss 19.17",
      round(mse, 2) == 19.17)
check("[B-limit] the achieved loss is within one percent of the bound",
      abs(mse - floor) / floor < 0.01)

t_line = np.array([tmin.min(), tmin.max()])
np.savetxt(OUT_DIR / "baseline_fit.csv",
           np.stack([t_line, w[0] * t_line + w[1]], 1), delimiter=",",
           header=header, comments="", fmt="%.3f")

# ---- outputs: preview PNG (checking only)
fig, ax = plt.subplots(figsize=(6.4, 4.2))
ax.plot(tmin[~band], tmax[~band], "o", color="gray", ms=3, mew=0,
        alpha=0.6, label="351 other days")
ax.plot(tmin[band], tmax[band], "^", mfc="none", mec="tab:red", ms=7,
        mew=1.5, label="15 days with tmin in [11.5, 12.5)")
ax.plot(t_line, w[0] * t_line + w[1], "k-", lw=1.5,
        label="hypothesis learned by linear regression")
ax.annotate("", xy=(12.0, 33.4), xytext=(12.0, 13.2),
            arrowprops=dict(arrowstyle="<->", lw=1.2))
ax.text(13.5, 21.0, "spread of 20.2 °C\nat the same feature value",
        fontsize=8)
ax.set_xlabel("minimum temperature of the day (feature, °C)")
ax.set_ylabel("maximum temperature of the day (label, °C)")
ax.set_title("366 days at Krems, 2024: the label spread limits the loss")
ax.legend(frameon=False, fontsize=8, loc="upper left")
fig.tight_layout()
fig.savefig(OUT_DIR / "baseline.png", dpi=150)

failed = [name for name, ok in report if not ok]
print(f"{len(report) - len(failed)}/{len(report)} checks passed"
      + (f", FAILED: {failed}" if failed else ""))
  [ok] [B-limit] the bound estimate is 19.34 squared degrees C
  [ok] [B-limit] the learned hypothesis reaches average loss 19.17
  [ok] [B-limit] the achieved loss is within one percent of the bound
9/9 checks passed
Preview figure produced by baseline.py
The preview figure the block B-limit writes when the script runs