{
 "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 `TimeSeriesList` |\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-04T19:11:01.502988Z",
     "iopub.status.busy": "2026-03-04T19:11:01.502589Z",
     "iopub.status.idle": "2026-03-04T19:11:01.553960Z",
     "shell.execute_reply": "2026-03-04T19:11:01.553427Z"
    }
   },
   "outputs": [],
   "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 `TimeSeriesList`. Here we model electricity consumption for five Norwegian cities."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-04T19:11:01.555340Z",
     "iopub.status.busy": "2026-03-04T19:11:01.555220Z",
     "iopub.status.idle": "2026-03-04T19:11:01.557837Z",
     "shell.execute_reply": "2026-03-04T19:11:01.557484Z"
    }
   },
   "outputs": [],
   "source": [
    "def make_consumption(name: str, base_mw: float) -> tdm.TimeSeriesList:\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.TimeSeriesList(\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 `TimeSeriesList`; interior nodes have `children`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-04T19:11:01.559001Z",
     "iopub.status.busy": "2026-03-04T19:11:01.558915Z",
     "iopub.status.idle": "2026-03-04T19:11:01.562769Z",
     "shell.execute_reply": "2026-03-04T19:11:01.562356Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<style>\n",
       ".ts-repr { font-family: monospace; font-size: 13px; max-width: 640px; display: inline-grid; }\n",
       ".ts-repr .ts-header {\n",
       "  font-weight: bold; font-size: 14px;\n",
       "  padding: 6px 10px; border-bottom: 2px solid #4a4a4a;\n",
       "  background: #f0f0f0; color: #1a1a1a;\n",
       "}\n",
       ".ts-repr .ts-meta { padding: 6px 10px; background: #fafafa; overflow: hidden; min-width: 0; }\n",
       ".ts-repr .ts-meta table { border-collapse: collapse; width: 100%; table-layout: fixed; }\n",
       ".ts-repr .ts-meta td { padding: 1px 8px 1px 0; white-space: nowrap; }\n",
       ".ts-repr .ts-meta td:first-child { color: #475569; font-weight: 600; width: 90px; }\n",
       ".ts-repr .ts-meta td:last-child { color: #1a1a1a; overflow: hidden; text-overflow: ellipsis; }\n",
       ".ts-repr .ts-data { padding: 6px 10px; }\n",
       ".ts-repr .ts-data table {\n",
       "  border-collapse: collapse; text-align: right;\n",
       "}\n",
       ".ts-repr .ts-data th {\n",
       "  text-align: right; padding: 3px 10px; border-bottom: 1px solid #ccc;\n",
       "  color: #555; font-weight: 600;\n",
       "}\n",
       ".ts-repr .ts-data th.ts-idx { text-align: left; }\n",
       ".ts-repr .ts-data td { padding: 2px 10px; }\n",
       ".ts-repr .ts-data tr:hover { background: #f5f5f5; }\n",
       ".ts-repr .ts-data td:first-child { text-align: left; color: #1e293b; }\n",
       ".ts-repr .ts-data td.ts-idx { text-align: left; color: #1e293b; }\n",
       ".ts-repr .ts-ellipsis { text-align: center !important; color: #999; }\n",
       "@media (prefers-color-scheme: dark) {\n",
       "  .ts-repr .ts-header { background: #1e293b; color: #e2e8f0; border-color: #475569; }\n",
       "  .ts-repr .ts-meta { background: #0f172a; }\n",
       "  .ts-repr .ts-meta td:first-child { color: #94a3b8; }\n",
       "  .ts-repr .ts-meta td:last-child { color: #e2e8f0; }\n",
       "  .ts-repr .ts-data th { color: #94a3b8; border-color: #334155; }\n",
       "  .ts-repr .ts-data td { color: #e2e8f0; }\n",
       "  .ts-repr .ts-data td:first-child { color: #cbd5e1; }\n",
       "  .ts-repr .ts-data td.ts-idx { color: #cbd5e1; }\n",
       "  .ts-repr .ts-data tr:hover { background: #1e293b; }\n",
       "  .ts-repr .ts-ellipsis { color: #64748b; }\n",
       "}\n",
       "</style>\n",
       "<div class=\"ts-repr\">\n",
       "<div class=\"ts-header\">HierarchicalTimeSeries</div>\n",
       "<div class=\"ts-meta\"><table>\n",
       "<tr><td>Name</td><td>Norway Consumption</td></tr>\n",
       "<tr><td>Levels</td><td>country, region, city</td></tr>\n",
       "<tr><td>Nodes</td><td>8 (5 leaves)</td></tr>\n",
       "<tr><td>Frequency</td><td>PT1H</td></tr>\n",
       "<tr><td>Timezone</td><td>Europe/Oslo (+00:00)</td></tr>\n",
       "<tr><td>Unit</td><td>MW</td></tr>\n",
       "<tr><td>Aggregation</td><td>sum</td></tr>\n",
       "</table></div>\n",
       "<div class=\"ts-data\"><table style=\"text-align: left;\">\n",
       "<tr><th>name</th><th>level</th><th>length</th><th>begin</th><th>end</th></tr>\n",
       "<tr><td>Oslo</td><td>city</td><td>24</td><td>2024-01-15 00:00</td><td>2024-01-15 23:00</td></tr>\n",
       "<tr><td>Bergen</td><td>city</td><td>24</td><td>2024-01-15 00:00</td><td>2024-01-15 23:00</td></tr>\n",
       "<tr><td>Stavanger</td><td>city</td><td>24</td><td>2024-01-15 00:00</td><td>2024-01-15 23:00</td></tr>\n",
       "<tr><td>Tromsø</td><td>city</td><td>24</td><td>2024-01-15 00:00</td><td>2024-01-15 23:00</td></tr>\n",
       "<tr><td>Bodø</td><td>city</td><td>24</td><td>2024-01-15 00:00</td><td>2024-01-15 23:00</td></tr>\n",
       "</table></div></div>"
      ],
      "text/plain": [
       "HierarchicalTimeSeries\n",
       "┌────────────────────────────────────────────────────────────────┐\n",
       "│  Name:             Norway Consumption                          │\n",
       "│  Levels:           country, region, city                       │\n",
       "│  Nodes:            8 (5 leaves)                                │\n",
       "│  Frequency:        PT1H                                        │\n",
       "│  Timezone:         Europe/Oslo (+00:00)                        │\n",
       "│  Unit:             MW                                          │\n",
       "│  Aggregation:      sum                                         │\n",
       "├────────────────────────────────────────────────────────────────┤\n",
       "│  name       level  length  begin             end               │\n",
       "├────────────────────────────────────────────────────────────────┤\n",
       "│  Oslo       city   24      2024-01-15 00:00  2024-01-15 23:00  │\n",
       "│  Bergen     city   24      2024-01-15 00:00  2024-01-15 23:00  │\n",
       "│  Stavanger  city   24      2024-01-15 00:00  2024-01-15 23:00  │\n",
       "│  Tromsø     city   24      2024-01-15 00:00  2024-01-15 23:00  │\n",
       "│  Bodø       city   24      2024-01-15 00:00  2024-01-15 23:00  │\n",
       "└────────────────────────────────────────────────────────────────┘"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "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-04T19:11:01.563909Z",
     "iopub.status.busy": "2026-03-04T19:11:01.563844Z",
     "iopub.status.idle": "2026-03-04T19:11:01.565734Z",
     "shell.execute_reply": "2026-03-04T19:11:01.565461Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Name:     Norway Consumption\n",
      "Levels:   ['country', 'region', 'city']\n",
      "# levels: 3\n",
      "# nodes:  8\n",
      "# leaves: 5\n"
     ]
    }
   ],
   "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-04T19:11:01.566965Z",
     "iopub.status.busy": "2026-03-04T19:11:01.566897Z",
     "iopub.status.idle": "2026-03-04T19:11:01.568889Z",
     "shell.execute_reply": "2026-03-04T19:11:01.568517Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Node:     South\n",
      "Level:    region\n",
      "Is leaf:  False\n",
      "Children: ['Oslo', 'Bergen', 'Stavanger']\n",
      "Leaves:   3\n"
     ]
    }
   ],
   "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-04T19:11:01.569803Z",
     "iopub.status.busy": "2026-03-04T19:11:01.569741Z",
     "iopub.status.idle": "2026-03-04T19:11:01.571672Z",
     "shell.execute_reply": "2026-03-04T19:11:01.571304Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Node:       Oslo\n",
      "Is leaf:    True\n",
      "Has series: True\n",
      "Path:       ['Norway', 'South', 'Oslo']\n",
      "Depth:      2\n",
      "Siblings:   ['Bergen', 'Stavanger']\n"
     ]
    }
   ],
   "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-04T19:11:01.572629Z",
     "iopub.status.busy": "2026-03-04T19:11:01.572567Z",
     "iopub.status.idle": "2026-03-04T19:11:01.574217Z",
     "shell.execute_reply": "2026-03-04T19:11:01.573902Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "City-level nodes:\n",
      "  Oslo — 24 data points\n",
      "  Bergen — 24 data points\n",
      "  Stavanger — 24 data points\n",
      "  Tromsø — 24 data points\n",
      "  Bodø — 24 data points\n"
     ]
    }
   ],
   "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-04T19:11:01.575085Z",
     "iopub.status.busy": "2026-03-04T19:11:01.575026Z",
     "iopub.status.idle": "2026-03-04T19:11:01.576602Z",
     "shell.execute_reply": "2026-03-04T19:11:01.576323Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "All leaves:\n",
      "  Oslo          path=['Norway', 'South', 'Oslo']\n",
      "  Bergen        path=['Norway', 'South', 'Bergen']\n",
      "  Stavanger     path=['Norway', 'South', 'Stavanger']\n",
      "  Tromsø        path=['Norway', 'North', 'Tromsø']\n",
      "  Bodø          path=['Norway', 'North', 'Bodø']\n"
     ]
    }
   ],
   "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-04T19:11:01.577472Z",
     "iopub.status.busy": "2026-03-04T19:11:01.577403Z",
     "iopub.status.idle": "2026-03-04T19:11:01.578955Z",
     "shell.execute_reply": "2026-03-04T19:11:01.578718Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Pre-order walk:\n",
      "Norway [country]\n",
      "  South [region]\n",
      "    Oslo [city] — 24 pts\n",
      "    Bergen [city] — 24 pts\n",
      "    Stavanger [city] — 24 pts\n",
      "  North [region]\n",
      "    Tromsø [city] — 24 pts\n",
      "    Bodø [city] — 24 pts\n"
     ]
    }
   ],
   "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-04T19:11:01.579925Z",
     "iopub.status.busy": "2026-03-04T19:11:01.579874Z",
     "iopub.status.idle": "2026-03-04T19:11:01.581631Z",
     "shell.execute_reply": "2026-03-04T19:11:01.581395Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Name:   Norway\n",
      "Length: 24 data points\n",
      "Mean:   979.5 MW\n"
     ]
    }
   ],
   "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-04T19:11:01.582445Z",
     "iopub.status.busy": "2026-03-04T19:11:01.582392Z",
     "iopub.status.idle": "2026-03-04T19:11:01.584112Z",
     "shell.execute_reply": "2026-03-04T19:11:01.583813Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "South region total — mean: 850.6 MW\n",
      "North region total — mean: 128.8 MW\n"
     ]
    }
   ],
   "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-04T19:11:01.585008Z",
     "iopub.status.busy": "2026-03-04T19:11:01.584960Z",
     "iopub.status.idle": "2026-03-04T19:11:01.586542Z",
     "shell.execute_reply": "2026-03-04T19:11:01.586299Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "South     mean=  850.6 MW  max= 1125.5 MW\n",
      "North     mean=  128.8 MW  max=  172.1 MW\n"
     ]
    }
   ],
   "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-04T19:11:01.587553Z",
     "iopub.status.busy": "2026-03-04T19:11:01.587490Z",
     "iopub.status.idle": "2026-03-04T19:11:01.589361Z",
     "shell.execute_reply": "2026-03-04T19:11:01.589030Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sum    mean=  979.5  min=  672.3  max= 1296.0\n",
      "mean   mean=  174.0  min=  119.5  max=  230.2\n",
      "min    mean=   49.5  min=   31.4  max=   67.9\n",
      "max    mean=  499.5  min=  326.4  max=  652.8\n"
     ]
    }
   ],
   "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-04T19:11:01.590245Z",
     "iopub.status.busy": "2026-03-04T19:11:01.590194Z",
     "iopub.status.idle": "2026-03-04T19:11:01.591987Z",
     "shell.execute_reply": "2026-03-04T19:11:01.591718Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "HierarchicalTimeSeries\n",
      "┌────────────────────────────────────────────────────────────────┐\n",
      "│  Name:             Norway Consumption                          │\n",
      "│  Levels:           region, city                                │\n",
      "│  Nodes:            4 (3 leaves)                                │\n",
      "│  Frequency:        PT1H                                        │\n",
      "│  Timezone:         Europe/Oslo (+00:00)                        │\n",
      "│  Unit:             MW                                          │\n",
      "│  Aggregation:      sum                                         │\n",
      "├────────────────────────────────────────────────────────────────┤\n",
      "│  name       level  length  begin             end               │\n",
      "├────────────────────────────────────────────────────────────────┤\n",
      "│  Oslo       city   24      2024-01-15 00:00  2024-01-15 23:00  │\n",
      "│  Bergen     city   24      2024-01-15 00:00  2024-01-15 23:00  │\n",
      "│  Stavanger  city   24      2024-01-15 00:00  2024-01-15 23:00  │\n",
      "└────────────────────────────────────────────────────────────────┘\n",
      "\n",
      "Levels: ['region', 'city']\n",
      "Leaves: 3\n"
     ]
    }
   ],
   "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-04T19:11:01.592833Z",
     "iopub.status.busy": "2026-03-04T19:11:01.592779Z",
     "iopub.status.idle": "2026-03-04T19:11:01.594252Z",
     "shell.execute_reply": "2026-03-04T19:11:01.593946Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Leaf-level collection: ['Oslo', 'Bergen', 'Stavanger', 'Tromsø', 'Bodø']\n"
     ]
    }
   ],
   "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-04T19:11:01.595133Z",
     "iopub.status.busy": "2026-03-04T19:11:01.595083Z",
     "iopub.status.idle": "2026-03-04T19:11:01.596579Z",
     "shell.execute_reply": "2026-03-04T19:11:01.596294Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Region-level collection (aggregated):\n",
      "  South: 24 pts, mean=850.6 MW\n",
      "  North: 24 pts, mean=128.8 MW\n"
     ]
    }
   ],
   "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-04T19:11:01.597384Z",
     "iopub.status.busy": "2026-03-04T19:11:01.597326Z",
     "iopub.status.idle": "2026-03-04T19:11:01.599322Z",
     "shell.execute_reply": "2026-03-04T19:11:01.599031Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Table shape: 24 rows × 5 columns\n",
      "Columns: ['Oslo', 'Bergen', 'Stavanger', 'Tromsø', 'Bodø']\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<style>\n",
       ".ts-repr { font-family: monospace; font-size: 13px; max-width: 640px; display: inline-grid; }\n",
       ".ts-repr .ts-header {\n",
       "  font-weight: bold; font-size: 14px;\n",
       "  padding: 6px 10px; border-bottom: 2px solid #4a4a4a;\n",
       "  background: #f0f0f0; color: #1a1a1a;\n",
       "}\n",
       ".ts-repr .ts-meta { padding: 6px 10px; background: #fafafa; overflow: hidden; min-width: 0; }\n",
       ".ts-repr .ts-meta table { border-collapse: collapse; width: 100%; table-layout: fixed; }\n",
       ".ts-repr .ts-meta td { padding: 1px 8px 1px 0; white-space: nowrap; }\n",
       ".ts-repr .ts-meta td:first-child { color: #475569; font-weight: 600; width: 90px; }\n",
       ".ts-repr .ts-meta td:last-child { color: #1a1a1a; overflow: hidden; text-overflow: ellipsis; }\n",
       ".ts-repr .ts-data { padding: 6px 10px; }\n",
       ".ts-repr .ts-data table {\n",
       "  border-collapse: collapse; text-align: right;\n",
       "}\n",
       ".ts-repr .ts-data th {\n",
       "  text-align: right; padding: 3px 10px; border-bottom: 1px solid #ccc;\n",
       "  color: #555; font-weight: 600;\n",
       "}\n",
       ".ts-repr .ts-data th.ts-idx { text-align: left; }\n",
       ".ts-repr .ts-data td { padding: 2px 10px; }\n",
       ".ts-repr .ts-data tr:hover { background: #f5f5f5; }\n",
       ".ts-repr .ts-data td:first-child { text-align: left; color: #1e293b; }\n",
       ".ts-repr .ts-data td.ts-idx { text-align: left; color: #1e293b; }\n",
       ".ts-repr .ts-ellipsis { text-align: center !important; color: #999; }\n",
       "@media (prefers-color-scheme: dark) {\n",
       "  .ts-repr .ts-header { background: #1e293b; color: #e2e8f0; border-color: #475569; }\n",
       "  .ts-repr .ts-meta { background: #0f172a; }\n",
       "  .ts-repr .ts-meta td:first-child { color: #94a3b8; }\n",
       "  .ts-repr .ts-meta td:last-child { color: #e2e8f0; }\n",
       "  .ts-repr .ts-data th { color: #94a3b8; border-color: #334155; }\n",
       "  .ts-repr .ts-data td { color: #e2e8f0; }\n",
       "  .ts-repr .ts-data td:first-child { color: #cbd5e1; }\n",
       "  .ts-repr .ts-data td.ts-idx { color: #cbd5e1; }\n",
       "  .ts-repr .ts-data tr:hover { background: #1e293b; }\n",
       "  .ts-repr .ts-ellipsis { color: #64748b; }\n",
       "}\n",
       "</style>\n",
       "<div class=\"ts-repr\">\n",
       "<div class=\"ts-header\">TimeSeriesTable</div>\n",
       "<div class=\"ts-meta\"><table>\n",
       "<tr><td>Name</td><td>unnamed</td></tr>\n",
       "<tr><td>Columns</td><td>Oslo, Bergen, Stavanger, Tromsø, Bodø</td></tr>\n",
       "<tr><td>Length</td><td>24 × 5</td></tr>\n",
       "<tr><td>Frequency</td><td>PT1H</td></tr>\n",
       "<tr><td>Timezone</td><td>Europe/Oslo (+00:00)</td></tr>\n",
       "<tr><td>Unit</td><td>MW, MW, MW, MW, MW</td></tr>\n",
       "</table></div>\n",
       "<div class=\"ts-data\"><table>\n",
       "<tr><th class=\"ts-idx\">timestamp</th><th>Oslo</th><th>Bergen</th><th>Stavanger</th><th>Tromsø</th><th>Bodø</th></tr>\n",
       "<tr><td>2024-01-15 00:00</td><td>507.618</td><td>195.717</td><td>155.092</td><td>76.3222</td><td>46.6933</td></tr>\n",
       "<tr><td>2024-01-15 01:00</td><td>514.47</td><td>212.666</td><td>162.648</td><td>88.4638</td><td>51.5538</td></tr>\n",
       "<tr><td>2024-01-15 02:00</td><td>596.699</td><td>236.498</td><td>175.55</td><td>93.0397</td><td>58.7932</td></tr>\n",
       "<tr><td class=\"ts-ellipsis\">&hellip;</td><td class=\"ts-ellipsis\">&hellip;</td><td class=\"ts-ellipsis\">&hellip;</td><td class=\"ts-ellipsis\">&hellip;</td><td class=\"ts-ellipsis\">&hellip;</td><td class=\"ts-ellipsis\">&hellip;</td></tr>\n",
       "<tr><td>2024-01-15 21:00</td><td>405.039</td><td>171.012</td><td>125.184</td><td>67.2111</td><td>42.2609</td></tr>\n",
       "<tr><td>2024-01-15 22:00</td><td>490.094</td><td>192.526</td><td>128.291</td><td>66.7755</td><td>49.9575</td></tr>\n",
       "<tr><td>2024-01-15 23:00</td><td>496.137</td><td>202.236</td><td>141.5</td><td>74.2116</td><td>49.4016</td></tr>\n",
       "</table></div>\n",
       "</div>"
      ],
      "text/plain": [
       "TimeSeriesTable\n",
       "┌───────────────────────────────────────────────────────────────────┐\n",
       "│  Name:             unnamed                                        │\n",
       "│  Columns:          Oslo, Bergen, Stavanger, Tromsø, Bodø          │\n",
       "│  Length:           24 × 5                                         │\n",
       "│  Frequency:        PT1H                                           │\n",
       "│  Timezone:         Europe/Oslo (+00:00)                           │\n",
       "│  Unit:             MW, MW, MW, MW, MW                             │\n",
       "├───────────────────────────────────────────────────────────────────┤\n",
       "│                       Oslo   Bergen  Stavanger   Tromsø     Bodø  │\n",
       "│  2024-01-15 00:00  507.618  195.717    155.092  76.3222  46.6933  │\n",
       "│  2024-01-15 01:00   514.47  212.666    162.648  88.4638  51.5538  │\n",
       "│  2024-01-15 02:00  596.699  236.498     175.55  93.0397  58.7932  │\n",
       "│  ...                   ...      ...        ...      ...      ...  │\n",
       "│  2024-01-15 21:00  405.039  171.012    125.184  67.2111  42.2609  │\n",
       "│  2024-01-15 22:00  490.094  192.526    128.291  66.7755  49.9575  │\n",
       "│  2024-01-15 23:00  496.137  202.236      141.5  74.2116  49.4016  │\n",
       "└───────────────────────────────────────────────────────────────────┘"
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "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-04T19:11:01.600257Z",
     "iopub.status.busy": "2026-03-04T19:11:01.600203Z",
     "iopub.status.idle": "2026-03-04T19:11:01.685714Z",
     "shell.execute_reply": "2026-03-04T19:11:01.685312Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "DataFrame shape: (96, 4)\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>timestamp</th>\n",
       "      <th>region</th>\n",
       "      <th>city</th>\n",
       "      <th>consumption_mw</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>2024-01-15 00:00:00+00:00</td>\n",
       "      <td>South</td>\n",
       "      <td>Oslo</td>\n",
       "      <td>169.295075</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>2024-01-15 00:00:00+00:00</td>\n",
       "      <td>South</td>\n",
       "      <td>Bergen</td>\n",
       "      <td>205.378269</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>2024-01-15 00:00:00+00:00</td>\n",
       "      <td>North</td>\n",
       "      <td>Tromsø</td>\n",
       "      <td>206.599901</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>2024-01-15 00:00:00+00:00</td>\n",
       "      <td>North</td>\n",
       "      <td>Bodø</td>\n",
       "      <td>240.775627</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>2024-01-15 01:00:00+00:00</td>\n",
       "      <td>South</td>\n",
       "      <td>Oslo</td>\n",
       "      <td>225.053337</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>2024-01-15 01:00:00+00:00</td>\n",
       "      <td>South</td>\n",
       "      <td>Bergen</td>\n",
       "      <td>210.706132</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>2024-01-15 01:00:00+00:00</td>\n",
       "      <td>North</td>\n",
       "      <td>Tromsø</td>\n",
       "      <td>243.899087</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>2024-01-15 01:00:00+00:00</td>\n",
       "      <td>North</td>\n",
       "      <td>Bodø</td>\n",
       "      <td>164.337108</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                  timestamp region    city  consumption_mw\n",
       "0 2024-01-15 00:00:00+00:00  South    Oslo      169.295075\n",
       "1 2024-01-15 00:00:00+00:00  South  Bergen      205.378269\n",
       "2 2024-01-15 00:00:00+00:00  North  Tromsø      206.599901\n",
       "3 2024-01-15 00:00:00+00:00  North    Bodø      240.775627\n",
       "4 2024-01-15 01:00:00+00:00  South    Oslo      225.053337\n",
       "5 2024-01-15 01:00:00+00:00  South  Bergen      210.706132\n",
       "6 2024-01-15 01:00:00+00:00  North  Tromsø      243.899087\n",
       "7 2024-01-15 01:00:00+00:00  North    Bodø      164.337108"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "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-04T19:11:01.686624Z",
     "iopub.status.busy": "2026-03-04T19:11:01.686559Z",
     "iopub.status.idle": "2026-03-04T19:11:01.690603Z",
     "shell.execute_reply": "2026-03-04T19:11:01.690233Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<style>\n",
       ".ts-repr { font-family: monospace; font-size: 13px; max-width: 640px; display: inline-grid; }\n",
       ".ts-repr .ts-header {\n",
       "  font-weight: bold; font-size: 14px;\n",
       "  padding: 6px 10px; border-bottom: 2px solid #4a4a4a;\n",
       "  background: #f0f0f0; color: #1a1a1a;\n",
       "}\n",
       ".ts-repr .ts-meta { padding: 6px 10px; background: #fafafa; overflow: hidden; min-width: 0; }\n",
       ".ts-repr .ts-meta table { border-collapse: collapse; width: 100%; table-layout: fixed; }\n",
       ".ts-repr .ts-meta td { padding: 1px 8px 1px 0; white-space: nowrap; }\n",
       ".ts-repr .ts-meta td:first-child { color: #475569; font-weight: 600; width: 90px; }\n",
       ".ts-repr .ts-meta td:last-child { color: #1a1a1a; overflow: hidden; text-overflow: ellipsis; }\n",
       ".ts-repr .ts-data { padding: 6px 10px; }\n",
       ".ts-repr .ts-data table {\n",
       "  border-collapse: collapse; text-align: right;\n",
       "}\n",
       ".ts-repr .ts-data th {\n",
       "  text-align: right; padding: 3px 10px; border-bottom: 1px solid #ccc;\n",
       "  color: #555; font-weight: 600;\n",
       "}\n",
       ".ts-repr .ts-data th.ts-idx { text-align: left; }\n",
       ".ts-repr .ts-data td { padding: 2px 10px; }\n",
       ".ts-repr .ts-data tr:hover { background: #f5f5f5; }\n",
       ".ts-repr .ts-data td:first-child { text-align: left; color: #1e293b; }\n",
       ".ts-repr .ts-data td.ts-idx { text-align: left; color: #1e293b; }\n",
       ".ts-repr .ts-ellipsis { text-align: center !important; color: #999; }\n",
       "@media (prefers-color-scheme: dark) {\n",
       "  .ts-repr .ts-header { background: #1e293b; color: #e2e8f0; border-color: #475569; }\n",
       "  .ts-repr .ts-meta { background: #0f172a; }\n",
       "  .ts-repr .ts-meta td:first-child { color: #94a3b8; }\n",
       "  .ts-repr .ts-meta td:last-child { color: #e2e8f0; }\n",
       "  .ts-repr .ts-data th { color: #94a3b8; border-color: #334155; }\n",
       "  .ts-repr .ts-data td { color: #e2e8f0; }\n",
       "  .ts-repr .ts-data td:first-child { color: #cbd5e1; }\n",
       "  .ts-repr .ts-data td.ts-idx { color: #cbd5e1; }\n",
       "  .ts-repr .ts-data tr:hover { background: #1e293b; }\n",
       "  .ts-repr .ts-ellipsis { color: #64748b; }\n",
       "}\n",
       "</style>\n",
       "<div class=\"ts-repr\">\n",
       "<div class=\"ts-header\">HierarchicalTimeSeries</div>\n",
       "<div class=\"ts-meta\"><table>\n",
       "<tr><td>Name</td><td>Consumption from DataFrame</td></tr>\n",
       "<tr><td>Levels</td><td>root, region, city</td></tr>\n",
       "<tr><td>Nodes</td><td>7 (4 leaves)</td></tr>\n",
       "<tr><td>Frequency</td><td>PT1H</td></tr>\n",
       "<tr><td>Timezone</td><td>Europe/Oslo (+00:00)</td></tr>\n",
       "<tr><td>Aggregation</td><td>sum</td></tr>\n",
       "</table></div>\n",
       "<div class=\"ts-data\"><table style=\"text-align: left;\">\n",
       "<tr><th>name</th><th>level</th><th>length</th><th>begin</th><th>end</th></tr>\n",
       "<tr><td>Bodø</td><td>city</td><td>24</td><td>2024-01-15 00:00</td><td>2024-01-15 23:00</td></tr>\n",
       "<tr><td>Tromsø</td><td>city</td><td>24</td><td>2024-01-15 00:00</td><td>2024-01-15 23:00</td></tr>\n",
       "<tr><td>Bergen</td><td>city</td><td>24</td><td>2024-01-15 00:00</td><td>2024-01-15 23:00</td></tr>\n",
       "<tr><td>Oslo</td><td>city</td><td>24</td><td>2024-01-15 00:00</td><td>2024-01-15 23:00</td></tr>\n",
       "</table></div></div>"
      ],
      "text/plain": [
       "HierarchicalTimeSeries\n",
       "┌─────────────────────────────────────────────────────────────┐\n",
       "│  Name:             Consumption from DataFrame               │\n",
       "│  Levels:           root, region, city                       │\n",
       "│  Nodes:            7 (4 leaves)                             │\n",
       "│  Frequency:        PT1H                                     │\n",
       "│  Timezone:         Europe/Oslo (+00:00)                     │\n",
       "│  Aggregation:      sum                                      │\n",
       "├─────────────────────────────────────────────────────────────┤\n",
       "│  name    level  length  begin             end               │\n",
       "├─────────────────────────────────────────────────────────────┤\n",
       "│  Bodø    city   24      2024-01-15 00:00  2024-01-15 23:00  │\n",
       "│  Tromsø  city   24      2024-01-15 00:00  2024-01-15 23:00  │\n",
       "│  Bergen  city   24      2024-01-15 00:00  2024-01-15 23:00  │\n",
       "│  Oslo    city   24      2024-01-15 00:00  2024-01-15 23:00  │\n",
       "└─────────────────────────────────────────────────────────────┘"
      ]
     },
     "execution_count": 19,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "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-04T19:11:01.691487Z",
     "iopub.status.busy": "2026-03-04T19:11:01.691437Z",
     "iopub.status.idle": "2026-03-04T19:11:01.692960Z",
     "shell.execute_reply": "2026-03-04T19:11:01.692707Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Total from DataFrame hierarchy: mean=797.6 MW\n"
     ]
    }
   ],
   "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-04T19:11:01.693762Z",
     "iopub.status.busy": "2026-03-04T19:11:01.693708Z",
     "iopub.status.idle": "2026-03-04T19:11:01.696000Z",
     "shell.execute_reply": "2026-03-04T19:11:01.695670Z"
    }
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<style>\n",
       ".ts-repr { font-family: monospace; font-size: 13px; max-width: 640px; display: inline-grid; }\n",
       ".ts-repr .ts-header {\n",
       "  font-weight: bold; font-size: 14px;\n",
       "  padding: 6px 10px; border-bottom: 2px solid #4a4a4a;\n",
       "  background: #f0f0f0; color: #1a1a1a;\n",
       "}\n",
       ".ts-repr .ts-meta { padding: 6px 10px; background: #fafafa; overflow: hidden; min-width: 0; }\n",
       ".ts-repr .ts-meta table { border-collapse: collapse; width: 100%; table-layout: fixed; }\n",
       ".ts-repr .ts-meta td { padding: 1px 8px 1px 0; white-space: nowrap; }\n",
       ".ts-repr .ts-meta td:first-child { color: #475569; font-weight: 600; width: 90px; }\n",
       ".ts-repr .ts-meta td:last-child { color: #1a1a1a; overflow: hidden; text-overflow: ellipsis; }\n",
       ".ts-repr .ts-data { padding: 6px 10px; }\n",
       ".ts-repr .ts-data table {\n",
       "  border-collapse: collapse; text-align: right;\n",
       "}\n",
       ".ts-repr .ts-data th {\n",
       "  text-align: right; padding: 3px 10px; border-bottom: 1px solid #ccc;\n",
       "  color: #555; font-weight: 600;\n",
       "}\n",
       ".ts-repr .ts-data th.ts-idx { text-align: left; }\n",
       ".ts-repr .ts-data td { padding: 2px 10px; }\n",
       ".ts-repr .ts-data tr:hover { background: #f5f5f5; }\n",
       ".ts-repr .ts-data td:first-child { text-align: left; color: #1e293b; }\n",
       ".ts-repr .ts-data td.ts-idx { text-align: left; color: #1e293b; }\n",
       ".ts-repr .ts-ellipsis { text-align: center !important; color: #999; }\n",
       "@media (prefers-color-scheme: dark) {\n",
       "  .ts-repr .ts-header { background: #1e293b; color: #e2e8f0; border-color: #475569; }\n",
       "  .ts-repr .ts-meta { background: #0f172a; }\n",
       "  .ts-repr .ts-meta td:first-child { color: #94a3b8; }\n",
       "  .ts-repr .ts-meta td:last-child { color: #e2e8f0; }\n",
       "  .ts-repr .ts-data th { color: #94a3b8; border-color: #334155; }\n",
       "  .ts-repr .ts-data td { color: #e2e8f0; }\n",
       "  .ts-repr .ts-data td:first-child { color: #cbd5e1; }\n",
       "  .ts-repr .ts-data td.ts-idx { color: #cbd5e1; }\n",
       "  .ts-repr .ts-data tr:hover { background: #1e293b; }\n",
       "  .ts-repr .ts-ellipsis { color: #64748b; }\n",
       "}\n",
       "</style>\n",
       "<div class=\"ts-repr\">\n",
       "<div class=\"ts-header\">HierarchicalTimeSeries</div>\n",
       "<div class=\"ts-meta\"><table>\n",
       "<tr><td>Name</td><td>Energy Production</td></tr>\n",
       "<tr><td>Levels</td><td>total, source, farm</td></tr>\n",
       "<tr><td>Nodes</td><td>6 (3 leaves)</td></tr>\n",
       "<tr><td>Frequency</td><td>PT1H</td></tr>\n",
       "<tr><td>Timezone</td><td>Europe/Oslo (+00:00)</td></tr>\n",
       "<tr><td>Unit</td><td>MW</td></tr>\n",
       "<tr><td>Aggregation</td><td>sum</td></tr>\n",
       "</table></div>\n",
       "<div class=\"ts-data\"><table style=\"text-align: left;\">\n",
       "<tr><th>name</th><th>level</th><th>length</th><th>begin</th><th>end</th></tr>\n",
       "<tr><td>Farm A</td><td>farm</td><td>24</td><td>2024-01-15 00:00</td><td>2024-01-15 23:00</td></tr>\n",
       "<tr><td>Farm B</td><td>farm</td><td>24</td><td>2024-01-15 00:00</td><td>2024-01-15 23:00</td></tr>\n",
       "<tr><td>Plant X</td><td>farm</td><td>24</td><td>2024-01-15 00:00</td><td>2024-01-15 23:00</td></tr>\n",
       "</table></div></div>"
      ],
      "text/plain": [
       "HierarchicalTimeSeries\n",
       "┌──────────────────────────────────────────────────────────────┐\n",
       "│  Name:             Energy Production                         │\n",
       "│  Levels:           total, source, farm                       │\n",
       "│  Nodes:            6 (3 leaves)                              │\n",
       "│  Frequency:        PT1H                                      │\n",
       "│  Timezone:         Europe/Oslo (+00:00)                      │\n",
       "│  Unit:             MW                                        │\n",
       "│  Aggregation:      sum                                       │\n",
       "├──────────────────────────────────────────────────────────────┤\n",
       "│  name     level  length  begin             end               │\n",
       "├──────────────────────────────────────────────────────────────┤\n",
       "│  Farm A   farm   24      2024-01-15 00:00  2024-01-15 23:00  │\n",
       "│  Farm B   farm   24      2024-01-15 00:00  2024-01-15 23:00  │\n",
       "│  Plant X  farm   24      2024-01-15 00:00  2024-01-15 23:00  │\n",
       "└──────────────────────────────────────────────────────────────┘"
      ]
     },
     "execution_count": 21,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "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-04T19:11:01.696859Z",
     "iopub.status.busy": "2026-03-04T19:11:01.696806Z",
     "iopub.status.idle": "2026-03-04T19:11:01.698405Z",
     "shell.execute_reply": "2026-03-04T19:11:01.698078Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Wind      mean=179.2 MW\n",
      "Solar     mean=60.5 MW\n"
     ]
    }
   ],
   "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-04T19:11:01.699219Z",
     "iopub.status.busy": "2026-03-04T19:11:01.699159Z",
     "iopub.status.idle": "2026-03-04T19:11:01.700781Z",
     "shell.execute_reply": "2026-03-04T19:11:01.700452Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Total nodes:        8\n",
      "'Oslo' in tree:     True\n",
      "'Helsinki' in tree: False\n",
      "\n",
      "Bracket access:     Oslo (leaf=True)\n"
     ]
    }
   ],
   "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, TimeSeriesList]` |\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
}
