{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# TimeSeries Operations\n",
    "\n",
    "A `TimeSeries` is not just a data container — it supports arithmetic, comparisons, and transformations. This notebook demonstrates how to manipulate time series data without dropping down to raw arrays."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:44.978783Z",
     "iopub.status.busy": "2026-03-01T13:36:44.978654Z",
     "iopub.status.idle": "2026-03-01T13:36:45.051678Z",
     "shell.execute_reply": "2026-03-01T13:36:45.051346Z"
    }
   },
   "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 3\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----> \u001b[39m\u001b[32m3\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      5\u001b[39m base = datetime(\u001b[32m2024\u001b[39m, \u001b[32m1\u001b[39m, \u001b[32m15\u001b[39m, tzinfo=timezone.utc)\n\u001b[32m      6\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 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",
    "\n",
    "generation = tdm.TimeSeries(\n",
    "    tdm.Frequency.PT1H,\n",
    "    timestamps=timestamps,\n",
    "    values=[\n",
    "        120.0, 115.0, 108.0, 105.0, 102.0, 100.0,\n",
    "        110.0, 135.0, 160.0, 175.0, 180.0, 178.0,\n",
    "        172.0, 170.0, 168.0, 165.0, 175.0, 190.0,\n",
    "        200.0, 195.0, 180.0, 165.0, 145.0, 130.0,\n",
    "    ],\n",
    "    name=\"generation\",\n",
    "    unit=\"MW\",\n",
    "    data_type=tdm.DataType.MEASUREMENT,\n",
    ")\n",
    "\n",
    "consumption = tdm.TimeSeries(\n",
    "    tdm.Frequency.PT1H,\n",
    "    timestamps=timestamps,\n",
    "    values=[\n",
    "        90.0, 85.0, 80.0, 78.0, 77.0, 80.0,\n",
    "        95.0, 120.0, 145.0, 155.0, 160.0, 158.0,\n",
    "        155.0, 150.0, 148.0, 150.0, 160.0, 170.0,\n",
    "        165.0, 155.0, 140.0, 120.0, 105.0, 95.0,\n",
    "    ],\n",
    "    name=\"consumption\",\n",
    "    unit=\"MW\",\n",
    "    data_type=tdm.DataType.MEASUREMENT,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Scalar arithmetic\n",
    "\n",
    "Scale, offset, negate, or round values with natural Python operators. The result is a new `TimeSeries` with metadata preserved."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:45.052872Z",
     "iopub.status.busy": "2026-03-01T13:36:45.052806Z",
     "iopub.status.idle": "2026-03-01T13:36:45.059214Z",
     "shell.execute_reply": "2026-03-01T13:36:45.058904Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'generation' 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 generation_kw = \u001b[43mgeneration\u001b[49m * \u001b[32m1000\u001b[39m\n\u001b[32m      2\u001b[39m generation_kw\n",
      "\u001b[31mNameError\u001b[39m: name 'generation' is not defined"
     ]
    }
   ],
   "source": [
    "generation_kw = generation * 1000\n",
    "generation_kw"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:45.060376Z",
     "iopub.status.busy": "2026-03-01T13:36:45.060310Z",
     "iopub.status.idle": "2026-03-01T13:36:45.067638Z",
     "shell.execute_reply": "2026-03-01T13:36:45.067238Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'generation' 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 offset = \u001b[43mgeneration\u001b[49m + \u001b[32m10.0\u001b[39m\n\u001b[32m      2\u001b[39m negated = -generation\n\u001b[32m      3\u001b[39m rounded = \u001b[38;5;28mround\u001b[39m(generation / \u001b[32m3\u001b[39m, \u001b[32m1\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'generation' is not defined"
     ]
    }
   ],
   "source": [
    "offset = generation + 10.0\n",
    "negated = -generation\n",
    "rounded = round(generation / 3, 1)\n",
    "\n",
    "print(f\"Original first value:  {generation[0].value}\")\n",
    "print(f\"+ 10:                  {offset[0].value}\")\n",
    "print(f\"Negated:               {negated[0].value}\")\n",
    "print(f\"Divided by 3, rounded: {rounded[0].value}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Element-wise arithmetic between two TimeSeries\n",
    "\n",
    "Subtract consumption from generation to get net surplus. Both series must share the same timestamps."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:45.068621Z",
     "iopub.status.busy": "2026-03-01T13:36:45.068573Z",
     "iopub.status.idle": "2026-03-01T13:36:45.074517Z",
     "shell.execute_reply": "2026-03-01T13:36:45.074199Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'generation' 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 surplus = \u001b[43mgeneration\u001b[49m - consumption\n\u001b[32m      2\u001b[39m surplus\n",
      "\u001b[31mNameError\u001b[39m: name 'generation' is not defined"
     ]
    }
   ],
   "source": [
    "surplus = generation - consumption\n",
    "surplus"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Comparison operators\n",
    "\n",
    "Comparisons return a `TimeSeries` of 1.0 / 0.0 (or NaN for missing). Useful for flagging thresholds."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:45.075438Z",
     "iopub.status.busy": "2026-03-01T13:36:45.075385Z",
     "iopub.status.idle": "2026-03-01T13:36:45.081695Z",
     "shell.execute_reply": "2026-03-01T13:36:45.081292Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'generation' 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 high_gen = \u001b[43mgeneration\u001b[49m > \u001b[32m170.0\u001b[39m\n\u001b[32m      2\u001b[39m high_gen\n",
      "\u001b[31mNameError\u001b[39m: name 'generation' is not defined"
     ]
    }
   ],
   "source": [
    "high_gen = generation > 170.0\n",
    "high_gen"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:45.082631Z",
     "iopub.status.busy": "2026-03-01T13:36:45.082579Z",
     "iopub.status.idle": "2026-03-01T13:36:45.088773Z",
     "shell.execute_reply": "2026-03-01T13:36:45.088310Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'generation' 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 gen_exceeds_cons = \u001b[43mgeneration\u001b[49m > consumption\n\u001b[32m      2\u001b[39m gen_exceeds_cons\n",
      "\u001b[31mNameError\u001b[39m: name 'generation' is not defined"
     ]
    }
   ],
   "source": [
    "gen_exceeds_cons = generation > consumption\n",
    "gen_exceeds_cons"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## `abs()` — absolute values\n",
    "\n",
    "Handy when you have signed deviations or residuals."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:45.089702Z",
     "iopub.status.busy": "2026-03-01T13:36:45.089652Z",
     "iopub.status.idle": "2026-03-01T13:36:45.095939Z",
     "shell.execute_reply": "2026-03-01T13:36:45.095551Z"
    }
   },
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'generation' 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 deviation = \u001b[43mgeneration\u001b[49m - consumption\n\u001b[32m      2\u001b[39m abs_deviation = \u001b[38;5;28mabs\u001b[39m(deviation)\n\u001b[32m      3\u001b[39m abs_deviation.head(\u001b[32m6\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'generation' is not defined"
     ]
    }
   ],
   "source": [
    "deviation = generation - consumption\n",
    "abs_deviation = abs(deviation)\n",
    "abs_deviation.head(6)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## `head()`, `tail()`, and `copy()`\n",
    "\n",
    "Quickly preview or duplicate a series."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:45.096856Z",
     "iopub.status.busy": "2026-03-01T13:36:45.096801Z",
     "iopub.status.idle": "2026-03-01T13:36:45.103884Z",
     "shell.execute_reply": "2026-03-01T13:36:45.103526Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "First 3:\n"
     ]
    },
    {
     "ename": "NameError",
     "evalue": "name 'generation' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
      "\u001b[31mNameError\u001b[39m                                 Traceback (most recent call last)",
      "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[8]\u001b[39m\u001b[32m, line 2\u001b[39m\n\u001b[32m      1\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33m\"\u001b[39m\u001b[33mFirst 3:\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m----> \u001b[39m\u001b[32m2\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m dp \u001b[38;5;129;01min\u001b[39;00m \u001b[43mgeneration\u001b[49m.head(\u001b[32m3\u001b[39m):\n\u001b[32m      3\u001b[39m     \u001b[38;5;28mprint\u001b[39m(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33m  \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mdp.timestamp\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m%H:%M\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m  \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mdp.value\u001b[38;5;132;01m:\u001b[39;00m\u001b[33m.1f\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m MW\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m      5\u001b[39m \u001b[38;5;28mprint\u001b[39m(\u001b[33m\"\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[33mLast 3:\u001b[39m\u001b[33m\"\u001b[39m)\n",
      "\u001b[31mNameError\u001b[39m: name 'generation' is not defined"
     ]
    }
   ],
   "source": [
    "print(\"First 3:\")\n",
    "for dp in generation.head(3):\n",
    "    print(f\"  {dp.timestamp:%H:%M}  {dp.value:.1f} MW\")\n",
    "\n",
    "print(\"\\nLast 3:\")\n",
    "for dp in generation.tail(3):\n",
    "    print(f\"  {dp.timestamp:%H:%M}  {dp.value:.1f} MW\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Handling missing values in arithmetic\n",
    "\n",
    "Missing values (`None`) propagate through operations as NaN, just like numpy."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-03-01T13:36:45.104931Z",
     "iopub.status.busy": "2026-03-01T13:36:45.104871Z",
     "iopub.status.idle": "2026-03-01T13:36:45.112233Z",
     "shell.execute_reply": "2026-03-01T13:36:45.111804Z"
    }
   },
   "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 1\u001b[39m\n\u001b[32m----> \u001b[39m\u001b[32m1\u001b[39m ts_with_gaps = \u001b[43mtdm\u001b[49m.TimeSeries(\n\u001b[32m      2\u001b[39m     tdm.Frequency.PT1H,\n\u001b[32m      3\u001b[39m     timestamps=timestamps[:\u001b[32m6\u001b[39m],\n\u001b[32m      4\u001b[39m     values=[\u001b[32m100.0\u001b[39m, \u001b[38;5;28;01mNone\u001b[39;00m, \u001b[32m110.0\u001b[39m, \u001b[38;5;28;01mNone\u001b[39;00m, \u001b[32m105.0\u001b[39m, \u001b[32m108.0\u001b[39m],\n\u001b[32m      5\u001b[39m     name=\u001b[33m\"\u001b[39m\u001b[33msensor\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      6\u001b[39m     unit=\u001b[33m\"\u001b[39m\u001b[33mMW\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m      7\u001b[39m )\n\u001b[32m      9\u001b[39m doubled = ts_with_gaps * \u001b[32m2\u001b[39m\n\u001b[32m     10\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m dp \u001b[38;5;129;01min\u001b[39;00m doubled:\n",
      "\u001b[31mNameError\u001b[39m: name 'tdm' is not defined"
     ]
    }
   ],
   "source": [
    "ts_with_gaps = tdm.TimeSeries(\n",
    "    tdm.Frequency.PT1H,\n",
    "    timestamps=timestamps[:6],\n",
    "    values=[100.0, None, 110.0, None, 105.0, 108.0],\n",
    "    name=\"sensor\",\n",
    "    unit=\"MW\",\n",
    ")\n",
    "\n",
    "doubled = ts_with_gaps * 2\n",
    "for dp in doubled:\n",
    "    print(f\"{dp.timestamp:%H:%M}  {dp.value}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Summary\n",
    "\n",
    "You can treat `TimeSeries` like a numeric object:\n",
    "\n",
    "- Scalar ops: `+`, `-`, `*`, `/`, `-ts`, `abs()`, `round()`\n",
    "- Element-wise ops between aligned series\n",
    "- Comparisons: `>`, `>=`, `<`, `<=`, `==`, `!=`\n",
    "- Missing values propagate cleanly\n",
    "\n",
    "Next up: **nb_05** introduces multivariate time series with `TimeSeriesTable`."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "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
}
