{
  "cells": [
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "# 2. Tänane Stockholmi temperatuur\n",
        "**Eesmärk:** kasuta registrist loetud mudelit ja küsi sama sisendiga prognoos ka veebiteenuselt.\n",
        "Treeningut pole vaja korrata. Enne peab esimene notebook olema lõpetatud ja Serving-teenus olekus **Ready** ehk valmis.\n",
        "Kasuta samu Environment-sõltuvusi nagu esimeses notebookis. Käivita lahtrid järjekorras.\n",
        "**Serverless** tähendab, et Databricks haldab arvutit sinu eest. **Endpoint** tähendab siin aadressiga mudeliteenust."
      ],
      "id": "cell-000"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "execution_count": null,
      "outputs": [],
      "source": [
        "import json, re, hashlib\n",
        "from pathlib import Path\n",
        "from datetime import datetime, timedelta\n",
        "from zoneinfo import ZoneInfo\n",
        "import numpy as np\n",
        "import pandas as pd\n",
        "import sklearn, mlflow\n",
        "from mlflow import MlflowClient\n",
        "from mlflow.models import infer_signature\n",
        "from sklearn.neural_network import MLPRegressor\n",
        "from sklearn.preprocessing import StandardScaler\n",
        "from sklearn.pipeline import Pipeline\n",
        "from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score\n",
        "\n",
        "mlflow.set_tracking_uri(\"databricks\")\n",
        "mlflow.set_registry_uri(\"databricks-uc\")\n",
        "mlflow.autolog(disable=True)\n",
        "print(\"MLflow:\", mlflow.__version__, \"| scikit-learn:\", sklearn.__version__)"
      ],
      "id": "cell-001"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 1. Failikaust ja mudel\n",
        "Muuda `VOLUME` oma volume'i teeks, samaks nagu esimeses notebookis.\n",
        "`projekt.json` sisaldab mudeli ja teenuse nime, mitte paroole. Mudeli nime kolm osa on kataloog, skeem ja mudelinimi."
      ],
      "id": "cell-002"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "execution_count": null,
      "outputs": [],
      "source": [
        "# MUUDA ainult seda teed: Catalog → sinu volume → Copy path.\n",
        "VOLUME = Path(\"/Volumes/workspace/ilm_anna/andmed\")\n",
        "if not VOLUME.is_dir():\n",
        "    raise ValueError(\"Seda failikausta ei leitud. Kopeeri oma volume'i tee VOLUME väärtuseks.\")\n",
        "osad = VOLUME.parts\n",
        "if len(osad) != 5 or osad[1] != \"Volumes\":\n",
        "    raise ValueError(\"Kasuta volume'i juurkausta: /Volumes/kataloog/skeem/volume\")\n",
        "CATALOG, SCHEMA = osad[2], osad[3]\n",
        "MODEL_NAME = f\"{CATALOG}.{SCHEMA}.stockholmi_ilm\"\n",
        "ENDPOINT = f\"stockholmi-ilm-{SCHEMA.replace('_','-')}\"\n",
        "print(\"Mudeli nimi:\", MODEL_NAME)\n",
        "print(\"Teenuse soovituslik nimi:\", ENDPOINT)"
      ],
      "id": "cell-003"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "execution_count": null,
      "outputs": [],
      "source": [
        "projekt = json.loads((VOLUME/\"projekt.json\").read_text(encoding=\"utf-8\"))\n",
        "MODEL_NAME = projekt[\"model_name\"]\n",
        "ENDPOINT = projekt[\"endpoint\"]\n",
        "client = MlflowClient()\n",
        "version = client.get_model_version_by_alias(MODEL_NAME, \"champion\")\n",
        "MODEL_URI = f\"models:/{MODEL_NAME}/{version.version}\"\n",
        "laetud = mlflow.pyfunc.load_model(MODEL_URI)\n",
        "meta_fail = mlflow.artifacts.download_artifacts(run_id=version.run_id, artifact_path=\"tunnused.json\")\n",
        "meta = json.loads(Path(meta_fail).read_text(encoding=\"utf-8\"))\n",
        "tunnused = meta[\"features\"]\n",
        "print(\"Kasutan mudelit:\", MODEL_NAME, \"versioon\", version.version)\n",
        "print(\"Serving-teenuses peab olema valitud SAMA mudeliversioon.\")"
      ],
      "id": "cell-004"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 2. Kuidas ilmaandmeid küsime?\n",
        "**API** on kokkulepitud viis programmide vahel andmeid küsida. **Päring** on küsimus teenusele, **vastus** sisaldab küsitud andmeid.\n",
        "`requests.get` küsib Open-Meteo API-lt andmeid. `timeout` piirab ootamise aega; `raise_for_status` peatab töö, kui teenus vastab veaga.\n",
        "`latitude` ja `longitude` on laius- ja pikkuskraad. `timezone` on ajavöönd. Kasutame `Europe/Stockholm`, et „täna“ tähendaks Stockholmi päeva.\n",
        "\n",
        "Tunnuse `day-0` väärtus tuleb **eilsest**, `day-1` üleeilsest jne. Tänast Open-Meteo prognoosi mudelile sisendiks ei anta.\n",
        "Järgmised funktsioonid küsivad ilma ja seavad veerud treenimisega samasse järjekorda. Kuupäevi kontrollitakse nimeliselt, mitte lihtsalt loendi järjekorra järgi."
      ],
      "id": "cell-005"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "execution_count": null,
      "outputs": [],
      "source": [
        "def fetch_weather(coordinates, lags=4, today=None):\n",
        "    \"\"\"Obtain city-keyed Open-Meteo response for notebook use.\"\"\"\n",
        "    import requests\n",
        "    today = today or datetime.now(ZoneInfo('Europe/Stockholm')).date()\n",
        "    cities = coordinates['linn'].tolist()\n",
        "    response = requests.get('https://api.open-meteo.com/v1/forecast', params={\n",
        "        'latitude': ','.join(map(str,coordinates['lat'])),\n",
        "        'longitude': ','.join(map(str,coordinates['lon'])),\n",
        "        'daily': 'temperature_2m_mean,temperature_2m_max,temperature_2m_min',\n",
        "        'start_date': (today-timedelta(days=lags)).isoformat(),\n",
        "        'end_date': today.isoformat(), 'timezone':'Europe/Stockholm',\n",
        "        'temperature_unit':'celsius',\n",
        "    }, timeout=60)\n",
        "    response.raise_for_status()\n",
        "    data = response.json()\n",
        "    if isinstance(data,dict): data=[data]\n",
        "    if len(data)!=len(cities):\n",
        "        raise ValueError('Ilmateenuse vastuses on vale arv linnu.')\n",
        "    return {'target_date':today.isoformat(), 'timezone':'Europe/Stockholm',\n",
        "            'cities':dict(zip(cities,data)), 'provider':'Open-Meteo'}"
      ],
      "id": "cell-006"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "execution_count": null,
      "outputs": [],
      "source": [
        "def make_weather_input(payload, features, today=None):\n",
        "    \"\"\"Match dates explicitly; never use today's forecast as a model input.\"\"\"\n",
        "    today = today or datetime.now(ZoneInfo('Europe/Stockholm')).date()\n",
        "    if payload.get('target_date') != today.isoformat():\n",
        "        raise ValueError('Ilmafail ei ole tänase päeva jaoks. Laadi värske fail või tee uus päring.')\n",
        "    mapping = {'temp_mean':'temperature_2m_mean', 'temp_max':'temperature_2m_max', 'temp_min':'temperature_2m_min'}\n",
        "    row = {}\n",
        "    for feature in features:\n",
        "        match = re.fullmatch(r'(.+)_(temp_mean|temp_max|temp_min)_day-(\\d+)', feature)\n",
        "        if not match:\n",
        "            raise ValueError(f'Tundmatu tunnus: {feature}. Kasuta temperatuuritunnuseid.')\n",
        "        city, measure, lag = match.groups()\n",
        "        wanted = (today - timedelta(days=1+int(lag))).isoformat()\n",
        "        try:\n",
        "            daily = payload['cities'][city]['daily']\n",
        "            if daily.get('time', []).count(wanted) != 1:\n",
        "                raise ValueError('Kuupäev puudub või kordub.')\n",
        "            position = daily['time'].index(wanted)\n",
        "            value = daily[mapping[measure]][position]\n",
        "            if value is None or not np.isfinite(float(value)):\n",
        "                raise ValueError('Puuduv temperatuur.')\n",
        "            row[feature] = float(value)\n",
        "        except (KeyError, ValueError, IndexError, TypeError) as exc:\n",
        "            raise ValueError(f'Puuduvad sobivad andmed: {city}, {wanted}, {measure}.') from exc\n",
        "    X = pd.DataFrame([row], columns=features).astype(float)\n",
        "    daily = payload['cities']['STOCKHOLM']['daily']\n",
        "    try:\n",
        "        value = daily['temperature_2m_mean'][daily['time'].index(today.isoformat())]\n",
        "        reference = float(value) if value is not None else None\n",
        "    except (KeyError, ValueError, IndexError):\n",
        "        reference = None\n",
        "    return X, reference"
      ],
      "id": "cell-007"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 3. Võta värsked andmed\n",
        "Esmalt jäta `KASUTA_FAILI = False` ja käivita lahter.\n",
        "Kui väline ühendus on keelatud: ava oma arvutis `laadi_ilm.html`, laadi andmed alla ja laadi saadud `open_meteo_ilm.json` oma Databricksi volume'i. Pane allpool `True` ja käivita see lahter uuesti.\n",
        "`False` tähendab „ei“, `True` tähendab „jah“. Vana kuupäevaga faili ei kasutata tänase prognoosina."
      ],
      "id": "cell-008"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "execution_count": null,
      "outputs": [],
      "source": [
        "KASUTA_FAILI = False\n",
        "if KASUTA_FAILI:\n",
        "    payload = json.loads((VOLUME/\"open_meteo_ilm.json\").read_text(encoding=\"utf-8\"))\n",
        "else:\n",
        "    payload = fetch_weather(pd.DataFrame(meta[\"coordinates\"]), lags=meta[\"lags\"])\n",
        "X_tana, open_meteo_prognoos = make_weather_input(payload, tunnused)\n",
        "display(X_tana)\n",
        "print(\"Ennustame päeva:\", payload[\"target_date\"])\n",
        "print(\"Open-Meteo prognoos on ainult võrdluseks:\", open_meteo_prognoos)"
      ],
      "id": "cell-009"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 4. Esimene kontroll: mudel notebookis\n",
        "**Ennustamine** ehk inference tähendab õpitud mudeli kasutamist uue sisendiga.\n",
        "See kontroll käivitab mudeli notebookis. Veebiteenuse kontroll on järgmine samm.\n",
        "Open-Meteo tänane arv on samuti prognoos, mitte päeva lõplik mõõdetud temperatuur. Kahe prognoosi erinevus ei ole mudeli tegelik viga."
      ],
      "id": "cell-010"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "execution_count": null,
      "outputs": [],
      "source": [
        "teenuse_prognoos = None  # Vana käivituse vastust ei tohi uue päeva tulemuseks salvestada.\n",
        "oma_prognoos = float(np.asarray(laetud.predict(X_tana)).reshape(-1)[0])\n",
        "print(f\"Meie prognoos {payload['target_date']}: {oma_prognoos:.1f} °C\")\n",
        "print(\"Open-Meteo prognoos:\", open_meteo_prognoos, \"°C\")\n",
        "paring = {\"dataframe_split\":{\"columns\":tunnused,\"data\":X_tana.values.tolist()}}\n",
        "(VOLUME/\"tanane_paring.json\").write_text(json.dumps(paring,ensure_ascii=False,indent=2),encoding=\"utf-8\")\n",
        "print(\"Järgmise JSON-teksti saad kopeerida Serving → Query endpoint aknasse:\")\n",
        "print(json.dumps(paring,ensure_ascii=False))"
      ],
      "id": "cell-011"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 5. Küsi sama prognoos veebiteenuselt\n",
        "**SDK** on valmis käskude komplekt teenusega suhtlemiseks. MLflow Deployments SDK saadab siin päringu Databricksi teenusele.\n",
        "`get_deploy_client(\"databricks\")` kasutab notebooki Databricksi ühendust; ära kleebi siia salajast võtit.\n",
        "`endpoint` on sinu loodud teenuse nimi. `predictions` on vastuse prognooside loend.\n",
        "\n",
        "Kui see lahter annab õiguste või sisselogimise vea, kasuta brauseris **Serving → oma teenus → Query endpoint** ning kleebi eelmise lahtri JSON. See on samuti päris teenusepäring. Märgi töö vastuses, kumba viisi kasutasid."
      ],
      "id": "cell-012"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "execution_count": null,
      "outputs": [],
      "source": [
        "import mlflow.deployments\n",
        "teenus = mlflow.deployments.get_deploy_client(\"databricks\")\n",
        "vastus = teenus.predict(endpoint=ENDPOINT, inputs=paring)\n",
        "teenuse_prognoos = float(np.asarray(vastus[\"predictions\"]).reshape(-1)[0])\n",
        "print(\"Teenuse vastus:\", teenuse_prognoos, \"°C\")\n",
        "if not np.isclose(teenuse_prognoos, oma_prognoos, rtol=1e-6, atol=1e-5):\n",
        "    raise ValueError(\"Vastused erinevad. Kontrolli Serving-teenuses mudeli nime ja versiooni.\")\n",
        "print(\"Kontroll korras: teenuse ja notebooki prognoosid klapivad.\")"
      ],
      "id": "cell-013"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## 6. Salvesta tulemus\n",
        "Salvestame iga päeva ja versiooni tulemuse eraldi JSON-faili. Sama päeva sama versiooniga uuesti käivitamine asendab selle ühe faili.\n",
        "Käivita lahter alles pärast edukat teenusepäringut. Brauseriga päringu korral kirjuta allpool `TEENUSE_VASTUS_BRAUSERIST` väärtuseks teenuse tegelik arv; ära kirjuta sinna notebooki prognoosi."
      ],
      "id": "cell-014"
    },
    {
      "cell_type": "code",
      "metadata": {},
      "execution_count": null,
      "outputs": [],
      "source": [
        "TEENUSE_VASTUS_BRAUSERIST = None  # Näiteks 5.8, aga ainult sinu päris teenusevastusest.\n",
        "if TEENUSE_VASTUS_BRAUSERIST is not None:\n",
        "    teenuse_prognoos = float(TEENUSE_VASTUS_BRAUSERIST)\n",
        "    if not np.isclose(teenuse_prognoos, oma_prognoos, rtol=1e-6, atol=1e-5):\n",
        "        raise ValueError(\"Brauseri ja notebooki vastused ei klapi. Kontrolli mudeliversiooni.\")\n",
        "if teenuse_prognoos is None:\n",
        "    raise ValueError(\"Küsi kõigepealt vastus teenuselt.\")\n",
        "ajalugu = VOLUME/\"prognoosid\"\n",
        "ajalugu.mkdir(exist_ok=True)\n",
        "fail = ajalugu/f\"{payload['target_date']}_v{version.version}.json\"\n",
        "fail.write_text(json.dumps({\"date\":payload[\"target_date\"],\"model_name\":MODEL_NAME,\n",
        "    \"version\":str(version.version),\"prediction_celsius\":teenuse_prognoos,\n",
        "    \"open_meteo_forecast_celsius\":open_meteo_prognoos,\n",
        "    \"created_at\":datetime.now(ZoneInfo(\"Europe/Stockholm\")).isoformat()},indent=2),encoding=\"utf-8\")\n",
        "print(\"Salvestatud:\", fail)"
      ],
      "id": "cell-015"
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "source": [
        "## Valmis\n",
        "Esita katsete võrdlus, valitud versioon, lõpliku kontrolli mõõdikud ja teenuse vastus.\n",
        "Selgita, miks Open-Meteo ja oma mudeli prognoosi vahe ei ole tegelik ennustusviga.\n",
        "Ära treeni iga hommik automaatselt uut mudelit: igapäevaseks ennustamiseks piisab sellest notebookist.\n",
        "Kui harjutus on läbi, kustuta ainult enda loodud Serving endpoint. Andmefailid ja register võivad alles jääda."
      ],
      "id": "cell-016"
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "name": "python"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 5
}