{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Hierarchical Time Series\n",
    "\n",
    "Many real-world datasets are naturally organised as trees: a country's electricity consumption breaks into regions, which break into cities.  \n",
    "`HierarchicalTimeSeries` lets you model that structure directly and **aggregate bottom-up** through the tree.\n",
    "\n",
    "| Class | Purpose |\n",
    "| --- | --- |\n",
    "| `HierarchyNode` | A single node — key, level, children, and an optional `TimeSeries` |\n",
    "| `HierarchicalTimeSeries` | The tree container — traversal, aggregation, conversion |\n",
    "| `AggregationMethod` | `SUM`, `MEAN`, `MIN`, `MAX` |"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:37.454950Z",
     "iopub.status.busy": "2026-03-01T13:36:37.454839Z",
     "iopub.status.idle": "2026-03-01T13:36:37.541183Z",
     "shell.execute_reply": "2026-03-01T13:36:37.540669Z"
    }
   },
   "outputs": [
    {
     "ename": "ModuleNotFoundError",
     "evalue": "No module named 'timedatamodel'",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mModuleNotFoundError\u001b[39m                       Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[1]\u001b[39m\u001b[32m, line 5\u001b[39m\n\u001b[32m      1\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mdatetime\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m datetime, timedelta, timezone\n\u001b[32m      3\u001b[39m \u001b[38;5;28;01mimport\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mnumpy\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mas\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mnp\u001b[39;00m\n\u001b[32m----> \u001b[39m\u001b[32m5\u001b[39m \u001b[38;5;28;01mimport\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mtimedatamodel\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mas\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mtdm\u001b[39;00m\n\u001b[32m      7\u001b[39m base = datetime(\u001b[32m2024\u001b[39m, \u001b[32m1\u001b[39m, \u001b[32m15\u001b[39m, tzinfo=timezone.utc)\n\u001b[32m      8\u001b[39m timestamps = [base + timedelta(hours=i) \u001b[38;5;28;01mfor\u001b[39;00m i \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mrange\u001b[39m(\u001b[32m24\u001b[39m)]\n",
      "\u001b[31mModuleNotFoundError\u001b[39m: No module named 'timedatamodel'"
     ]
    }
   ],
   "source": [
    "from datetime import datetime, timedelta, timezone\n",
    "\n",
    "import numpy as np\n",
    "\n",
    "import timedatamodel as tdm\n",
    "\n",
    "base = datetime(2024, 1, 15, tzinfo=timezone.utc)\n",
    "timestamps = [base + timedelta(hours=i) for i in range(24)]\n",
    "rng = np.random.default_rng(42)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Create leaf time series\n",
    "\n",
    "Each leaf in the hierarchy holds a `TimeSeries`. Here we model electricity consumption for five Norwegian cities."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:37.542320Z",
     "iopub.status.busy": "2026-03-01T13:36:37.542241Z",
     "iopub.status.idle": "2026-03-01T13:36:37.554527Z",
     "shell.execute_reply": "2026-03-01T13:36:37.554158Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'rng' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[2]\u001b[39m\u001b[32m, line 13\u001b[39m\n\u001b[32m      3\u001b[39m     noise = rng.normal(\u001b[32m0\u001b[39m, base_mw * \u001b[32m0.05\u001b[39m, \u001b[32m24\u001b[39m)\n\u001b[32m      4\u001b[39m     \u001b[38;5;28;01mreturn\u001b[39;00m tdm.TimeSeries(\n\u001b[32m      5\u001b[39m         tdm.Frequency.PT1H,\n\u001b[32m      6\u001b[39m         timezone=\u001b[33m\"\u001b[39m\u001b[33mEurope/Oslo\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m   (...)\u001b[39m\u001b[32m     10\u001b[39m         unit=\u001b[33m\"\u001b[39m\u001b[33mMW\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m     11\u001b[39m     )\n\u001b[32m---> \u001b[39m\u001b[32m13\u001b[39m ts_oslo = \u001b[43mmake_consumption\u001b[49m\u001b[43m(\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mOslo\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[32;43m500\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[32m     14\u001b[39m ts_bergen = make_consumption(\u001b[33m\"\u001b[39m\u001b[33mBergen\u001b[39m\u001b[33m\"\u001b[39m, \u001b[32m200\u001b[39m)\n\u001b[32m     15\u001b[39m ts_stavanger = make_consumption(\u001b[33m\"\u001b[39m\u001b[33mStavanger\u001b[39m\u001b[33m\"\u001b[39m, \u001b[32m150\u001b[39m)\n",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[2]\u001b[39m\u001b[32m, line 3\u001b[39m, in \u001b[36mmake_consumption\u001b[39m\u001b[34m(name, base_mw)\u001b[39m\n\u001b[32m      1\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mmake_consumption\u001b[39m(name: \u001b[38;5;28mstr\u001b[39m, base_mw: \u001b[38;5;28mfloat\u001b[39m) -> tdm.TimeSeries:\n\u001b[32m      2\u001b[39m     pattern = base_mw * (\u001b[32m1\u001b[39m + \u001b[32m0.3\u001b[39m * np.sin(np.linspace(\u001b[32m0\u001b[39m, \u001b[32m2\u001b[39m * np.pi, \u001b[32m24\u001b[39m)))\n\u001b[32m----> \u001b[39m\u001b[32m3\u001b[39m     noise = \u001b[43mrng\u001b[49m.normal(\u001b[32m0\u001b[39m, base_mw * \u001b[32m0.05\u001b[39m, \u001b[32m24\u001b[39m)\n\u001b[32m      4\u001b[39m     \u001b[38;5;28;01mreturn\u001b[39;00m tdm.TimeSeries(\n\u001b[32m      5\u001b[39m         tdm.Frequency.PT1H,\n\u001b[32m      6\u001b[39m         timezone=\u001b[33m\"\u001b[39m\u001b[33mEurope/Oslo\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m   (...)\u001b[39m\u001b[32m     10\u001b[39m         unit=\u001b[33m\"\u001b[39m\u001b[33mMW\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m     11\u001b[39m     )\n",
      "\u001b[31mNameError\u001b[39m: name 'rng' is not defined"
     ]
    }
   ],
   "source": [
    "def make_consumption(name: str, base_mw: float) -> tdm.TimeSeries:\n",
    "    pattern = base_mw * (1 + 0.3 * np.sin(np.linspace(0, 2 * np.pi, 24)))\n",
    "    noise = rng.normal(0, base_mw * 0.05, 24)\n",
    "    return tdm.TimeSeries(\n",
    "        tdm.Frequency.PT1H,\n",
    "        timezone=\"Europe/Oslo\",\n",
    "        timestamps=timestamps,\n",
    "        values=(pattern + noise).tolist(),\n",
    "        name=name,\n",
    "        unit=\"MW\",\n",
    "    )\n",
    "\n",
    "ts_oslo = make_consumption(\"Oslo\", 500)\n",
    "ts_bergen = make_consumption(\"Bergen\", 200)\n",
    "ts_stavanger = make_consumption(\"Stavanger\", 150)\n",
    "ts_tromsoe = make_consumption(\"Tromsø\", 80)\n",
    "ts_bodoe = make_consumption(\"Bodø\", 50)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Building a hierarchy with HierarchyNode\n",
    "\n",
    "Construct the tree by nesting `HierarchyNode` objects. Leaves hold a `TimeSeries`; interior nodes have `children`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:37.555554Z",
     "iopub.status.busy": "2026-03-01T13:36:37.555498Z",
     "iopub.status.idle": "2026-03-01T13:36:37.564132Z",
     "shell.execute_reply": "2026-03-01T13:36:37.563723Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'tdm' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[3]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m root = \u001b[43mtdm\u001b[49m.HierarchyNode(\n\u001b[32m      2\u001b[39m     key=\u001b[33m\"\u001b[39m\u001b[33mNorway\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      3\u001b[39m     level=\u001b[33m\"\u001b[39m\u001b[33mcountry\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      4\u001b[39m     children=[\n\u001b[32m      5\u001b[39m         tdm.HierarchyNode(\n\u001b[32m      6\u001b[39m             key=\u001b[33m\"\u001b[39m\u001b[33mSouth\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      7\u001b[39m             level=\u001b[33m\"\u001b[39m\u001b[33mregion\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      8\u001b[39m             children=[\n\u001b[32m      9\u001b[39m                 tdm.HierarchyNode(key=\u001b[33m\"\u001b[39m\u001b[33mOslo\u001b[39m\u001b[33m\"\u001b[39m, level=\u001b[33m\"\u001b[39m\u001b[33mcity\u001b[39m\u001b[33m\"\u001b[39m, timeseries=ts_oslo),\n\u001b[32m     10\u001b[39m                 tdm.HierarchyNode(key=\u001b[33m\"\u001b[39m\u001b[33mBergen\u001b[39m\u001b[33m\"\u001b[39m, level=\u001b[33m\"\u001b[39m\u001b[33mcity\u001b[39m\u001b[33m\"\u001b[39m, timeseries=ts_bergen),\n\u001b[32m     11\u001b[39m                 tdm.HierarchyNode(key=\u001b[33m\"\u001b[39m\u001b[33mStavanger\u001b[39m\u001b[33m\"\u001b[39m, level=\u001b[33m\"\u001b[39m\u001b[33mcity\u001b[39m\u001b[33m\"\u001b[39m, timeseries=ts_stavanger),\n\u001b[32m     12\u001b[39m             ],\n\u001b[32m     13\u001b[39m         ),\n\u001b[32m     14\u001b[39m         tdm.HierarchyNode(\n\u001b[32m     15\u001b[39m             key=\u001b[33m\"\u001b[39m\u001b[33mNorth\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m     16\u001b[39m             level=\u001b[33m\"\u001b[39m\u001b[33mregion\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m     17\u001b[39m             children=[\n\u001b[32m     18\u001b[39m                 tdm.HierarchyNode(key=\u001b[33m\"\u001b[39m\u001b[33mTromsø\u001b[39m\u001b[33m\"\u001b[39m, level=\u001b[33m\"\u001b[39m\u001b[33mcity\u001b[39m\u001b[33m\"\u001b[39m, timeseries=ts_tromsoe),\n\u001b[32m     19\u001b[39m                 tdm.HierarchyNode(key=\u001b[33m\"\u001b[39m\u001b[33mBodø\u001b[39m\u001b[33m\"\u001b[39m, level=\u001b[33m\"\u001b[39m\u001b[33mcity\u001b[39m\u001b[33m\"\u001b[39m, timeseries=ts_bodoe),\n\u001b[32m     20\u001b[39m             ],\n\u001b[32m     21\u001b[39m         ),\n\u001b[32m     22\u001b[39m     ],\n\u001b[32m     23\u001b[39m )\n\u001b[32m     25\u001b[39m hierarchy = tdm.HierarchicalTimeSeries(\n\u001b[32m     26\u001b[39m     root,\n\u001b[32m     27\u001b[39m     name=\u001b[33m\"\u001b[39m\u001b[33mNorway Consumption\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m   (...)\u001b[39m\u001b[32m     30\u001b[39m     aggregation=tdm.AggregationMethod.SUM,\n\u001b[32m     31\u001b[39m )\n\u001b[32m     32\u001b[39m hierarchy\n",
      "\u001b[31mNameError\u001b[39m: name 'tdm' is not defined"
     ]
    }
   ],
   "source": [
    "root = tdm.HierarchyNode(\n",
    "    key=\"Norway\",\n",
    "    level=\"country\",\n",
    "    children=[\n",
    "        tdm.HierarchyNode(\n",
    "            key=\"South\",\n",
    "            level=\"region\",\n",
    "            children=[\n",
    "                tdm.HierarchyNode(key=\"Oslo\", level=\"city\", timeseries=ts_oslo),\n",
    "                tdm.HierarchyNode(key=\"Bergen\", level=\"city\", timeseries=ts_bergen),\n",
    "                tdm.HierarchyNode(key=\"Stavanger\", level=\"city\", timeseries=ts_stavanger),\n",
    "            ],\n",
    "        ),\n",
    "        tdm.HierarchyNode(\n",
    "            key=\"North\",\n",
    "            level=\"region\",\n",
    "            children=[\n",
    "                tdm.HierarchyNode(key=\"Tromsø\", level=\"city\", timeseries=ts_tromsoe),\n",
    "                tdm.HierarchyNode(key=\"Bodø\", level=\"city\", timeseries=ts_bodoe),\n",
    "            ],\n",
    "        ),\n",
    "    ],\n",
    ")\n",
    "\n",
    "hierarchy = tdm.HierarchicalTimeSeries(\n",
    "    root,\n",
    "    name=\"Norway Consumption\",\n",
    "    description=\"Hourly electricity consumption by city\",\n",
    "    levels=[\"country\", \"region\", \"city\"],\n",
    "    aggregation=tdm.AggregationMethod.SUM,\n",
    ")\n",
    "hierarchy"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Inspecting the tree\n",
    "\n",
    "Basic properties tell you the shape of the hierarchy."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:37.565052Z",
     "iopub.status.busy": "2026-03-01T13:36:37.564995Z",
     "iopub.status.idle": "2026-03-01T13:36:37.571581Z",
     "shell.execute_reply": "2026-03-01T13:36:37.571163Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'hierarchy' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[4]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mName:     \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[43mhierarchy\u001b[49m.name\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m      2\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mLevels:   \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mhierarchy.levels\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m      3\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33m# levels: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mhierarchy.n_levels\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'hierarchy' is not defined"
     ]
    }
   ],
   "source": [
    "print(f\"Name:     {hierarchy.name}\")\n",
    "print(f\"Levels:   {hierarchy.levels}\")\n",
    "print(f\"# levels: {hierarchy.n_levels}\")\n",
    "print(f\"# nodes:  {hierarchy.n_nodes}\")\n",
    "print(f\"# leaves: {hierarchy.n_leaves}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Navigating the tree\n",
    "\n",
    "`get_node(*path)` walks by key. `get_level(name)` returns all nodes at a given level."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:37.572602Z",
     "iopub.status.busy": "2026-03-01T13:36:37.572538Z",
     "iopub.status.idle": "2026-03-01T13:36:37.579396Z",
     "shell.execute_reply": "2026-03-01T13:36:37.579020Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'hierarchy' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[5]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m south = \u001b[43mhierarchy\u001b[49m.get_node(\u001b[33m\"\u001b[39m\u001b[33mSouth\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m      2\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mNode:     \u001b[39m\u001b[38;5;132;01m{\u001b[39;00msouth.key\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m      3\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mLevel:    \u001b[39m\u001b[38;5;132;01m{\u001b[39;00msouth.level\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'hierarchy' is not defined"
     ]
    }
   ],
   "source": [
    "south = hierarchy.get_node(\"South\")\n",
    "print(f\"Node:     {south.key}\")\n",
    "print(f\"Level:    {south.level}\")\n",
    "print(f\"Is leaf:  {south.is_leaf}\")\n",
    "print(f\"Children: {[c.key for c in south.children]}\")\n",
    "print(f\"Leaves:   {south.leaf_count}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:37.580418Z",
     "iopub.status.busy": "2026-03-01T13:36:37.580354Z",
     "iopub.status.idle": "2026-03-01T13:36:37.593331Z",
     "shell.execute_reply": "2026-03-01T13:36:37.592946Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'hierarchy' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[6]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m oslo_node = \u001b[43mhierarchy\u001b[49m.get_node(\u001b[33m\"\u001b[39m\u001b[33mSouth\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33mOslo\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m      2\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mNode:       \u001b[39m\u001b[38;5;132;01m{\u001b[39;00moslo_node.key\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m      3\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mIs leaf:    \u001b[39m\u001b[38;5;132;01m{\u001b[39;00moslo_node.is_leaf\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'hierarchy' is not defined"
     ]
    }
   ],
   "source": [
    "oslo_node = hierarchy.get_node(\"South\", \"Oslo\")\n",
    "print(f\"Node:       {oslo_node.key}\")\n",
    "print(f\"Is leaf:    {oslo_node.is_leaf}\")\n",
    "print(f\"Has series: {oslo_node.timeseries is not None}\")\n",
    "print(f\"Path:       {oslo_node.path}\")\n",
    "print(f\"Depth:      {oslo_node.depth}\")\n",
    "print(f\"Siblings:   {[s.key for s in oslo_node.siblings]}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:37.594328Z",
     "iopub.status.busy": "2026-03-01T13:36:37.594248Z",
     "iopub.status.idle": "2026-03-01T13:36:37.601069Z",
     "shell.execute_reply": "2026-03-01T13:36:37.600692Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'hierarchy' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[7]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m city_nodes = \u001b[43mhierarchy\u001b[49m.get_level(\u001b[33m\"\u001b[39m\u001b[33mcity\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m      2\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33m\"\u001b[39m\u001b[33mCity-level nodes:\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m      3\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m node \u001b[38;5;129;01min\u001b[39;00m city_nodes:\n",
      "\u001b[31mNameError\u001b[39m: name 'hierarchy' is not defined"
     ]
    }
   ],
   "source": [
    "city_nodes = hierarchy.get_level(\"city\")\n",
    "print(\"City-level nodes:\")\n",
    "for node in city_nodes:\n",
    "    print(f\"  {node.key} — {len(node.timeseries)} data points\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Leaves and walking\n",
    "\n",
    "`leaves()` returns all leaf nodes. `walk()` yields nodes in pre-order (default) or post-order."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:37.602024Z",
     "iopub.status.busy": "2026-03-01T13:36:37.601959Z",
     "iopub.status.idle": "2026-03-01T13:36:37.609355Z",
     "shell.execute_reply": "2026-03-01T13:36:37.608987Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "All leaves:\n"
     ]
    },
    {
     "ename": "NameError",
     "evalue": "name 'hierarchy' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[8]\u001b[39m\u001b[32m, line 2\u001b[39m\n\u001b[32m      1\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33m\"\u001b[39m\u001b[33mAll leaves:\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m----> \u001b[39m\u001b[32m2\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m leaf \u001b[38;5;129;01min\u001b[39;00m \u001b[43mhierarchy\u001b[49m.leaves():\n\u001b[32m      3\u001b[39m     \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33m  \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mleaf.key\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m12s\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m  path=\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mleaf.path\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'hierarchy' is not defined"
     ]
    }
   ],
   "source": [
    "print(\"All leaves:\")\n",
    "for leaf in hierarchy.leaves():\n",
    "    print(f\"  {leaf.key:12s}  path={leaf.path}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:37.610268Z",
     "iopub.status.busy": "2026-03-01T13:36:37.610207Z",
     "iopub.status.idle": "2026-03-01T13:36:37.617596Z",
     "shell.execute_reply": "2026-03-01T13:36:37.617232Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Pre-order walk:\n"
     ]
    },
    {
     "ename": "NameError",
     "evalue": "name 'hierarchy' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[9]\u001b[39m\u001b[32m, line 2\u001b[39m\n\u001b[32m      1\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33m\"\u001b[39m\u001b[33mPre-order walk:\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m----> \u001b[39m\u001b[32m2\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m node \u001b[38;5;129;01min\u001b[39;00m \u001b[43mhierarchy\u001b[49m.walk(order=\u001b[33m\"\u001b[39m\u001b[33mpre\u001b[39m\u001b[33m\"\u001b[39m):\n\u001b[32m      3\u001b[39m     indent = \u001b[33m\"\u001b[39m\u001b[33m  \u001b[39m\u001b[33m\"\u001b[39m * node.depth\n\u001b[32m      4\u001b[39m     label = \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mnode.key\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m [\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mnode.level\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m]\u001b[39m\u001b[33m\"\u001b[39m\n",
      "\u001b[31mNameError\u001b[39m: name 'hierarchy' is not defined"
     ]
    }
   ],
   "source": [
    "print(\"Pre-order walk:\")\n",
    "for node in hierarchy.walk(order=\"pre\"):\n",
    "    indent = \"  \" * node.depth\n",
    "    label = f\"{node.key} [{node.level}]\"\n",
    "    if node.is_leaf:\n",
    "        label += f\" — {len(node.timeseries)} pts\"\n",
    "    print(f\"{indent}{label}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Bottom-up aggregation\n",
    "\n",
    "`aggregate()` recursively combines leaf series using the chosen method (default: `SUM`).  \n",
    "Calling it on the root gives the total for the whole hierarchy."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:37.618466Z",
     "iopub.status.busy": "2026-03-01T13:36:37.618419Z",
     "iopub.status.idle": "2026-03-01T13:36:37.624760Z",
     "shell.execute_reply": "2026-03-01T13:36:37.624407Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'hierarchy' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[10]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m total = \u001b[43mhierarchy\u001b[49m.aggregate()\n\u001b[32m      2\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mName:   \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mtotal.name\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m      3\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mLength: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mlen\u001b[39m(total)\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m data points\u001b[39m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'hierarchy' is not defined"
     ]
    }
   ],
   "source": [
    "total = hierarchy.aggregate()\n",
    "print(f\"Name:   {total.name}\")\n",
    "print(f\"Length: {len(total)} data points\")\n",
    "print(f\"Mean:   {np.nanmean(total.arr):.1f} MW\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:37.625588Z",
     "iopub.status.busy": "2026-03-01T13:36:37.625540Z",
     "iopub.status.idle": "2026-03-01T13:36:37.632781Z",
     "shell.execute_reply": "2026-03-01T13:36:37.632436Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'hierarchy' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[11]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m south_total = \u001b[43mhierarchy\u001b[49m.aggregate(south)\n\u001b[32m      2\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mSouth region total — mean: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mnp.nanmean(south_total.arr)\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m.1f\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m MW\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m      4\u001b[39m north = hierarchy.get_node(\u001b[33m\"\u001b[39m\u001b[33mNorth\u001b[39m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'hierarchy' is not defined"
     ]
    }
   ],
   "source": [
    "south_total = hierarchy.aggregate(south)\n",
    "print(f\"South region total — mean: {np.nanmean(south_total.arr):.1f} MW\")\n",
    "\n",
    "north = hierarchy.get_node(\"North\")\n",
    "north_total = hierarchy.aggregate(north)\n",
    "print(f\"North region total — mean: {np.nanmean(north_total.arr):.1f} MW\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Level-wise aggregation\n",
    "\n",
    "`aggregate_level(level)` aggregates every node at the named level, returning a dict."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:37.633837Z",
     "iopub.status.busy": "2026-03-01T13:36:37.633774Z",
     "iopub.status.idle": "2026-03-01T13:36:37.640777Z",
     "shell.execute_reply": "2026-03-01T13:36:37.640395Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'hierarchy' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[12]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m region_agg = \u001b[43mhierarchy\u001b[49m.aggregate_level(\u001b[33m\"\u001b[39m\u001b[33mregion\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m      3\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m name, ts \u001b[38;5;129;01min\u001b[39;00m region_agg.items():\n\u001b[32m      4\u001b[39m     \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mname\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m8s\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m  mean=\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mnp.nanmean(ts.arr)\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m7.1f\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m MW  max=\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mnp.nanmax(ts.arr)\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m7.1f\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m MW\u001b[39m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'hierarchy' is not defined"
     ]
    }
   ],
   "source": [
    "region_agg = hierarchy.aggregate_level(\"region\")\n",
    "\n",
    "for name, ts in region_agg.items():\n",
    "    print(f\"{name:8s}  mean={np.nanmean(ts.arr):7.1f} MW  max={np.nanmax(ts.arr):7.1f} MW\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Choosing an aggregation method\n",
    "\n",
    "Override the default method by passing a different `AggregationMethod`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:37.641698Z",
     "iopub.status.busy": "2026-03-01T13:36:37.641649Z",
     "iopub.status.idle": "2026-03-01T13:36:37.648600Z",
     "shell.execute_reply": "2026-03-01T13:36:37.648238Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'tdm' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[13]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m method \u001b[38;5;129;01min\u001b[39;00m \u001b[43mtdm\u001b[49m.AggregationMethod:\n\u001b[32m      2\u001b[39m     agg = hierarchy.aggregate(method=method)\n\u001b[32m      3\u001b[39m     vals = agg.arr\n",
      "\u001b[31mNameError\u001b[39m: name 'tdm' is not defined"
     ]
    }
   ],
   "source": [
    "for method in tdm.AggregationMethod:\n",
    "    agg = hierarchy.aggregate(method=method)\n",
    "    vals = agg.arr\n",
    "    print(f\"{method.value:5s}  mean={np.nanmean(vals):7.1f}  min={np.nanmin(vals):7.1f}  max={np.nanmax(vals):7.1f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Subtree extraction\n",
    "\n",
    "`subtree(*path)` creates a new `HierarchicalTimeSeries` rooted at the specified node."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:37.649979Z",
     "iopub.status.busy": "2026-03-01T13:36:37.649915Z",
     "iopub.status.idle": "2026-03-01T13:36:37.656725Z",
     "shell.execute_reply": "2026-03-01T13:36:37.656375Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'hierarchy' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[14]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m south_tree = \u001b[43mhierarchy\u001b[49m.subtree(\u001b[33m\"\u001b[39m\u001b[33mSouth\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m      2\u001b[39m \u001b[38;5;28mprint\u001b[39m(south_tree)\n\u001b[32m      3\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[33mLevels: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00msouth_tree.levels\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'hierarchy' is not defined"
     ]
    }
   ],
   "source": [
    "south_tree = hierarchy.subtree(\"South\")\n",
    "print(south_tree)\n",
    "print(f\"\\nLevels: {south_tree.levels}\")\n",
    "print(f\"Leaves: {south_tree.n_leaves}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Converting to other containers\n",
    "\n",
    "Flatten the hierarchy into a `TimeSeriesCollection` or `TimeSeriesTable`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:37.657618Z",
     "iopub.status.busy": "2026-03-01T13:36:37.657566Z",
     "iopub.status.idle": "2026-03-01T13:36:37.663874Z",
     "shell.execute_reply": "2026-03-01T13:36:37.663579Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'hierarchy' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[15]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m collection = \u001b[43mhierarchy\u001b[49m.to_collection()\n\u001b[32m      2\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mLeaf-level collection: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mlist\u001b[39m(collection.keys())\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'hierarchy' is not defined"
     ]
    }
   ],
   "source": [
    "collection = hierarchy.to_collection()\n",
    "print(f\"Leaf-level collection: {list(collection.keys())}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:37.665154Z",
     "iopub.status.busy": "2026-03-01T13:36:37.665082Z",
     "iopub.status.idle": "2026-03-01T13:36:37.672285Z",
     "shell.execute_reply": "2026-03-01T13:36:37.671929Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'hierarchy' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[16]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m collection_regions = \u001b[43mhierarchy\u001b[49m.to_collection(level=\u001b[33m\"\u001b[39m\u001b[33mregion\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m      2\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33m\"\u001b[39m\u001b[33mRegion-level collection (aggregated):\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m      3\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m key, ts \u001b[38;5;129;01min\u001b[39;00m collection_regions.items():\n",
      "\u001b[31mNameError\u001b[39m: name 'hierarchy' is not defined"
     ]
    }
   ],
   "source": [
    "collection_regions = hierarchy.to_collection(level=\"region\")\n",
    "print(\"Region-level collection (aggregated):\")\n",
    "for key, ts in collection_regions.items():\n",
    "    print(f\"  {key}: {len(ts)} pts, mean={np.nanmean(ts.arr):.1f} MW\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:37.673155Z",
     "iopub.status.busy": "2026-03-01T13:36:37.673104Z",
     "iopub.status.idle": "2026-03-01T13:36:37.679519Z",
     "shell.execute_reply": "2026-03-01T13:36:37.679218Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'hierarchy' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[17]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m table = \u001b[43mhierarchy\u001b[49m.to_table()\n\u001b[32m      2\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mTable shape: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mlen\u001b[39m(table)\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m rows × \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mtable.n_columns\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m columns\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m      3\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mColumns: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mtable.names\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'hierarchy' is not defined"
     ]
    }
   ],
   "source": [
    "table = hierarchy.to_table()\n",
    "print(f\"Table shape: {len(table)} rows × {table.n_columns} columns\")\n",
    "print(f\"Columns: {table.names}\")\n",
    "table"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Building from a DataFrame\n",
    "\n",
    "`from_dataframe` builds the tree from a long-format DataFrame with hierarchy columns.  \n",
    "Each unique combination of level columns becomes a leaf."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:37.680448Z",
     "iopub.status.busy": "2026-03-01T13:36:37.680397Z",
     "iopub.status.idle": "2026-03-01T13:36:38.414024Z",
     "shell.execute_reply": "2026-03-01T13:36:38.413614Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'timestamps' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[18]\u001b[39m\u001b[32m, line 4\u001b[39m\n\u001b[32m      1\u001b[39m \u001b[38;5;28;01mimport\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mpandas\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mas\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mpd\u001b[39;00m\n\u001b[32m      3\u001b[39m rows = []\n\u001b[32m----> \u001b[39m\u001b[32m4\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m ts_dt \u001b[38;5;129;01min\u001b[39;00m \u001b[43mtimestamps\u001b[49m:\n\u001b[32m      5\u001b[39m     \u001b[38;5;28;01mfor\u001b[39;00m region, cities \u001b[38;5;129;01min\u001b[39;00m [(\u001b[33m\"\u001b[39m\u001b[33mSouth\u001b[39m\u001b[33m\"\u001b[39m, [\u001b[33m\"\u001b[39m\u001b[33mOslo\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33mBergen\u001b[39m\u001b[33m\"\u001b[39m]), (\u001b[33m\"\u001b[39m\u001b[33mNorth\u001b[39m\u001b[33m\"\u001b[39m, [\u001b[33m\"\u001b[39m\u001b[33mTromsø\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33mBodø\u001b[39m\u001b[33m\"\u001b[39m])]:\n\u001b[32m      6\u001b[39m         \u001b[38;5;28;01mfor\u001b[39;00m city \u001b[38;5;129;01min\u001b[39;00m cities:\n",
      "\u001b[31mNameError\u001b[39m: name 'timestamps' is not defined"
     ]
    }
   ],
   "source": [
    "import pandas as pd\n",
    "\n",
    "rows = []\n",
    "for ts_dt in timestamps:\n",
    "    for region, cities in [(\"South\", [\"Oslo\", \"Bergen\"]), (\"North\", [\"Tromsø\", \"Bodø\"])]:\n",
    "        for city in cities:\n",
    "            rows.append({\n",
    "                \"timestamp\": ts_dt,\n",
    "                \"region\": region,\n",
    "                \"city\": city,\n",
    "                \"consumption_mw\": float(rng.normal(200, 30)),\n",
    "            })\n",
    "\n",
    "df = pd.DataFrame(rows)\n",
    "print(f\"DataFrame shape: {df.shape}\")\n",
    "df.head(8)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:38.415138Z",
     "iopub.status.busy": "2026-03-01T13:36:38.415061Z",
     "iopub.status.idle": "2026-03-01T13:36:38.423423Z",
     "shell.execute_reply": "2026-03-01T13:36:38.423058Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'tdm' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[19]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m h_from_df = \u001b[43mtdm\u001b[49m.HierarchicalTimeSeries.from_dataframe(\n\u001b[32m      2\u001b[39m     df,\n\u001b[32m      3\u001b[39m     level_columns=[\u001b[33m\"\u001b[39m\u001b[33mregion\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33mcity\u001b[39m\u001b[33m\"\u001b[39m],\n\u001b[32m      4\u001b[39m     value_column=\u001b[33m\"\u001b[39m\u001b[33mconsumption_mw\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      5\u001b[39m     timestamp_column=\u001b[33m\"\u001b[39m\u001b[33mtimestamp\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      6\u001b[39m     name=\u001b[33m\"\u001b[39m\u001b[33mConsumption from DataFrame\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      7\u001b[39m     frequency=tdm.Frequency.PT1H,\n\u001b[32m      8\u001b[39m     timezone=\u001b[33m\"\u001b[39m\u001b[33mEurope/Oslo\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      9\u001b[39m )\n\u001b[32m     10\u001b[39m h_from_df\n",
      "\u001b[31mNameError\u001b[39m: name 'tdm' is not defined"
     ]
    }
   ],
   "source": [
    "h_from_df = tdm.HierarchicalTimeSeries.from_dataframe(\n",
    "    df,\n",
    "    level_columns=[\"region\", \"city\"],\n",
    "    value_column=\"consumption_mw\",\n",
    "    timestamp_column=\"timestamp\",\n",
    "    name=\"Consumption from DataFrame\",\n",
    "    frequency=tdm.Frequency.PT1H,\n",
    "    timezone=\"Europe/Oslo\",\n",
    ")\n",
    "h_from_df"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:38.424543Z",
     "iopub.status.busy": "2026-03-01T13:36:38.424474Z",
     "iopub.status.idle": "2026-03-01T13:36:38.431887Z",
     "shell.execute_reply": "2026-03-01T13:36:38.431478Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'h_from_df' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[20]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m total_df = \u001b[43mh_from_df\u001b[49m.aggregate()\n\u001b[32m      2\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mTotal from DataFrame hierarchy: mean=\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mnp.nanmean(total_df.arr)\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m.1f\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m MW\u001b[39m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'h_from_df' is not defined"
     ]
    }
   ],
   "source": [
    "total_df = h_from_df.aggregate()\n",
    "print(f\"Total from DataFrame hierarchy: mean={np.nanmean(total_df.arr):.1f} MW\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Another example: energy production by source\n",
    "\n",
    "Hierarchies can model any tree-shaped relationship — here, power production broken down by energy source."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:38.432900Z",
     "iopub.status.busy": "2026-03-01T13:36:38.432836Z",
     "iopub.status.idle": "2026-03-01T13:36:38.441910Z",
     "shell.execute_reply": "2026-03-01T13:36:38.441590Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'tdm' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[21]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m energy_root = \u001b[43mtdm\u001b[49m.HierarchyNode(\n\u001b[32m      2\u001b[39m     key=\u001b[33m\"\u001b[39m\u001b[33mTotal\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      3\u001b[39m     level=\u001b[33m\"\u001b[39m\u001b[33mtotal\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      4\u001b[39m     children=[\n\u001b[32m      5\u001b[39m         tdm.HierarchyNode(\n\u001b[32m      6\u001b[39m             key=\u001b[33m\"\u001b[39m\u001b[33mWind\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      7\u001b[39m             level=\u001b[33m\"\u001b[39m\u001b[33msource\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      8\u001b[39m             children=[\n\u001b[32m      9\u001b[39m                 tdm.HierarchyNode(key=\u001b[33m\"\u001b[39m\u001b[33mFarm A\u001b[39m\u001b[33m\"\u001b[39m, level=\u001b[33m\"\u001b[39m\u001b[33mfarm\u001b[39m\u001b[33m\"\u001b[39m, timeseries=make_consumption(\u001b[33m\"\u001b[39m\u001b[33mFarm A\u001b[39m\u001b[33m\"\u001b[39m, \u001b[32m100\u001b[39m)),\n\u001b[32m     10\u001b[39m                 tdm.HierarchyNode(key=\u001b[33m\"\u001b[39m\u001b[33mFarm B\u001b[39m\u001b[33m\"\u001b[39m, level=\u001b[33m\"\u001b[39m\u001b[33mfarm\u001b[39m\u001b[33m\"\u001b[39m, timeseries=make_consumption(\u001b[33m\"\u001b[39m\u001b[33mFarm B\u001b[39m\u001b[33m\"\u001b[39m, \u001b[32m80\u001b[39m)),\n\u001b[32m     11\u001b[39m             ],\n\u001b[32m     12\u001b[39m         ),\n\u001b[32m     13\u001b[39m         tdm.HierarchyNode(\n\u001b[32m     14\u001b[39m             key=\u001b[33m\"\u001b[39m\u001b[33mSolar\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m     15\u001b[39m             level=\u001b[33m\"\u001b[39m\u001b[33msource\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m     16\u001b[39m             children=[\n\u001b[32m     17\u001b[39m                 tdm.HierarchyNode(key=\u001b[33m\"\u001b[39m\u001b[33mPlant X\u001b[39m\u001b[33m\"\u001b[39m, level=\u001b[33m\"\u001b[39m\u001b[33mfarm\u001b[39m\u001b[33m\"\u001b[39m, timeseries=make_consumption(\u001b[33m\"\u001b[39m\u001b[33mPlant X\u001b[39m\u001b[33m\"\u001b[39m, \u001b[32m60\u001b[39m)),\n\u001b[32m     18\u001b[39m             ],\n\u001b[32m     19\u001b[39m         ),\n\u001b[32m     20\u001b[39m     ],\n\u001b[32m     21\u001b[39m )\n\u001b[32m     23\u001b[39m energy = tdm.HierarchicalTimeSeries(\n\u001b[32m     24\u001b[39m     energy_root,\n\u001b[32m     25\u001b[39m     name=\u001b[33m\"\u001b[39m\u001b[33mEnergy Production\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m     26\u001b[39m     levels=[\u001b[33m\"\u001b[39m\u001b[33mtotal\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33msource\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33mfarm\u001b[39m\u001b[33m\"\u001b[39m],\n\u001b[32m     27\u001b[39m     aggregation=tdm.AggregationMethod.SUM,\n\u001b[32m     28\u001b[39m )\n\u001b[32m     29\u001b[39m energy\n",
      "\u001b[31mNameError\u001b[39m: name 'tdm' is not defined"
     ]
    }
   ],
   "source": [
    "energy_root = tdm.HierarchyNode(\n",
    "    key=\"Total\",\n",
    "    level=\"total\",\n",
    "    children=[\n",
    "        tdm.HierarchyNode(\n",
    "            key=\"Wind\",\n",
    "            level=\"source\",\n",
    "            children=[\n",
    "                tdm.HierarchyNode(key=\"Farm A\", level=\"farm\", timeseries=make_consumption(\"Farm A\", 100)),\n",
    "                tdm.HierarchyNode(key=\"Farm B\", level=\"farm\", timeseries=make_consumption(\"Farm B\", 80)),\n",
    "            ],\n",
    "        ),\n",
    "        tdm.HierarchyNode(\n",
    "            key=\"Solar\",\n",
    "            level=\"source\",\n",
    "            children=[\n",
    "                tdm.HierarchyNode(key=\"Plant X\", level=\"farm\", timeseries=make_consumption(\"Plant X\", 60)),\n",
    "            ],\n",
    "        ),\n",
    "    ],\n",
    ")\n",
    "\n",
    "energy = tdm.HierarchicalTimeSeries(\n",
    "    energy_root,\n",
    "    name=\"Energy Production\",\n",
    "    levels=[\"total\", \"source\", \"farm\"],\n",
    "    aggregation=tdm.AggregationMethod.SUM,\n",
    ")\n",
    "energy"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:38.442957Z",
     "iopub.status.busy": "2026-03-01T13:36:38.442893Z",
     "iopub.status.idle": "2026-03-01T13:36:38.450727Z",
     "shell.execute_reply": "2026-03-01T13:36:38.450380Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'energy' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[22]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m source_agg = \u001b[43menergy\u001b[49m.aggregate_level(\u001b[33m\"\u001b[39m\u001b[33msource\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m      2\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m name, ts \u001b[38;5;129;01min\u001b[39;00m source_agg.items():\n\u001b[32m      3\u001b[39m     \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mname\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m8s\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m  mean=\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mnp.nanmean(ts.arr)\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m.1f\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m MW\u001b[39m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'energy' is not defined"
     ]
    }
   ],
   "source": [
    "source_agg = energy.aggregate_level(\"source\")\n",
    "for name, ts in source_agg.items():\n",
    "    print(f\"{name:8s}  mean={np.nanmean(ts.arr):.1f} MW\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Sequence protocol\n",
    "\n",
    "`HierarchicalTimeSeries` supports `len`, `in`, and bracket indexing with slash-separated paths."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:38.451749Z",
     "iopub.status.busy": "2026-03-01T13:36:38.451689Z",
     "iopub.status.idle": "2026-03-01T13:36:38.458678Z",
     "shell.execute_reply": "2026-03-01T13:36:38.458303Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'hierarchy' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[23]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mTotal nodes:        \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mlen\u001b[39m(\u001b[43mhierarchy\u001b[49m)\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m      2\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33m'\u001b[39m\u001b[33mOslo\u001b[39m\u001b[33m'\u001b[39m\u001b[33m in tree:     \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[33m'\u001b[39m\u001b[33mOslo\u001b[39m\u001b[33m'\u001b[39m\u001b[38;5;250m \u001b[39m\u001b[38;5;129;01min\u001b[39;00m\u001b[38;5;250m \u001b[39mhierarchy\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m      3\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33m'\u001b[39m\u001b[33mHelsinki\u001b[39m\u001b[33m'\u001b[39m\u001b[33m in tree: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[33m'\u001b[39m\u001b[33mHelsinki\u001b[39m\u001b[33m'\u001b[39m\u001b[38;5;250m \u001b[39m\u001b[38;5;129;01min\u001b[39;00m\u001b[38;5;250m \u001b[39mhierarchy\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'hierarchy' is not defined"
     ]
    }
   ],
   "source": [
    "print(f\"Total nodes:        {len(hierarchy)}\")\n",
    "print(f\"'Oslo' in tree:     {'Oslo' in hierarchy}\")\n",
    "print(f\"'Helsinki' in tree: {'Helsinki' in hierarchy}\")\n",
    "\n",
    "node = hierarchy[\"South/Oslo\"]\n",
    "print(f\"\\nBracket access:     {node.key} (leaf={node.is_leaf})\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Summary\n",
    "\n",
    "| Feature | API |\n",
    "| --- | --- |\n",
    "| Build manually | `HierarchyNode(key, level, children, timeseries)` |\n",
    "| Build from DataFrame | `HierarchicalTimeSeries.from_dataframe(df, level_columns, value_column)` |\n",
    "| Build from dict | `HierarchicalTimeSeries.from_dict(tree, series_map, levels=...)` |\n",
    "| Navigate | `get_node(*path)`, `get_level(name)`, `leaves()` |\n",
    "| Walk | `walk(order=\"pre\")` / `walk(order=\"post\")` |\n",
    "| Aggregate | `aggregate(node, method)` — bottom-up recursion |\n",
    "| Level aggregate | `aggregate_level(level)` → `dict[str, TimeSeries]` |\n",
    "| Subtree | `subtree(*path)` → new `HierarchicalTimeSeries` |\n",
    "| Convert | `to_collection(level)`, `to_table(level)` |\n",
    "| Sequence ops | `len(h)`, `key in h`, `h[\"path/to/node\"]` |"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.14.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
