{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# I/O and Interoperability\n",
    "\n",
    "TimeDataModel is designed to sit between your domain logic and the broader Python data ecosystem. This notebook shows how to move data seamlessly between `TimeSeries` / `TimeSeriesTable` and pandas, numpy, polars, JSON, and CSV."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:40.151602Z",
     "iopub.status.busy": "2026-03-01T13:36:40.151472Z",
     "iopub.status.idle": "2026-03-01T13:36:40.234269Z",
     "shell.execute_reply": "2026-03-01T13:36:40.233828Z"
    }
   },
   "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",
    "values = [100.0 + 50 * np.sin(2 * np.pi * i / 24) for i in range(24)]\n",
    "\n",
    "ts = tdm.TimeSeries(\n",
    "    tdm.Frequency.PT1H,\n",
    "    timezone=\"UTC\",\n",
    "    timestamps=timestamps,\n",
    "    values=values,\n",
    "    name=\"power\",\n",
    "    unit=\"MW\",\n",
    "    description=\"Synthetic daily power curve\",\n",
    "    data_type=tdm.DataType.SYNTHETIC,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## NumPy\n",
    "\n",
    "`to_numpy()` returns a float64 array (None becomes NaN). Use it when you need fast vectorized computation."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:40.235486Z",
     "iopub.status.busy": "2026-03-01T13:36:40.235420Z",
     "iopub.status.idle": "2026-03-01T13:36:40.242412Z",
     "shell.execute_reply": "2026-03-01T13:36:40.242128Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'ts' 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 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m arr = \u001b[43mts\u001b[49m.to_numpy()\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(arr)\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[33mShape: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00marr.shape\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'ts' is not defined"
     ]
    }
   ],
   "source": [
    "arr = ts.to_numpy()\n",
    "print(f\"Type:  {type(arr)}\")\n",
    "print(f\"Shape: {arr.shape}\")\n",
    "print(f\"Mean:  {arr.mean():.2f} MW\")\n",
    "print(f\"Max:   {arr.max():.2f} MW\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Pandas — export\n",
    "\n",
    "`to_pandas_dataframe()` produces a DataFrame with a DatetimeIndex."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:40.243523Z",
     "iopub.status.busy": "2026-03-01T13:36:40.243454Z",
     "iopub.status.idle": "2026-03-01T13:36:40.249719Z",
     "shell.execute_reply": "2026-03-01T13:36:40.249368Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'ts' 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 df = \u001b[43mts\u001b[49m.to_pandas_dataframe()\n\u001b[32m      2\u001b[39m df.head()\n",
      "\u001b[31mNameError\u001b[39m: name 'ts' is not defined"
     ]
    }
   ],
   "source": [
    "df = ts.to_pandas_dataframe()\n",
    "df.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Pandas — import with `from_pandas()`\n",
    "\n",
    "Create a TimeSeries from any pandas DataFrame that has a DatetimeIndex. Frequency and timezone are auto-inferred when possible."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:40.250757Z",
     "iopub.status.busy": "2026-03-01T13:36:40.250707Z",
     "iopub.status.idle": "2026-03-01T13:36:40.364418Z",
     "shell.execute_reply": "2026-03-01T13:36:40.363979Z"
    }
   },
   "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[4]\u001b[39m\u001b[32m, line 6\u001b[39m\n\u001b[32m      3\u001b[39m idx = pd.date_range(\u001b[33m\"\u001b[39m\u001b[33m2024-06-01\u001b[39m\u001b[33m\"\u001b[39m, periods=\u001b[32m48\u001b[39m, freq=\u001b[33m\"\u001b[39m\u001b[33mh\u001b[39m\u001b[33m\"\u001b[39m, tz=\u001b[33m\"\u001b[39m\u001b[33mUTC\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m      4\u001b[39m df_external = pd.DataFrame({\u001b[33m\"\u001b[39m\u001b[33mload\u001b[39m\u001b[33m\"\u001b[39m: np.random.default_rng(\u001b[32m42\u001b[39m).normal(\u001b[32m150\u001b[39m, \u001b[32m20\u001b[39m, \u001b[32m48\u001b[39m)}, index=idx)\n\u001b[32m----> \u001b[39m\u001b[32m6\u001b[39m ts_from_pd = \u001b[43mtdm\u001b[49m.TimeSeries.from_pandas(df_external, unit=\u001b[33m\"\u001b[39m\u001b[33mMW\u001b[39m\u001b[33m\"\u001b[39m, data_type=tdm.DataType.MEASUREMENT)\n\u001b[32m      7\u001b[39m ts_from_pd\n",
      "\u001b[31mNameError\u001b[39m: name 'tdm' is not defined"
     ]
    }
   ],
   "source": [
    "import pandas as pd\n",
    "\n",
    "idx = pd.date_range(\"2024-06-01\", periods=48, freq=\"h\", tz=\"UTC\")\n",
    "df_external = pd.DataFrame({\"load\": np.random.default_rng(42).normal(150, 20, 48)}, index=idx)\n",
    "\n",
    "ts_from_pd = tdm.TimeSeries.from_pandas(df_external, unit=\"MW\", data_type=tdm.DataType.MEASUREMENT)\n",
    "ts_from_pd"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Pandas — TimeSeriesTable round-trip"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:40.365581Z",
     "iopub.status.busy": "2026-03-01T13:36:40.365507Z",
     "iopub.status.idle": "2026-03-01T13:36:40.373955Z",
     "shell.execute_reply": "2026-03-01T13:36:40.373570Z"
    }
   },
   "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[5]\u001b[39m\u001b[32m, line 2\u001b[39m\n\u001b[32m      1\u001b[39m rng = np.random.default_rng(\u001b[32m42\u001b[39m)\n\u001b[32m----> \u001b[39m\u001b[32m2\u001b[39m table = \u001b[43mtdm\u001b[49m.TimeSeriesTable(\n\u001b[32m      3\u001b[39m     tdm.Frequency.PT1H,\n\u001b[32m      4\u001b[39m     timestamps=timestamps,\n\u001b[32m      5\u001b[39m     values=np.column_stack([\n\u001b[32m      6\u001b[39m         \u001b[32m100\u001b[39m + rng.normal(\u001b[32m0\u001b[39m, \u001b[32m10\u001b[39m, \u001b[32m24\u001b[39m),\n\u001b[32m      7\u001b[39m         \u001b[32m50\u001b[39m + rng.normal(\u001b[32m0\u001b[39m, \u001b[32m5\u001b[39m, \u001b[32m24\u001b[39m),\n\u001b[32m      8\u001b[39m     ]),\n\u001b[32m      9\u001b[39m     names=[\u001b[33m\"\u001b[39m\u001b[33mwind\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33msolar\u001b[39m\u001b[33m\"\u001b[39m],\n\u001b[32m     10\u001b[39m     units=[\u001b[33m\"\u001b[39m\u001b[33mMW\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33mMW\u001b[39m\u001b[33m\"\u001b[39m],\n\u001b[32m     11\u001b[39m )\n\u001b[32m     13\u001b[39m df_table = table.to_pandas_dataframe()\n\u001b[32m     14\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;00m\u001b[38;5;28mlist\u001b[39m(df_table.columns)\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'tdm' is not defined"
     ]
    }
   ],
   "source": [
    "rng = np.random.default_rng(42)\n",
    "table = tdm.TimeSeriesTable(\n",
    "    tdm.Frequency.PT1H,\n",
    "    timestamps=timestamps,\n",
    "    values=np.column_stack([\n",
    "        100 + rng.normal(0, 10, 24),\n",
    "        50 + rng.normal(0, 5, 24),\n",
    "    ]),\n",
    "    names=[\"wind\", \"solar\"],\n",
    "    units=[\"MW\", \"MW\"],\n",
    ")\n",
    "\n",
    "df_table = table.to_pandas_dataframe()\n",
    "print(f\"Columns: {list(df_table.columns)}\")\n",
    "\n",
    "table_back = tdm.TimeSeriesTable.from_pandas(df_table, units=[\"MW\", \"MW\"])\n",
    "print(f\"Round-trip equals: {table.equals(table_back)}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Polars (optional)\n",
    "\n",
    "If polars is installed, you get `to_polars_dataframe()` and `from_polars()`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:40.374944Z",
     "iopub.status.busy": "2026-03-01T13:36:40.374886Z",
     "iopub.status.idle": "2026-03-01T13:36:40.382511Z",
     "shell.execute_reply": "2026-03-01T13:36:40.382102Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'ts' 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 2\u001b[39m\n\u001b[32m      1\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m----> \u001b[39m\u001b[32m2\u001b[39m     df_pl = \u001b[43mts\u001b[49m.to_polars_dataframe()\n\u001b[32m      3\u001b[39m     \u001b[38;5;28mprint\u001b[39m(df_pl.head())\n\u001b[32m      5\u001b[39m     ts_from_pl = tdm.TimeSeries.from_polars(df_pl, tdm.Frequency.PT1H, unit=\u001b[33m\"\u001b[39m\u001b[33mMW\u001b[39m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'ts' is not defined"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    df_pl = ts.to_polars_dataframe()\n",
    "    print(df_pl.head())\n",
    "\n",
    "    ts_from_pl = tdm.TimeSeries.from_polars(df_pl, tdm.Frequency.PT1H, unit=\"MW\")\n",
    "    print(f\"\\nRound-trip length: {len(ts_from_pl)}\")\n",
    "except ImportError:\n",
    "    print(\"polars not installed — skip this cell or: pip install timedatamodel[polars]\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## JSON serialization\n",
    "\n",
    "`to_json()` produces an ISO-8601 JSON string. `from_json()` reconstructs the series with full metadata."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:40.383801Z",
     "iopub.status.busy": "2026-03-01T13:36:40.383727Z",
     "iopub.status.idle": "2026-03-01T13:36:40.390464Z",
     "shell.execute_reply": "2026-03-01T13:36:40.390080Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'ts' 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 json_str = \u001b[43mts\u001b[49m.to_json()\n\u001b[32m      2\u001b[39m \u001b[38;5;28mprint\u001b[39m(json_str[:\u001b[32m200\u001b[39m], \u001b[33m\"\u001b[39m\u001b[33m...\u001b[39m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'ts' is not defined"
     ]
    }
   ],
   "source": [
    "json_str = ts.to_json()\n",
    "print(json_str[:200], \"...\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:40.391364Z",
     "iopub.status.busy": "2026-03-01T13:36:40.391300Z",
     "iopub.status.idle": "2026-03-01T13:36:40.398975Z",
     "shell.execute_reply": "2026-03-01T13:36:40.398615Z"
    }
   },
   "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 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m ts_restored = \u001b[43mtdm\u001b[49m.TimeSeries.from_json(json_str)\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;00mts_restored.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[33mUnit:      \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mts_restored.unit\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'tdm' is not defined"
     ]
    }
   ],
   "source": [
    "ts_restored = tdm.TimeSeries.from_json(json_str)\n",
    "print(f\"Name:      {ts_restored.name}\")\n",
    "print(f\"Unit:      {ts_restored.unit}\")\n",
    "print(f\"Frequency: {ts_restored.frequency}\")\n",
    "print(f\"Length:    {len(ts_restored)}\")\n",
    "print(f\"Equals original: {ts.equals(ts_restored)}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### JSON for TimeSeriesTable"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:40.399937Z",
     "iopub.status.busy": "2026-03-01T13:36:40.399883Z",
     "iopub.status.idle": "2026-03-01T13:36:40.406869Z",
     "shell.execute_reply": "2026-03-01T13:36:40.406416Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'table' 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 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m json_table = \u001b[43mtable\u001b[49m.to_json()\n\u001b[32m      2\u001b[39m table_restored = tdm.TimeSeriesTable.from_json(json_table)\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_restored.column_names\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'table' is not defined"
     ]
    }
   ],
   "source": [
    "json_table = table.to_json()\n",
    "table_restored = tdm.TimeSeriesTable.from_json(json_table)\n",
    "print(f\"Columns: {table_restored.column_names}\")\n",
    "print(f\"Equals original: {table.equals(table_restored)}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## CSV serialization\n",
    "\n",
    "`to_csv()` and `from_csv()` write and read simple CSV files."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:40.407819Z",
     "iopub.status.busy": "2026-03-01T13:36:40.407757Z",
     "iopub.status.idle": "2026-03-01T13:36:40.416376Z",
     "shell.execute_reply": "2026-03-01T13:36:40.415948Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'ts' 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 6\u001b[39m\n\u001b[32m      4\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m tempfile.TemporaryDirectory() \u001b[38;5;28;01mas\u001b[39;00m tmp:\n\u001b[32m      5\u001b[39m     csv_path = Path(tmp) / \u001b[33m\"\u001b[39m\u001b[33mpower.csv\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m6\u001b[39m     \u001b[43mts\u001b[49m.to_csv(csv_path)\n\u001b[32m      8\u001b[39m     \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28mopen\u001b[39m(csv_path) \u001b[38;5;28;01mas\u001b[39;00m f:\n\u001b[32m      9\u001b[39m         \u001b[38;5;28;01mfor\u001b[39;00m i, line \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28menumerate\u001b[39m(f):\n",
      "\u001b[31mNameError\u001b[39m: name 'ts' is not defined"
     ]
    }
   ],
   "source": [
    "import tempfile\n",
    "from pathlib import Path\n",
    "\n",
    "with tempfile.TemporaryDirectory() as tmp:\n",
    "    csv_path = Path(tmp) / \"power.csv\"\n",
    "    ts.to_csv(csv_path)\n",
    "\n",
    "    with open(csv_path) as f:\n",
    "        for i, line in enumerate(f):\n",
    "            print(line.rstrip())\n",
    "            if i >= 4:\n",
    "                print(\"...\")\n",
    "                break"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:40.417389Z",
     "iopub.status.busy": "2026-03-01T13:36:40.417334Z",
     "iopub.status.idle": "2026-03-01T13:36:40.425181Z",
     "shell.execute_reply": "2026-03-01T13:36:40.424779Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'ts' 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 3\u001b[39m\n\u001b[32m      1\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m tempfile.TemporaryDirectory() \u001b[38;5;28;01mas\u001b[39;00m tmp:\n\u001b[32m      2\u001b[39m     csv_path = Path(tmp) / \u001b[33m\"\u001b[39m\u001b[33mpower.csv\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m3\u001b[39m     \u001b[43mts\u001b[49m.to_csv(csv_path)\n\u001b[32m      4\u001b[39m     ts_from_csv = tdm.TimeSeries.from_csv(csv_path, tdm.Frequency.PT1H, unit=\u001b[33m\"\u001b[39m\u001b[33mMW\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m      6\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(ts_from_csv)\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'ts' is not defined"
     ]
    }
   ],
   "source": [
    "with tempfile.TemporaryDirectory() as tmp:\n",
    "    csv_path = Path(tmp) / \"power.csv\"\n",
    "    ts.to_csv(csv_path)\n",
    "    ts_from_csv = tdm.TimeSeries.from_csv(csv_path, tdm.Frequency.PT1H, unit=\"MW\")\n",
    "\n",
    "print(f\"Length: {len(ts_from_csv)}\")\n",
    "print(f\"Name:   {ts_from_csv.name}\")\n",
    "ts_from_csv.head()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Multi-index round-trip\n",
    "\n",
    "TimeSeries supports tuple-based timestamps for hierarchical indexing. This is preserved through pandas and JSON round-trips."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:40.426136Z",
     "iopub.status.busy": "2026-03-01T13:36:40.426074Z",
     "iopub.status.idle": "2026-03-01T13:36:40.436451Z",
     "shell.execute_reply": "2026-03-01T13:36:40.436047Z"
    }
   },
   "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 18\u001b[39m\n\u001b[32m      1\u001b[39m issue_times = [\n\u001b[32m      2\u001b[39m     datetime(\u001b[32m2024\u001b[39m, \u001b[32m1\u001b[39m, \u001b[32m15\u001b[39m, \u001b[32m0\u001b[39m, tzinfo=timezone.utc),\n\u001b[32m      3\u001b[39m     datetime(\u001b[32m2024\u001b[39m, \u001b[32m1\u001b[39m, \u001b[32m15\u001b[39m, \u001b[32m0\u001b[39m, tzinfo=timezone.utc),\n\u001b[32m   (...)\u001b[39m\u001b[32m      7\u001b[39m     datetime(\u001b[32m2024\u001b[39m, \u001b[32m1\u001b[39m, \u001b[32m15\u001b[39m, \u001b[32m12\u001b[39m, tzinfo=timezone.utc),\n\u001b[32m      8\u001b[39m ]\n\u001b[32m      9\u001b[39m valid_times = [\n\u001b[32m     10\u001b[39m     datetime(\u001b[32m2024\u001b[39m, \u001b[32m1\u001b[39m, \u001b[32m15\u001b[39m, \u001b[32m0\u001b[39m, tzinfo=timezone.utc),\n\u001b[32m     11\u001b[39m     datetime(\u001b[32m2024\u001b[39m, \u001b[32m1\u001b[39m, \u001b[32m15\u001b[39m, \u001b[32m1\u001b[39m, tzinfo=timezone.utc),\n\u001b[32m   (...)\u001b[39m\u001b[32m     15\u001b[39m     datetime(\u001b[32m2024\u001b[39m, \u001b[32m1\u001b[39m, \u001b[32m15\u001b[39m, \u001b[32m14\u001b[39m, tzinfo=timezone.utc),\n\u001b[32m     16\u001b[39m ]\n\u001b[32m---> \u001b[39m\u001b[32m18\u001b[39m ts_multi = \u001b[43mtdm\u001b[49m.TimeSeries(\n\u001b[32m     19\u001b[39m     tdm.Frequency.PT1H,\n\u001b[32m     20\u001b[39m     timestamps=\u001b[38;5;28mlist\u001b[39m(\u001b[38;5;28mzip\u001b[39m(issue_times, valid_times)),\n\u001b[32m     21\u001b[39m     values=[\u001b[32m100.0\u001b[39m, \u001b[32m105.0\u001b[39m, \u001b[32m110.0\u001b[39m, \u001b[32m95.0\u001b[39m, \u001b[32m100.0\u001b[39m, \u001b[32m108.0\u001b[39m],\n\u001b[32m     22\u001b[39m     name=\u001b[33m\"\u001b[39m\u001b[33mforecast\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m     23\u001b[39m     unit=\u001b[33m\"\u001b[39m\u001b[33mMW\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m     24\u001b[39m     index_names=[\u001b[33m\"\u001b[39m\u001b[33missue_time\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33mvalid_time\u001b[39m\u001b[33m\"\u001b[39m],\n\u001b[32m     25\u001b[39m )\n\u001b[32m     27\u001b[39m df_multi = ts_multi.to_pandas_dataframe()\n\u001b[32m     28\u001b[39m df_multi\n",
      "\u001b[31mNameError\u001b[39m: name 'tdm' is not defined"
     ]
    }
   ],
   "source": [
    "issue_times = [\n",
    "    datetime(2024, 1, 15, 0, tzinfo=timezone.utc),\n",
    "    datetime(2024, 1, 15, 0, tzinfo=timezone.utc),\n",
    "    datetime(2024, 1, 15, 0, tzinfo=timezone.utc),\n",
    "    datetime(2024, 1, 15, 12, tzinfo=timezone.utc),\n",
    "    datetime(2024, 1, 15, 12, tzinfo=timezone.utc),\n",
    "    datetime(2024, 1, 15, 12, tzinfo=timezone.utc),\n",
    "]\n",
    "valid_times = [\n",
    "    datetime(2024, 1, 15, 0, tzinfo=timezone.utc),\n",
    "    datetime(2024, 1, 15, 1, tzinfo=timezone.utc),\n",
    "    datetime(2024, 1, 15, 2, tzinfo=timezone.utc),\n",
    "    datetime(2024, 1, 15, 12, tzinfo=timezone.utc),\n",
    "    datetime(2024, 1, 15, 13, tzinfo=timezone.utc),\n",
    "    datetime(2024, 1, 15, 14, tzinfo=timezone.utc),\n",
    "]\n",
    "\n",
    "ts_multi = tdm.TimeSeries(\n",
    "    tdm.Frequency.PT1H,\n",
    "    timestamps=list(zip(issue_times, valid_times)),\n",
    "    values=[100.0, 105.0, 110.0, 95.0, 100.0, 108.0],\n",
    "    name=\"forecast\",\n",
    "    unit=\"MW\",\n",
    "    index_names=[\"issue_time\", \"valid_time\"],\n",
    ")\n",
    "\n",
    "df_multi = ts_multi.to_pandas_dataframe()\n",
    "df_multi"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:40.437503Z",
     "iopub.status.busy": "2026-03-01T13:36:40.437446Z",
     "iopub.status.idle": "2026-03-01T13:36:40.444679Z",
     "shell.execute_reply": "2026-03-01T13:36:40.444305Z"
    }
   },
   "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 ts_multi_back = \u001b[43mtdm\u001b[49m.TimeSeries.from_pandas(df_multi, tdm.Frequency.PT1H, unit=\u001b[33m\"\u001b[39m\u001b[33mMW\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[33mIndex names: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mts_multi_back.index_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[33mMulti-index: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mts_multi_back.is_multi_index\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'tdm' is not defined"
     ]
    }
   ],
   "source": [
    "ts_multi_back = tdm.TimeSeries.from_pandas(df_multi, tdm.Frequency.PT1H, unit=\"MW\")\n",
    "print(f\"Index names: {ts_multi_back.index_names}\")\n",
    "print(f\"Multi-index: {ts_multi_back.is_multi_index}\")\n",
    "print(f\"Equals original: {ts_multi.equals(ts_multi_back)}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Summary\n",
    "\n",
    "| Format | Export | Import | Metadata preserved |\n",
    "|--------|--------|--------|-------------------|\n",
    "| NumPy | `to_numpy()` | — | Values only |\n",
    "| pandas | `to_pandas_dataframe()` | `from_pandas()` | Name, freq, tz |\n",
    "| polars | `to_polars_dataframe()` | `from_polars()` | Name |\n",
    "| JSON | `to_json()` | `from_json()` | Full |\n",
    "| CSV | `to_csv()` | `from_csv()` | Timestamps + values |\n",
    "\n",
    "Next up: **nb_09** covers geographical support — GeoLocation, GeoArea, and spatial queries."
   ]
  }
 ],
 "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
}
