{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Cubes and Collections\n",
    "\n",
    "For multi-dimensional data (e.g., scenario x time, or region x time) use `TimeSeriesCube`. For grouping heterogeneous time series that don't share the same timestamps, use `TimeSeriesCollection`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## TimeSeriesCube\n",
    "\n",
    "A cube stores an N-dimensional array with named `Dimension` objects. Common use cases include ensemble forecasts, scenario analysis, and region-by-time grids."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:30.906752Z",
     "iopub.status.busy": "2026-03-01T13:36:30.906648Z",
     "iopub.status.idle": "2026-03-01T13:36:30.985422Z",
     "shell.execute_reply": "2026-03-01T13:36:30.985097Z"
    }
   },
   "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 hours = [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",
    "hours = [base + timedelta(hours=i) for i in range(24)]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Building a cube from scratch\n",
    "\n",
    "Create a 3-scenario x 24-hour cube representing price forecasts under different assumptions."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:31.000047Z",
     "iopub.status.busy": "2026-03-01T13:36:30.999945Z",
     "iopub.status.idle": "2026-03-01T13:36:31.151103Z",
     "shell.execute_reply": "2026-03-01T13:36:31.150680Z"
    }
   },
   "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[2]\u001b[39m\u001b[32m, line 10\u001b[39m\n\u001b[32m      2\u001b[39m base_prices = \u001b[32m50\u001b[39m + \u001b[32m20\u001b[39m * np.sin(np.linspace(\u001b[32m0\u001b[39m, \u001b[32m2\u001b[39m * np.pi, \u001b[32m24\u001b[39m))\n\u001b[32m      4\u001b[39m data = np.array([\n\u001b[32m      5\u001b[39m     base_prices * \u001b[32m0.8\u001b[39m + rng.normal(\u001b[32m0\u001b[39m, \u001b[32m2\u001b[39m, \u001b[32m24\u001b[39m),  \u001b[38;5;66;03m# low scenario\u001b[39;00m\n\u001b[32m      6\u001b[39m     base_prices + rng.normal(\u001b[32m0\u001b[39m, \u001b[32m2\u001b[39m, \u001b[32m24\u001b[39m),          \u001b[38;5;66;03m# base scenario\u001b[39;00m\n\u001b[32m      7\u001b[39m     base_prices * \u001b[32m1.3\u001b[39m + rng.normal(\u001b[32m0\u001b[39m, \u001b[32m3\u001b[39m, \u001b[32m24\u001b[39m),   \u001b[38;5;66;03m# high scenario\u001b[39;00m\n\u001b[32m      8\u001b[39m ])\n\u001b[32m---> \u001b[39m\u001b[32m10\u001b[39m cube = \u001b[43mtdm\u001b[49m.TimeSeriesCube(\n\u001b[32m     11\u001b[39m     tdm.Frequency.PT1H,\n\u001b[32m     12\u001b[39m     timezone=\u001b[33m\"\u001b[39m\u001b[33mUTC\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m     13\u001b[39m     name=\u001b[33m\"\u001b[39m\u001b[33mprice_forecast\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m     14\u001b[39m     unit=\u001b[33m\"\u001b[39m\u001b[33mEUR/MWh\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m     15\u001b[39m     data_type=tdm.DataType.FORECAST,\n\u001b[32m     16\u001b[39m     dimensions=[\n\u001b[32m     17\u001b[39m         tdm.Dimension(\u001b[33m\"\u001b[39m\u001b[33mscenario\u001b[39m\u001b[33m\"\u001b[39m, [\u001b[33m\"\u001b[39m\u001b[33mlow\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33mbase\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33mhigh\u001b[39m\u001b[33m\"\u001b[39m]),\n\u001b[32m     18\u001b[39m         tdm.Dimension(\u001b[33m\"\u001b[39m\u001b[33mvalid_time\u001b[39m\u001b[33m\"\u001b[39m, hours),\n\u001b[32m     19\u001b[39m     ],\n\u001b[32m     20\u001b[39m     values=data,\n\u001b[32m     21\u001b[39m )\n\u001b[32m     22\u001b[39m cube\n",
      "\u001b[31mNameError\u001b[39m: name 'tdm' is not defined"
     ]
    }
   ],
   "source": [
    "rng = np.random.default_rng(42)\n",
    "base_prices = 50 + 20 * np.sin(np.linspace(0, 2 * np.pi, 24))\n",
    "\n",
    "data = np.array([\n",
    "    base_prices * 0.8 + rng.normal(0, 2, 24),  # low scenario\n",
    "    base_prices + rng.normal(0, 2, 24),          # base scenario\n",
    "    base_prices * 1.3 + rng.normal(0, 3, 24),   # high scenario\n",
    "])\n",
    "\n",
    "cube = tdm.TimeSeriesCube(\n",
    "    tdm.Frequency.PT1H,\n",
    "    timezone=\"UTC\",\n",
    "    name=\"price_forecast\",\n",
    "    unit=\"EUR/MWh\",\n",
    "    data_type=tdm.DataType.FORECAST,\n",
    "    dimensions=[\n",
    "        tdm.Dimension(\"scenario\", [\"low\", \"base\", \"high\"]),\n",
    "        tdm.Dimension(\"valid_time\", hours),\n",
    "    ],\n",
    "    values=data,\n",
    ")\n",
    "cube"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Cube properties"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:31.152304Z",
     "iopub.status.busy": "2026-03-01T13:36:31.152224Z",
     "iopub.status.idle": "2026-03-01T13:36:31.160269Z",
     "shell.execute_reply": "2026-03-01T13:36:31.159870Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'cube' 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 \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mShape:      \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[43mcube\u001b[49m.shape\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[33mDimensions: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mcube.dim_names\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[33mBegin:      \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mcube.begin\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'cube' is not defined"
     ]
    }
   ],
   "source": [
    "print(f\"Shape:      {cube.shape}\")\n",
    "print(f\"Dimensions: {cube.dim_names}\")\n",
    "print(f\"Begin:      {cube.begin}\")\n",
    "print(f\"End:        {cube.end}\")\n",
    "print(f\"Has missing:{cube.has_missing}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Selecting with `sel()` — label-based\n",
    "\n",
    "Select a single scenario to collapse the cube into a `TimeSeries`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:31.161514Z",
     "iopub.status.busy": "2026-03-01T13:36:31.161449Z",
     "iopub.status.idle": "2026-03-01T13:36:31.168221Z",
     "shell.execute_reply": "2026-03-01T13:36:31.167885Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'cube' 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 base_scenario = \u001b[43mcube\u001b[49m.sel(scenario=\u001b[33m\"\u001b[39m\u001b[33mbase\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m      2\u001b[39m base_scenario\n",
      "\u001b[31mNameError\u001b[39m: name 'cube' is not defined"
     ]
    }
   ],
   "source": [
    "base_scenario = cube.sel(scenario=\"base\")\n",
    "base_scenario"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Selecting with `isel()` — index-based\n",
    "\n",
    "Select by integer position."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:31.169100Z",
     "iopub.status.busy": "2026-03-01T13:36:31.169043Z",
     "iopub.status.idle": "2026-03-01T13:36:31.175581Z",
     "shell.execute_reply": "2026-03-01T13:36:31.175301Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'cube' 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 first_scenario = \u001b[43mcube\u001b[49m.isel(scenario=\u001b[32m0\u001b[39m)\n\u001b[32m      2\u001b[39m first_scenario\n",
      "\u001b[31mNameError\u001b[39m: name 'cube' is not defined"
     ]
    }
   ],
   "source": [
    "first_scenario = cube.isel(scenario=0)\n",
    "first_scenario"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Slicing a dimension\n",
    "\n",
    "Select a range of labels to get a smaller cube or table."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:31.176687Z",
     "iopub.status.busy": "2026-03-01T13:36:31.176635Z",
     "iopub.status.idle": "2026-03-01T13:36:31.183428Z",
     "shell.execute_reply": "2026-03-01T13:36:31.183054Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'cube' 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 two_scenarios = \u001b[43mcube\u001b[49m.sel(scenario=\u001b[38;5;28mslice\u001b[39m(\u001b[33m\"\u001b[39m\u001b[33mlow\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33mbase\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[33mType:  \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mtype\u001b[39m(two_scenarios).\u001b[34m__name__\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m      3\u001b[39m \u001b[38;5;28mprint\u001b[39m(two_scenarios)\n",
      "\u001b[31mNameError\u001b[39m: name 'cube' is not defined"
     ]
    }
   ],
   "source": [
    "two_scenarios = cube.sel(scenario=slice(\"low\", \"base\"))\n",
    "print(f\"Type:  {type(two_scenarios).__name__}\")\n",
    "print(two_scenarios)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Auto-collapse to Table or Series\n",
    "\n",
    "When a `sel()` or `isel()` call removes enough dimensions, the result automatically becomes a `TimeSeriesTable` (2D) or `TimeSeries` (1D)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:31.184397Z",
     "iopub.status.busy": "2026-03-01T13:36:31.184341Z",
     "iopub.status.idle": "2026-03-01T13:36:31.197155Z",
     "shell.execute_reply": "2026-03-01T13:36:31.196717Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'cube' 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 table_view = \u001b[43mcube\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[33mType: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mtype\u001b[39m(table_view).\u001b[34m__name__\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m      3\u001b[39m table_view\n",
      "\u001b[31mNameError\u001b[39m: name 'cube' is not defined"
     ]
    }
   ],
   "source": [
    "table_view = cube.to_table()\n",
    "print(f\"Type: {type(table_view).__name__}\")\n",
    "table_view"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Building a cube from a list of TimeSeries\n",
    "\n",
    "`from_timeseries_list()` is handy when you already have individual scenario forecasts."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:31.198243Z",
     "iopub.status.busy": "2026-03-01T13:36:31.198188Z",
     "iopub.status.idle": "2026-03-01T13:36:31.205790Z",
     "shell.execute_reply": "2026-03-01T13:36:31.205464Z"
    }
   },
   "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[8]\u001b[39m\u001b[32m, line 2\u001b[39m\n\u001b[32m      1\u001b[39m series_list = [\n\u001b[32m----> \u001b[39m\u001b[32m2\u001b[39m     \u001b[43mtdm\u001b[49m.TimeSeries(\n\u001b[32m      3\u001b[39m         tdm.Frequency.PT1H,\n\u001b[32m      4\u001b[39m         timestamps=hours,\n\u001b[32m      5\u001b[39m         values=(base_prices * factor + rng.normal(\u001b[32m0\u001b[39m, \u001b[32m2\u001b[39m, \u001b[32m24\u001b[39m)).tolist(),\n\u001b[32m      6\u001b[39m         name=\u001b[33m\"\u001b[39m\u001b[33mprice\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      7\u001b[39m         unit=\u001b[33m\"\u001b[39m\u001b[33mEUR/MWh\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      8\u001b[39m     )\n\u001b[32m      9\u001b[39m     \u001b[38;5;28;01mfor\u001b[39;00m factor \u001b[38;5;129;01min\u001b[39;00m [\u001b[32m0.7\u001b[39m, \u001b[32m0.85\u001b[39m, \u001b[32m1.0\u001b[39m, \u001b[32m1.15\u001b[39m, \u001b[32m1.3\u001b[39m]\n\u001b[32m     10\u001b[39m ]\n\u001b[32m     12\u001b[39m ensemble = tdm.TimeSeriesCube.from_timeseries_list(\n\u001b[32m     13\u001b[39m     series_list,\n\u001b[32m     14\u001b[39m     dimension=tdm.Dimension(\u001b[33m\"\u001b[39m\u001b[33mpercentile\u001b[39m\u001b[33m\"\u001b[39m, [\u001b[33m\"\u001b[39m\u001b[33mp10\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33mp25\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33mp50\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33mp75\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33mp90\u001b[39m\u001b[33m\"\u001b[39m]),\n\u001b[32m     15\u001b[39m     name=\u001b[33m\"\u001b[39m\u001b[33mprice_ensemble\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m     16\u001b[39m )\n\u001b[32m     17\u001b[39m ensemble\n",
      "\u001b[31mNameError\u001b[39m: name 'tdm' is not defined"
     ]
    }
   ],
   "source": [
    "series_list = [\n",
    "    tdm.TimeSeries(\n",
    "        tdm.Frequency.PT1H,\n",
    "        timestamps=hours,\n",
    "        values=(base_prices * factor + rng.normal(0, 2, 24)).tolist(),\n",
    "        name=\"price\",\n",
    "        unit=\"EUR/MWh\",\n",
    "    )\n",
    "    for factor in [0.7, 0.85, 1.0, 1.15, 1.3]\n",
    "]\n",
    "\n",
    "ensemble = tdm.TimeSeriesCube.from_timeseries_list(\n",
    "    series_list,\n",
    "    dimension=tdm.Dimension(\"percentile\", [\"p10\", \"p25\", \"p50\", \"p75\", \"p90\"]),\n",
    "    name=\"price_ensemble\",\n",
    ")\n",
    "ensemble"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "## TimeSeriesCollection\n",
    "\n",
    "A `TimeSeriesCollection` groups time series that may have different frequencies, time ranges, or numbers of points. Think of it as a named bag of `TimeSeries` and `TimeSeriesTable` objects."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:31.206738Z",
     "iopub.status.busy": "2026-03-01T13:36:31.206680Z",
     "iopub.status.idle": "2026-03-01T13:36:31.215766Z",
     "shell.execute_reply": "2026-03-01T13:36:31.215318Z"
    }
   },
   "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[9]\u001b[39m\u001b[32m, line 3\u001b[39m\n\u001b[32m      1\u001b[39m daily_base = datetime(\u001b[32m2024\u001b[39m, \u001b[32m1\u001b[39m, \u001b[32m1\u001b[39m, tzinfo=timezone.utc)\n\u001b[32m----> \u001b[39m\u001b[32m3\u001b[39m ts_hourly = \u001b[43mtdm\u001b[49m.TimeSeries(\n\u001b[32m      4\u001b[39m     tdm.Frequency.PT1H,\n\u001b[32m      5\u001b[39m     timestamps=hours,\n\u001b[32m      6\u001b[39m     values=[\u001b[32m100.0\u001b[39m + rng.normal(\u001b[32m0\u001b[39m, \u001b[32m10\u001b[39m) \u001b[38;5;28;01mfor\u001b[39;00m _ \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mrange\u001b[39m(\u001b[32m24\u001b[39m)],\n\u001b[32m      7\u001b[39m     name=\u001b[33m\"\u001b[39m\u001b[33mwind_hourly\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      8\u001b[39m     unit=\u001b[33m\"\u001b[39m\u001b[33mMW\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      9\u001b[39m )\n\u001b[32m     11\u001b[39m ts_daily = tdm.TimeSeries(\n\u001b[32m     12\u001b[39m     tdm.Frequency.P1D,\n\u001b[32m     13\u001b[39m     timestamps=[daily_base + timedelta(days=d) \u001b[38;5;28;01mfor\u001b[39;00m d \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mrange\u001b[39m(\u001b[32m30\u001b[39m)],\n\u001b[32m   (...)\u001b[39m\u001b[32m     16\u001b[39m     unit=\u001b[33m\"\u001b[39m\u001b[33mMWh\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m     17\u001b[39m )\n\u001b[32m     19\u001b[39m ts_15min = tdm.TimeSeries(\n\u001b[32m     20\u001b[39m     tdm.Frequency.PT15M,\n\u001b[32m     21\u001b[39m     timestamps=[base + timedelta(minutes=\u001b[32m15\u001b[39m * 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[32m96\u001b[39m)],\n\u001b[32m   (...)\u001b[39m\u001b[32m     24\u001b[39m     unit=\u001b[33m\"\u001b[39m\u001b[33mMW\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m     25\u001b[39m )\n",
      "\u001b[31mNameError\u001b[39m: name 'tdm' is not defined"
     ]
    }
   ],
   "source": [
    "daily_base = datetime(2024, 1, 1, tzinfo=timezone.utc)\n",
    "\n",
    "ts_hourly = tdm.TimeSeries(\n",
    "    tdm.Frequency.PT1H,\n",
    "    timestamps=hours,\n",
    "    values=[100.0 + rng.normal(0, 10) for _ in range(24)],\n",
    "    name=\"wind_hourly\",\n",
    "    unit=\"MW\",\n",
    ")\n",
    "\n",
    "ts_daily = tdm.TimeSeries(\n",
    "    tdm.Frequency.P1D,\n",
    "    timestamps=[daily_base + timedelta(days=d) for d in range(30)],\n",
    "    values=[2400.0 + rng.normal(0, 200) for _ in range(30)],\n",
    "    name=\"wind_daily_energy\",\n",
    "    unit=\"MWh\",\n",
    ")\n",
    "\n",
    "ts_15min = tdm.TimeSeries(\n",
    "    tdm.Frequency.PT15M,\n",
    "    timestamps=[base + timedelta(minutes=15 * i) for i in range(96)],\n",
    "    values=[50.0 + rng.normal(0, 5) for _ in range(96)],\n",
    "    name=\"solar_15min\",\n",
    "    unit=\"MW\",\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Creating a collection"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:31.216703Z",
     "iopub.status.busy": "2026-03-01T13:36:31.216655Z",
     "iopub.status.idle": "2026-03-01T13:36:31.222742Z",
     "shell.execute_reply": "2026-03-01T13:36:31.222474Z"
    }
   },
   "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[10]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m collection = \u001b[43mtdm\u001b[49m.TimeSeriesCollection(\n\u001b[32m      2\u001b[39m     [ts_hourly, ts_daily, ts_15min],\n\u001b[32m      3\u001b[39m     name=\u001b[33m\"\u001b[39m\u001b[33mPlant overview\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      4\u001b[39m     description=\u001b[33m\"\u001b[39m\u001b[33mMixed-frequency data for a single plant\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      5\u001b[39m )\n\u001b[32m      6\u001b[39m collection\n",
      "\u001b[31mNameError\u001b[39m: name 'tdm' is not defined"
     ]
    }
   ],
   "source": [
    "collection = tdm.TimeSeriesCollection(\n",
    "    [ts_hourly, ts_daily, ts_15min],\n",
    "    name=\"Plant overview\",\n",
    "    description=\"Mixed-frequency data for a single plant\",\n",
    ")\n",
    "collection"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Dictionary-like access"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:31.223735Z",
     "iopub.status.busy": "2026-03-01T13:36:31.223676Z",
     "iopub.status.idle": "2026-03-01T13:36:31.229890Z",
     "shell.execute_reply": "2026-03-01T13:36:31.229527Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'collection' 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 \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mNames: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[43mcollection\u001b[49m.names\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[33mCount: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mcollection.series_count\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m      4\u001b[39m collection[\u001b[33m\"\u001b[39m\u001b[33mwind_hourly\u001b[39m\u001b[33m\"\u001b[39m]\n",
      "\u001b[31mNameError\u001b[39m: name 'collection' is not defined"
     ]
    }
   ],
   "source": [
    "print(f\"Names: {collection.names}\")\n",
    "print(f\"Count: {collection.series_count}\")\n",
    "\n",
    "collection[\"wind_hourly\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Adding and removing series\n",
    "\n",
    "Collections are immutable — `add()` and `remove()` return new collections."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:31.230784Z",
     "iopub.status.busy": "2026-03-01T13:36:31.230734Z",
     "iopub.status.idle": "2026-03-01T13:36:31.237643Z",
     "shell.execute_reply": "2026-03-01T13:36:31.237288Z"
    }
   },
   "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[12]\u001b[39m\u001b[32m, line 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m ts_price = \u001b[43mtdm\u001b[49m.TimeSeries(\n\u001b[32m      2\u001b[39m     tdm.Frequency.PT1H,\n\u001b[32m      3\u001b[39m     timestamps=hours,\n\u001b[32m      4\u001b[39m     values=[\u001b[32m45.0\u001b[39m + rng.normal(\u001b[32m0\u001b[39m, \u001b[32m8\u001b[39m) \u001b[38;5;28;01mfor\u001b[39;00m _ \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mrange\u001b[39m(\u001b[32m24\u001b[39m)],\n\u001b[32m      5\u001b[39m     name=\u001b[33m\"\u001b[39m\u001b[33mspot_price\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      6\u001b[39m     unit=\u001b[33m\"\u001b[39m\u001b[33mEUR/MWh\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      7\u001b[39m )\n\u001b[32m      9\u001b[39m extended = collection.add(ts_price)\n\u001b[32m     10\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mOriginal: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mcollection.names\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'tdm' is not defined"
     ]
    }
   ],
   "source": [
    "ts_price = tdm.TimeSeries(\n",
    "    tdm.Frequency.PT1H,\n",
    "    timestamps=hours,\n",
    "    values=[45.0 + rng.normal(0, 8) for _ in range(24)],\n",
    "    name=\"spot_price\",\n",
    "    unit=\"EUR/MWh\",\n",
    ")\n",
    "\n",
    "extended = collection.add(ts_price)\n",
    "print(f\"Original: {collection.names}\")\n",
    "print(f\"Extended: {extended.names}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:31.238473Z",
     "iopub.status.busy": "2026-03-01T13:36:31.238425Z",
     "iopub.status.idle": "2026-03-01T13:36:31.244447Z",
     "shell.execute_reply": "2026-03-01T13:36:31.244095Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'extended' 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 reduced = \u001b[43mextended\u001b[49m.remove(\u001b[33m\"\u001b[39m\u001b[33mwind_daily_energy\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[33mReduced: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mreduced.names\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'extended' is not defined"
     ]
    }
   ],
   "source": [
    "reduced = extended.remove(\"wind_daily_energy\")\n",
    "print(f\"Reduced: {reduced.names}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Iterating over a collection"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:31.245360Z",
     "iopub.status.busy": "2026-03-01T13:36:31.245298Z",
     "iopub.status.idle": "2026-03-01T13:36:31.252076Z",
     "shell.execute_reply": "2026-03-01T13:36:31.251701Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'collection' 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 \u001b[38;5;28;01mfor\u001b[39;00m name, series \u001b[38;5;129;01min\u001b[39;00m \u001b[43mcollection\u001b[49m.items():\n\u001b[32m      2\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[33m20s\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m  freq=\u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mstr\u001b[39m(series.frequency)\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m5s\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m  len=\u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mlen\u001b[39m(series)\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m3d\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m  begin=\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mseries.begin\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'collection' is not defined"
     ]
    }
   ],
   "source": [
    "for name, series in collection.items():\n",
    "    print(f\"{name:20s}  freq={str(series.frequency):5s}  len={len(series):3d}  begin={series.begin}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Summary\n",
    "\n",
    "- **`TimeSeriesCube`**: N-dimensional time series with `Dimension` labels; slice with `sel()` / `isel()`; auto-collapses to Table or Series\n",
    "- **`TimeSeriesCollection`**: heterogeneous container for series with different frequencies and time ranges; dictionary-like access; immutable add/remove\n",
    "\n",
    "Next up: **nb_07** covers data quality tools — coverage bars and validation."
   ]
  }
 ],
 "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
}
