{
 "nbformat": 4,
 "nbformat_minor": 5,
 "metadata": {
  "kernelspec": {
   "name": "python3",
   "display_name": "Python 3",
   "language": "python"
  },
  "language_info": {
   "name": "python"
  },
  "colab": {
   "name": "baseline.ipynb"
  }
 },
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "# baseline \u2014 Python demo\n\nNumerical companion to the entry [baseline](https://dictionaryofml.org/terms/baseline.html) of the [Dictionary of Applied Machine Learning](https://dictionaryofml.org/): it recomputes what the entry states and prints one line per check.\n\nNumerical 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.\n\nRequires NumPy and Matplotlib only, and uses fixed seeds, so the printed numbers reproduce exactly. Generated from [`pythondemos/baseline.py`](https://dictionaryofml.org/terms/baseline.py); CC BY 4.0."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "# Notebook shim: the script resolves output paths relative to __file__,\n# which a notebook kernel does not define; everything lands in the\n# working directory instead.\nimport os\n__file__ = os.path.join(os.getcwd(), \"baseline.py\")\nos.makedirs(\"pythondemos\", exist_ok=True)"
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "\"\"\"A fundamental limit for the achievable loss, read off a weather record:\ndaily temperatures at Krems an der Donau, 2024.\n\nPurpose\n-------\nNumerical companion to the glossary entry 'baseline'.  The GeoSphere\nAustria weather station Krems (station id 3805, 48.42 N, 15.62 E)\nrecords the minimum and maximum air temperature of each day; this\nscript downloads the records for all 366 days of 2024 from the\nGeoSphere data hub (dataset klima-v2-1d) and writes them to\nbaseline_weather.csv, so the exact numbers behind the figure stay on\nrecord.  Checks pin the downloaded values to the 2024 archive, so a\nchange on the server side is caught rather than silently absorbed.\n\nEach day is one data point: its feature is the minimum temperature of\nthe day, its label the maximum temperature.  The 15 days whose minimum\ntemperature lies between 11.5 and 12.5 degrees C carry maximum\ntemperatures from 13.2 to 33.4 degrees C: for (almost) the same\nfeature value, the label spreads over 20 degrees.  Every hypothesis\nmaps (almost) the same feature value to (almost) the same prediction,\nso this spread bounds the achievable average loss from below.  The\nband-wise variance of the label, averaged over all 1-degree bands with\nat least five days, estimates that bound as 19.34 (squared degrees C);\na hypothesis learned by linear regression reaches an average squared\nerror loss of 19.17 on the 366 days -- within one percent of the\nestimated bound, so the learned hypothesis is already close to\noptimal.  That comparison against a baseline is exactly what the entry\nis about.\n\nDeterministic: no randomness (the linear hypothesis is the closed-form\nleast-squares solution).  Self-contained: numpy + matplotlib only\n(stdlib urllib for the download).\n\nBlocks\n------\n[B-fetch]  Download the daily minimum and maximum temperature at Krems\n           for 2024 from the GeoSphere data hub; write the 366 records\n           to baseline_weather.csv and check them against the archive.\n[B-spread] The 15 days with minimum temperature in [11.5, 12.5): their\n           maximum temperatures range from 13.2 to 33.4 degrees C, and\n           two of them share the exact feature value 11.8 with labels\n           13.2 and 29.9 -- no hypothesis can predict both correctly.\n           Write the band days and the remaining days to separate CSVs.\n[B-limit]  Estimate the smallest achievable average squared error loss\n           by the band-wise variance of the label (19.34); learn a\n           linear hypothesis and check that its average squared error\n           loss (19.17) is within one percent of that estimate.\n\nOutputs\n-------\nbaseline_weather.csv : date, tmin, tmax of the 366 downloaded days\nbaseline_band.csv    : tmin, tmax of the 15 days in the band [11.5, 12.5)\nbaseline_points.csv  : tmin, tmax of the remaining 351 days\nbaseline_fit.csv     : tmin, tmax along the learned linear hypothesis\nbaseline.png         : preview (checking only) -- the 366 data points,\n                       the band, its label spread, and the learned\n                       linear hypothesis\n\"\"\"\n\nimport json\nimport urllib.request\nfrom pathlib import Path\n\nimport numpy as np\nimport matplotlib\n\nmatplotlib.use(\"Agg\")\nimport matplotlib.pyplot as plt\n\nOUT_DIR = Path(__file__).parent\n\nreport = []                         # collects (check name, pass/fail) pairs\n\n\ndef check(name, ok):                # records and prints one verification\n    report.append((name, bool(ok)))\n    print(f\"  [{'ok' if ok else 'FAIL'}] {name}\")"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "**[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."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "URL = (\"https://dataset.api.hub.geosphere.at/v1/station/historical/\"\n       \"klima-v2-1d?parameters=tlmin,tlmax&station_ids=3805\"\n       \"&start=2024-01-01&end=2024-12-31\")\nwith urllib.request.urlopen(URL, timeout=120) as resp:\n    payload = json.load(resp)\nparams = payload[\"features\"][0][\"properties\"][\"parameters\"]\nrecords = [(stamp[:10], lo, hi)\n           for stamp, lo, hi in zip(payload[\"timestamps\"],\n                                    params[\"tlmin\"][\"data\"],\n                                    params[\"tlmax\"][\"data\"])]\nwith open(OUT_DIR / \"baseline_weather.csv\", \"w\") as f:\n    f.write(\"date,tmin,tmax\\n\")\n    for day, lo, hi in records:\n        f.write(f\"{day},{lo},{hi}\\n\")\ncheck(\"[B-fetch] 366 days downloaded for 2024\", len(records) == 366)\ncheck(\"[B-fetch] no day is missing a temperature\",\n      all(lo is not None and hi is not None for _, lo, hi in records))\ncheck(\"[B-fetch] the record matches the archive (Jan 1: 2.6 to 9.6)\",\n      records[0] == (\"2024-01-01\", 2.6, 9.6))"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "**[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."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "temps = np.array([[lo, hi] for _, lo, hi in records])\ntmin, tmax = temps[:, 0], temps[:, 1]\nband = (tmin >= 11.5) & (tmin < 12.5)\n\ncheck(\"[B-spread] 15 days have their minimum temperature in [11.5, 12.5)\",\n      int(band.sum()) == 15)\ncheck(\"[B-spread] their maximum temperatures range from 13.2 to 33.4\",\n      round(float(tmax[band].min()), 1) == 13.2\n      and round(float(tmax[band].max()), 1) == 33.4)\ncheck(\"[B-spread] two days share the feature value 11.8, labels 13.2 / 29.9\",\n      sorted(tmax[tmin == 11.8].tolist()) == [13.2, 29.9])\n\nheader = \"tmin,tmax\"\nnp.savetxt(OUT_DIR / \"baseline_band.csv\", temps[band], delimiter=\",\",\n           header=header, comments=\"\", fmt=\"%.1f\")\nnp.savetxt(OUT_DIR / \"baseline_points.csv\", temps[~band], delimiter=\",\",\n           header=header, comments=\"\", fmt=\"%.1f\")"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": "**[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."
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": "floor_sum, floor_days = 0.0, 0      # bound estimate: band-wise variance\nfor center in range(-10, 25):\n    in_band = (tmin >= center - 0.5) & (tmin < center + 0.5)\n    if int(in_band.sum()) >= 5:\n        floor_sum += float(tmax[in_band].var(ddof=1)) * int(in_band.sum())\n        floor_days += int(in_band.sum())\nfloor = floor_sum / floor_days\n\nA = np.stack([tmin, np.ones(len(tmin))], 1)\nw, *_ = np.linalg.lstsq(A, tmax, rcond=None)\nmse = float(np.mean((tmax - A @ w) ** 2))\nprint(f\"  learned hypothesis: tmax = {w[0]:.3f} * tmin + {w[1]:.3f}\")\nprint(f\"  average squared error loss {mse:.2f}, \"\n      f\"estimated bound {floor:.2f} (over {floor_days} days)\")\n\ncheck(\"[B-limit] the bound estimate is 19.34 squared degrees C\",\n      round(floor, 2) == 19.34)\ncheck(\"[B-limit] the learned hypothesis reaches average loss 19.17\",\n      round(mse, 2) == 19.17)\ncheck(\"[B-limit] the achieved loss is within one percent of the bound\",\n      abs(mse - floor) / floor < 0.01)\n\nt_line = np.array([tmin.min(), tmin.max()])\nnp.savetxt(OUT_DIR / \"baseline_fit.csv\",\n           np.stack([t_line, w[0] * t_line + w[1]], 1), delimiter=\",\",\n           header=header, comments=\"\", fmt=\"%.3f\")\n\n# ---- outputs: preview PNG (checking only)\nfig, ax = plt.subplots(figsize=(6.4, 4.2))\nax.plot(tmin[~band], tmax[~band], \"o\", color=\"gray\", ms=3, mew=0,\n        alpha=0.6, label=\"351 other days\")\nax.plot(tmin[band], tmax[band], \"^\", mfc=\"none\", mec=\"tab:red\", ms=7,\n        mew=1.5, label=\"15 days with tmin in [11.5, 12.5)\")\nax.plot(t_line, w[0] * t_line + w[1], \"k-\", lw=1.5,\n        label=\"hypothesis learned by linear regression\")\nax.annotate(\"\", xy=(12.0, 33.4), xytext=(12.0, 13.2),\n            arrowprops=dict(arrowstyle=\"<->\", lw=1.2))\nax.text(13.5, 21.0, \"spread of 20.2 \u00b0C\\nat the same feature value\",\n        fontsize=8)\nax.set_xlabel(\"minimum temperature of the day (feature, \u00b0C)\")\nax.set_ylabel(\"maximum temperature of the day (label, \u00b0C)\")\nax.set_title(\"366 days at Krems, 2024: the label spread limits the loss\")\nax.legend(frameon=False, fontsize=8, loc=\"upper left\")\nfig.tight_layout()\nfig.savefig(OUT_DIR / \"baseline.png\", dpi=150)\n\nfailed = [name for name, ok in report if not ok]\nprint(f\"{len(report) - len(failed)}/{len(report)} checks passed\"\n      + (f\", FAILED: {failed}\" if failed else \"\"))"
  }
 ]
}