diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 7aefbd54..ae47ad51 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -11,8 +11,10 @@ ## New Features +* Add reusable, modular data processing functions for reporting notebooks. ## Bug Fixes * Replaces multiple duplicated plot functions with a single reusable one. * Handle empty weather/reporting dataframes gracefully to avoid transformation errors. The "Solar Maintenance" notebook is updated accordingly. + diff --git a/examples/Reporting NB.ipynb b/examples/Reporting NB.ipynb new file mode 100644 index 00000000..41712ef3 --- /dev/null +++ b/examples/Reporting NB.ipynb @@ -0,0 +1,2719 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cell_id": "713dd8be46154acca61944f016a50993", + "deepnote_app_is_code_hidden": true, + "deepnote_cell_type": "code", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275930602, + "is_code_hidden": true, + "source_hash": "b4c16b33" + }, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import numpy as np\n", + "from datetime import datetime, timedelta, date\n", + "import os\n", + "from zoneinfo import ZoneInfo\n", + "from typing import Dict, Iterable, Union, Tuple, List, Literal, Mapping, Optional, Any\n", + "\n", + "from frequenz.data.microgrid import component_data\n", + "from frequenz.data.microgrid.config import MicrogridConfig\n", + "import frequenz.lib.notebooks.reporting.plotter as pl\n", + "from frequenz.lib.notebooks.reporting.data_processing import *\n", + "from frequenz.lib.notebooks.reporting.utils.helpers import *\n", + "from frequenz.client.common.metric import Metric \n", + "from frequenz.client.reporting import ReportingApiClient \n", + "from IPython.display import display, Markdown\n", + "\n", + "import logging\n", + "logging.basicConfig()\n", + "logging.getLogger(\"frequenz.lib.notebooks\").setLevel(logging.WARNING)\n", + "\n", + "pv_production_sum=0\n", + "net_site_consumption_sum=0\n", + "grid_consumption_sum=0\n", + "pv_feed_in_sum=0\n", + "peak=0\n", + "pv_self_consumption_sum=0\n", + "pv_self_consumption_share=0\n", + "pv_total_consumption_share=0" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "7713fff5d3b24285a6408684689b7550", + "deepnote_cell_type": "text-cell-h1", + "formattedRanges": [] + }, + "source": [ + "# Reporting Notebook" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "8171293980414c1a86cfa13bfb0bc951", + "deepnote_cell_type": "markdown" + }, + "source": [ + "⚠️ **Wichtig:**\n", + "Um die Anweisungen anzuzeigen (falls nicht bereits angezeigt), müssen Sie **die nächste Zelle ausführen**.\n", + "\n", + "▶️ **Wie führe ich die nächste Zelle aus?**\n", + "--> Drücken Sie die **Run** ▶️-Taste oben.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "cell_id": "f76062430fd44a8bbdf2bc55181e5b2e", + "deepnote_app_is_code_hidden": true, + "deepnote_cell_type": "code", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275930652, + "is_code_hidden": true, + "source_hash": "6c524c54" + }, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "
\n", + " 🇩🇪 Deutsche Anleitung (Klicken zum Anzeigen)\n", + "\n", + "

🛠️ Anleitung

\n", + "

Das Notebook benötigt einige Einstellungen, bevor es zum ersten Mal ausgeführt werden kann.

\n", + "\n", + " \n", + "\n", + "
\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "%%html\n", + "\n", + "\n", + "
\n", + " 🇩🇪 Deutsche Anleitung (Klicken zum Anzeigen)\n", + "\n", + "

🛠️ Anleitung

\n", + "

Das Notebook benötigt einige Einstellungen, bevor es zum ersten Mal ausgeführt werden kann.

\n", + "\n", + " \n", + "\n", + "
\n", + "\n", + "" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "5d84b2ad45b8481e87988d24d56d4be0", + "deepnote_app_block_group_id": null, + "deepnote_app_block_order": 0, + "deepnote_app_block_visible": true, + "deepnote_cell_type": "text-cell-h1", + "formattedRanges": [] + }, + "source": [ + "# Intro" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "270a2dee7ada4f73b5519178c2db906b", + "deepnote_app_block_group_id": null, + "deepnote_app_block_order": 1, + "deepnote_app_block_visible": true, + "deepnote_cell_type": "text-cell-p", + "formattedRanges": [] + }, + "source": [ + "In diesem Notebook können Sie die Daten aus der Reporting API analysieren und visualisieren. Sie haben die Möglichkeit, den gewünschten Zeitraum und die Auflösung der zu analysierenden Daten festzulegen.\n", + "Die Analyse umfasst wichtige Kennzahlen zur Stromgewinnung aus Photovoltaikanlagen (PV), zur Nutzung von Batteriespeichern sowie zum Netzanschluss. Die Ergebnisse werden in anschaulichen Charts dargestellt, um Ihnen einen klaren Überblick über die Energieflüsse zu ermöglichen.\n", + "Wählen Sie einfach Ihre gewünschten Parameter aus, um tiefere Einblicke in Ihre Energieerzeugung und -nutzung zu erhalten." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "7297b3f1eb444881bced1e88a1fe898d", + "deepnote_app_block_group_id": null, + "deepnote_app_block_order": 3, + "deepnote_app_block_visible": true, + "deepnote_cell_type": "text-cell-h1", + "formattedRanges": [] + }, + "source": [ + "# Inputs" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "cell_id": "6f41fc6501a144e4b39d218a4ee1f04a", + "deepnote_app_is_code_hidden": true, + "deepnote_cell_type": "code", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 577, + "execution_start": 1758275930702, + "is_code_hidden": true, + "source_hash": "ee7004ba" + }, + "outputs": [], + "source": [ + "directory = \"/work\"\n", + "toml_files = [f for f in os.listdir(directory) if f.endswith(\".toml\")]\n", + "if not toml_files:\n", + " raise FileNotFoundError(\"No .toml files found in /work.\")\n", + "\n", + "configs: dict[str, \"MicrogridConfig\"] = {}\n", + "for toml_file in toml_files:\n", + " configs.update(MicrogridConfig.load_configs(toml_file))\n", + "available_microgrids = sorted(list(int(x) for x in configs.keys()))" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "1452286565354ed48f8a1f5a598fa4b1", + "deepnote_app_block_group_id": null, + "deepnote_app_block_order": 4, + "deepnote_app_block_visible": true, + "deepnote_cell_type": "text-cell-p", + "formattedRanges": [ + { + "fromCodePoint": 97, + "ranges": [], + "toCodePoint": 106, + "type": "link", + "url": "google.com" + }, + { + "fromCodePoint": 152, + "ranges": [], + "toCodePoint": 165, + "type": "link", + "url": "https://kuiper.frequenz.com/" + }, + { + "fromCodePoint": 481, + "ranges": [], + "toCodePoint": 485, + "type": "link", + "url": "google.com" + } + ] + }, + "source": [ + "Um das Notebook nutzen zu können müssen zunächst die API Anmeldeinformationen hinterlegt werden (Anleitung). Die API Anmeldeinformationen finden Sie im Kuiper-Portal. Dort finden Sie ebenfalls ihre Microgrid ID, die Meter PQ ID, die Meter Batterie IDs sowie die Meter PV IDs. Bitte geben Sie Ihre Komponenten IDs unten ein, wählen sie ein Start- und Enddatum, wählen sie eine Auflösung und klicken Sie auf Start, um das Notebook auszuführen.\n", + "Eine Anleitung zum Vorgehen finden Sie hier." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "allow_embed": false, + "cell_id": "b0c192cc84ba4bd2a4d69e0a7f834b33", + "deepnote_allow_multiple_values": false, + "deepnote_app_block_group_id": null, + "deepnote_app_block_order": 5, + "deepnote_app_block_visible": true, + "deepnote_cell_type": "input-select", + "deepnote_input_label": "Microgrid Name", + "deepnote_variable_custom_options": [ + "grid", + "consumption", + "battery", + "chp", + "ev", + "pv" + ], + "deepnote_variable_default_value": "", + "deepnote_variable_name": "microgrid_id", + "deepnote_variable_options": [ + "1", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "10", + "11", + "13", + "36", + "66", + "67", + "115", + "116", + "117", + "118", + "119", + "120", + "121", + "126", + "131", + "132", + "133", + "137", + "138", + "139", + "140", + "141", + "146", + "152", + "153", + "154", + "155", + "156", + "157", + "158", + "159", + "160", + "161", + "162", + "163", + "164", + "165", + "166", + "167", + "168", + "169", + "170", + "171", + "172", + "173", + "174", + "175", + "176", + "177", + "178", + "179", + "180", + "181", + "182", + "183", + "184", + "185", + "186", + "187", + "188", + "189", + "190", + "191", + "193", + "194", + "195", + "196", + "197", + "198", + "199", + "200", + "201", + "202", + "203", + "204", + "220", + "221", + "222", + "223", + "224", + "225", + "226", + "227", + "228", + "229", + "233", + "239" + ], + "deepnote_variable_select_type": "from-variable", + "deepnote_variable_selected_variable": "available_microgrids", + "deepnote_variable_value": "13", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275931332, + "source_hash": "b9c1d5fa" + }, + "outputs": [], + "source": [ + "microgrid_id = '13'" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "f1f0a050ac414d34809621e7b0ed86bb", + "deepnote_app_block_visible": false, + "deepnote_cell_type": "text-cell-p", + "formattedRanges": [] + }, + "source": [ + "Zeitrahmen" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "cell_id": "976dd017eeb34145861b965741af3b6a", + "deepnote_app_block_group_id": "d4f39a494830424e97016c07754914c6", + "deepnote_app_block_order": 9, + "deepnote_app_block_visible": true, + "deepnote_cell_type": "input-date", + "deepnote_input_date_version": 2, + "deepnote_input_label": "Start Datum (yyyy-mm-dd)", + "deepnote_variable_name": "start_date", + "deepnote_variable_value": "2025-08-01T00:00:00.000Z", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275931382, + "source_hash": "6ef890cf" + }, + "outputs": [], + "source": [ + "\n", + "from dateutil.parser import parse as _deepnote_parse\n", + "start_date = _deepnote_parse('2025-08-01T00:00:00.000Z').date()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "cell_id": "391f9ece0acd4b9d828752d6149327ff", + "deepnote_app_block_group_id": "d4f39a494830424e97016c07754914c6", + "deepnote_app_block_order": 10, + "deepnote_app_block_visible": true, + "deepnote_cell_type": "input-date", + "deepnote_input_date_version": 2, + "deepnote_input_label": "Enddatum (yyyy-mm-dd)", + "deepnote_variable_name": "end_date", + "deepnote_variable_value": "2025-08-31T00:00:00.000Z", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 1, + "execution_start": 1758275931431, + "source_hash": "c872cec1" + }, + "outputs": [], + "source": [ + "\n", + "from dateutil.parser import parse as _deepnote_parse\n", + "end_date = _deepnote_parse('2025-08-31T00:00:00.000Z').date()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "cell_id": "3223472cff8d43db9a0d766a4b6a409c", + "deepnote_app_block_group_id": "d4f39a494830424e97016c07754914c6", + "deepnote_app_block_order": 11, + "deepnote_app_block_visible": true, + "deepnote_cell_type": "input-text", + "deepnote_input_label": "Auflösung in Minuten (Standard: 15min)", + "deepnote_variable_name": "resolution", + "deepnote_variable_value": "15", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275931482, + "source_hash": "3440d099" + }, + "outputs": [], + "source": [ + "resolution = '15'" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "abe29461342a43e2ab37ca2280edbb02", + "deepnote_app_block_group_id": null, + "deepnote_app_block_order": 12, + "deepnote_app_block_visible": true, + "deepnote_button_behavior": "set_variable", + "deepnote_button_color_scheme": "blue", + "deepnote_button_title": "Start", + "deepnote_cell_type": "button", + "deepnote_variable_name": "run_notebook", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275931532, + "source_hash": "ec0a32f1" + }, + "source": [ + "run_notebook = False" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "cell_id": "5785e11d28af42839b3ab165a0513a0c", + "deepnote_app_block_group_id": null, + "deepnote_app_block_order": 13, + "deepnote_app_block_visible": true, + "deepnote_app_is_code_hidden": true, + "deepnote_app_is_output_hidden": false, + "deepnote_cell_type": "code", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275931592, + "is_code_hidden": true, + "source_hash": "e875544e" + }, + "outputs": [], + "source": [ + "# Casting input values\n", + "def cast_input(input_string, output_type=int):\n", + " if input_string == 'microgrid_id':\n", + " try :\n", + " globals()[f'{input_string}'] = int(globals()[f'{input_string}'])\n", + " # Check if valid microgrid id\n", + " except:\n", + " globals()[f'{input_string}'] = 0\n", + " Exception(input_string + ' muss ganzzahlig sein')\n", + " if input_string == 'resolution':\n", + " try:\n", + " current_val = globals()[input_string]\n", + "\n", + " # Only convert if it's not already a timedelta\n", + " if not isinstance(current_val, timedelta):\n", + " globals()[input_string] = timedelta(seconds=int(current_val) * 60)\n", + "\n", + " except Exception:\n", + " globals()[input_string] = timedelta(seconds=0)\n", + " raise Exception(input_string + ' muss ganzzahlig sein')\n", + " elif output_type == int:\n", + " try:\n", + " globals()[f'{input_string}'] = int(globals()[f'{input_string}'])\n", + " except:\n", + " globals()[f'{input_string}'] = 0\n", + " raise Exception(input_string + ' muss ganzzahlig sein')\n", + "\n", + " elif output_type == list: # Added list\n", + " try:\n", + " strings = globals()[f'{input_string}'].split(',')\n", + " globals()[f'{input_string}'] = [int(x) for x in strings]\n", + " except Exception as e:\n", + " raise Exception(f'Prüfen Sie die {input_string}: {str(e)}')\n", + "\n", + " elif output_type == int:\n", + " try:\n", + " globals()[f'{input_string}'] = int(globals()[f'{input_string}'])\n", + " except:\n", + " globals()[f'{input_string}'] = 0\n", + " raise Exception(input_string + ' muss ganzzahlig sein')\n", + "\n", + " elif output_type == date:\n", + " try:\n", + " dt = globals()[f'{input_string}']\n", + " if input_string == 'start_date':\n", + " dt = datetime(dt.year, dt.month, dt.day, 0, 0, 0, tzinfo=ZoneInfo(\"CET\"))\n", + " else:\n", + " dt = datetime(dt.year, dt.month, dt.day, 0, 0, 0, tzinfo=ZoneInfo(\"CET\"))\n", + " globals()[f'{input_string}'] = dt.astimezone(ZoneInfo('UTC'))\n", + " except:\n", + " raise Exception(input_string + ' muss ein Datumsformat sein')\n", + "\n", + "if run_notebook:\n", + " mapper = ColumnMapper.from_yaml(\"schema_mapping.yaml\")\n", + " variable_types = {\n", + " 'start_date': date,\n", + " 'end_date': date,\n", + " 'microgrid_id': int,\n", + " 'resolution': timedelta\n", + " }\n", + "\n", + " for input in variable_types:\n", + " if globals()[input]:\n", + " cast_input(input, variable_types[input])\n", + "\n", + " if start_date >= end_date:\n", + " raise Exception('Prüfen Sie die eingegebenen Daten. Das Startdatum muss kleiner oder gleich dem Enddatum sein.')\n", + " else:\n", + " start_dt = start_date\n", + " end_dt = end_date" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "5a2f1e7b832446d89c4f0ca928a2ddf7", + "deepnote_app_block_visible": false, + "deepnote_cell_type": "text-cell-h2", + "formattedRanges": [] + }, + "source": [ + "## Datenimport" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "cell_id": "56302f513aee43c9b1e77a105692a762", + "deepnote_app_is_code_hidden": true, + "deepnote_cell_type": "code", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275931652, + "is_code_hidden": true, + "source_hash": "4cc54536" + }, + "outputs": [ + { + "data": { + "application/vnd.deepnote.dataframe.v3+json": { + "column_count": 0, + "columns": [], + "preview_row_count": 0, + "row_count": 0, + "rows": [], + "type": "dataframe" + }, + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
" + ], + "text/plain": [ + "Empty DataFrame\n", + "Columns: []\n", + "Index: []" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "if run_notebook:\n", + " toml_path = os.path.join(directory, f'microgrids_eid{configs[str(microgrid_id)]._metadata.gid}.toml')\n", + "\n", + " mdata = component_data.MicrogridData(\n", + " server_url=os.environ[\"REPORTING_SERVER_URL\"],\n", + " auth_key=os.environ[\"REPORTING_API_KEY\"],\n", + " sign_secret=os.environ[\"REPORTING_API_SECRET\"],\n", + " microgrid_config_path=toml_path,\n", + " ) \n", + "\n", + " mids = [i.replace(\"iot\", \"\") for i in mdata.microgrid_ids] \n", + "\n", + " mcfg = mdata.microgrid_configs[str(microgrid_id)]\n", + " ctypes = mcfg.component_types()\n", + " component_types = ctypes\n", + "\n", + " df = await mdata.ac_active_power(\n", + " microgrid_id=microgrid_id, \n", + " component_types=component_types,\n", + " start=start_date,\n", + " end=end_date,\n", + " resampling_period=resolution,\n", + " keep_components=True,\n", + " splits=True,\n", + " )\n", + "\n", + " print(f\"Received {df.shape[0]} rows and {df.shape[1]} columns\")\n", + " # df.to_csv('raw_df.csv')\n", + "\n", + "else:\n", + " df = pd.DataFrame()\n", + "\n", + "df" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "9d90c66811b8427aa5f2dee24a00e729", + "deepnote_app_block_group_id": null, + "deepnote_app_block_order": 16, + "deepnote_app_block_visible": true, + "deepnote_cell_type": "text-cell-h1", + "formattedRanges": [] + }, + "source": [ + "# Output" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "cell_id": "993a8e57489b4957856228d6796b98e7", + "deepnote_app_is_code_hidden": true, + "deepnote_cell_type": "code", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275931712, + "is_code_hidden": true, + "source_hash": "d8859aec" + }, + "outputs": [], + "source": [ + "if run_notebook:\n", + " df = _add_pv_energy_flows(df)\n", + " df = df.reset_index()\n", + " df = mapper.to_canonical(df)\n", + "\n", + " energy_report_df = create_energy_report_dfs(df, component_types, mcfg)\n", + "\n", + " # base_cols = ['Zeitpunkt', 'Netzbezug', 'Netto Gesamtverbrauch']\n", + " base_cols = ['timestamp', 'net_import', 'net_consumption']\n", + " optional_cols = {\n", + " # 'pv': ['PV Produktion', 'PV Einspeisung'],\n", + " # 'battery': ['Batterie Durchsatz'],\n", + " 'pv': ['pv_prod', 'pv_feedin'],\n", + " 'battery': ['battery_throughput'],\n", + "\n", + " }\n", + "\n", + " # Start with base columns\n", + " cols = base_cols[:]\n", + "\n", + " # Add optional columns if the component is present\n", + " for comp, comp_cols in optional_cols.items():\n", + " if comp in component_types:\n", + " cols.extend(comp_cols)\n", + "\n", + " overview_df = energy_report_df[cols]" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "cell_id": "6af522afeeee4cb1aa3cb90201dec110", + "deepnote_app_is_code_hidden": true, + "deepnote_cell_type": "code", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275931782, + "is_code_hidden": true, + "source_hash": "ec500581" + }, + "outputs": [ + { + "data": { + "application/vnd.deepnote.dataframe.v3+json": { + "column_count": 0, + "columns": [], + "preview_row_count": 0, + "row_count": 0, + "rows": [], + "type": "dataframe" + }, + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
" + ], + "text/plain": [ + "Empty DataFrame\n", + "Columns: []\n", + "Index: []" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "if run_notebook:\n", + " grid_consumption_sum = (energy_report_df.get(\"net_import\", 0) * (resolution.seconds / 3600)).sum()\n", + " energy_summary_df = compute_energy_summary(energy_report_df, resolution=resolution)\n", + " energy_metrics_dict = aggregate_pv_metrics(energy_report_df=energy_report_df, resolution=resolution, grid_consumption_sum=grid_consumption_sum)\n", + "\n", + " (pv_feed_in_sum,\n", + " pv_production_sum,\n", + " pv_self_consumption_sum,\n", + " pv_bat_sum,\n", + " pv_self_consumption_share,\n", + " pv_total_consumption_share,\n", + " net_site_consumption_sum,\n", + " peak,\n", + " peak_date) = aggregate_pv_metrics(energy_report_df, resolution, grid_consumption_sum).values()\n", + "\n", + "else:\n", + " energy_summary_df = pd.DataFrame()\n", + "\n", + "energy_summary_df" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "a0595ecdadae4c11accf52717904faa7", + "deepnote_app_block_group_id": null, + "deepnote_app_block_order": 20, + "deepnote_app_block_visible": true, + "deepnote_cell_type": "text-cell-h2", + "formattedRanges": [], + "is_collapsed": false + }, + "source": [ + "## Übersicht" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "cell_id": "257b9b778483402ea47d8808668c0fa6", + "deepnote_app_block_group_id": null, + "deepnote_app_block_order": 21, + "deepnote_app_block_visible": true, + "deepnote_app_is_code_hidden": true, + "deepnote_app_is_output_hidden": false, + "deepnote_cell_type": "code", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275931842, + "is_code_hidden": true, + "source_hash": "f4691930" + }, + "outputs": [], + "source": [ + "if run_notebook:\n", + " print(f'{datetime.strftime(start_date.astimezone(ZoneInfo(\"CET\")).date(), \"%d.%m.%Y\")} - {datetime.strftime(end_date.astimezone(ZoneInfo(\"CET\")).date(), \"%d.%m.%Y\")}')" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "cell_id": "43d48c5ef7bf47ef8f0b3476d233a89d", + "deepnote_app_block_group_id": "63eefa7b8b0249eeb99f3cb3fc6d385e", + "deepnote_app_block_order": 23, + "deepnote_app_block_visible": true, + "deepnote_big_number_comparison_enabled": false, + "deepnote_big_number_comparison_format": "", + "deepnote_big_number_comparison_title": "", + "deepnote_big_number_comparison_type": "", + "deepnote_big_number_comparison_value": "", + "deepnote_big_number_format": "number", + "deepnote_big_number_title": "Brutto Stromverbrauch kWh", + "deepnote_big_number_value": "net_site_consumption_sum", + "deepnote_cell_type": "big-number", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275931912, + "source_hash": "763a5c17" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'{\"comparisonTitle\": \"\", \"comparisonValue\": \"\", \"title\": \"Brutto Stromverbrauch kWh\", \"value\": \"0\"}'" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\n", + "def __deepnote_big_number__():\n", + " import json\n", + " import jinja2\n", + " from jinja2 import meta\n", + "\n", + " def render_template(template):\n", + " parsed_content = jinja2.Environment().parse(template)\n", + "\n", + " required_variables = meta.find_undeclared_variables(parsed_content)\n", + "\n", + " context = {\n", + " variable_name: globals().get(variable_name)\n", + " for variable_name in required_variables\n", + " }\n", + "\n", + " result = jinja2.Environment().from_string(template).render(context)\n", + "\n", + " return result\n", + "\n", + " rendered_title = render_template(\"Brutto Stromverbrauch kWh\")\n", + " rendered_comparison_title = render_template(\"\")\n", + "\n", + " return json.dumps({\n", + " \"comparisonTitle\": rendered_comparison_title,\n", + " \"comparisonValue\": \"\",\n", + " \"title\": rendered_title,\n", + " \"value\": f\"{net_site_consumption_sum}\"\n", + " })\n", + "\n", + "__deepnote_big_number__()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "cell_id": "11b6e22ab1ae4da4b4092d7efb8740b7", + "deepnote_app_block_group_id": "63eefa7b8b0249eeb99f3cb3fc6d385e", + "deepnote_app_block_order": 24, + "deepnote_app_block_visible": true, + "deepnote_big_number_comparison_enabled": false, + "deepnote_big_number_comparison_format": "", + "deepnote_big_number_comparison_title": "", + "deepnote_big_number_comparison_type": "", + "deepnote_big_number_comparison_value": "", + "deepnote_big_number_format": "number", + "deepnote_big_number_title": "Bezug in kWh", + "deepnote_big_number_value": "grid_consumption_sum", + "deepnote_cell_type": "big-number", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275931972, + "source_hash": "1c6e87d9" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'{\"comparisonTitle\": \"\", \"comparisonValue\": \"\", \"title\": \"Bezug in kWh\", \"value\": \"0\"}'" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\n", + "def __deepnote_big_number__():\n", + " import json\n", + " import jinja2\n", + " from jinja2 import meta\n", + "\n", + " def render_template(template):\n", + " parsed_content = jinja2.Environment().parse(template)\n", + "\n", + " required_variables = meta.find_undeclared_variables(parsed_content)\n", + "\n", + " context = {\n", + " variable_name: globals().get(variable_name)\n", + " for variable_name in required_variables\n", + " }\n", + "\n", + " result = jinja2.Environment().from_string(template).render(context)\n", + "\n", + " return result\n", + "\n", + " rendered_title = render_template(\"Bezug in kWh\")\n", + " rendered_comparison_title = render_template(\"\")\n", + "\n", + " return json.dumps({\n", + " \"comparisonTitle\": rendered_comparison_title,\n", + " \"comparisonValue\": \"\",\n", + " \"title\": rendered_title,\n", + " \"value\": f\"{grid_consumption_sum}\"\n", + " })\n", + "\n", + "__deepnote_big_number__()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "allow_embed": false, + "cell_id": "52f0a28347bf4b188ea395fea0258f9d", + "deepnote_app_block_visible": true, + "deepnote_big_number_comparison_enabled": false, + "deepnote_big_number_comparison_format": "", + "deepnote_big_number_comparison_title": "", + "deepnote_big_number_comparison_type": "", + "deepnote_big_number_comparison_value": "", + "deepnote_big_number_format": "number", + "deepnote_big_number_title": "PV Eigenverbrauch in kWh", + "deepnote_big_number_value": "pv_self_consumption_sum", + "deepnote_cell_type": "big-number", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275932032, + "source_hash": "24149a25" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'{\"comparisonTitle\": \"\", \"comparisonValue\": \"\", \"title\": \"PV Eigenverbrauch in kWh\", \"value\": \"0\"}'" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\n", + "def __deepnote_big_number__():\n", + " import json\n", + " import jinja2\n", + " from jinja2 import meta\n", + "\n", + " def render_template(template):\n", + " parsed_content = jinja2.Environment().parse(template)\n", + "\n", + " required_variables = meta.find_undeclared_variables(parsed_content)\n", + "\n", + " context = {\n", + " variable_name: globals().get(variable_name)\n", + " for variable_name in required_variables\n", + " }\n", + "\n", + " result = jinja2.Environment().from_string(template).render(context)\n", + "\n", + " return result\n", + "\n", + " rendered_title = render_template(\"PV Eigenverbrauch in kWh\")\n", + " rendered_comparison_title = render_template(\"\")\n", + "\n", + " return json.dumps({\n", + " \"comparisonTitle\": rendered_comparison_title,\n", + " \"comparisonValue\": \"\",\n", + " \"title\": rendered_title,\n", + " \"value\": f\"{pv_self_consumption_sum}\"\n", + " })\n", + "\n", + "__deepnote_big_number__()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": { + "cell_id": "2804afd629924243a4ec97789142e33a", + "deepnote_app_block_group_id": "63eefa7b8b0249eeb99f3cb3fc6d385e", + "deepnote_app_block_order": 22, + "deepnote_app_block_visible": true, + "deepnote_big_number_comparison_enabled": false, + "deepnote_big_number_comparison_format": "", + "deepnote_big_number_comparison_title": "", + "deepnote_big_number_comparison_type": "", + "deepnote_big_number_comparison_value": "", + "deepnote_big_number_format": "number", + "deepnote_big_number_title": "PV Gesamterzeugung in kWh", + "deepnote_big_number_value": "pv_production_sum", + "deepnote_cell_type": "big-number", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275932082, + "source_hash": "74535792" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'{\"comparisonTitle\": \"\", \"comparisonValue\": \"\", \"title\": \"PV Gesamterzeugung in kWh\", \"value\": \"0\"}'" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\n", + "def __deepnote_big_number__():\n", + " import json\n", + " import jinja2\n", + " from jinja2 import meta\n", + "\n", + " def render_template(template):\n", + " parsed_content = jinja2.Environment().parse(template)\n", + "\n", + " required_variables = meta.find_undeclared_variables(parsed_content)\n", + "\n", + " context = {\n", + " variable_name: globals().get(variable_name)\n", + " for variable_name in required_variables\n", + " }\n", + "\n", + " result = jinja2.Environment().from_string(template).render(context)\n", + "\n", + " return result\n", + "\n", + " rendered_title = render_template(\"PV Gesamterzeugung in kWh\")\n", + " rendered_comparison_title = render_template(\"\")\n", + "\n", + " return json.dumps({\n", + " \"comparisonTitle\": rendered_comparison_title,\n", + " \"comparisonValue\": \"\",\n", + " \"title\": rendered_title,\n", + " \"value\": f\"{pv_production_sum}\"\n", + " })\n", + "\n", + "__deepnote_big_number__()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": { + "cell_id": "c2dff4f16e3a4fd8bf74dc231380933a", + "deepnote_app_block_group_id": "63eefa7b8b0249eeb99f3cb3fc6d385e", + "deepnote_app_block_order": 25, + "deepnote_app_block_visible": true, + "deepnote_big_number_comparison_enabled": false, + "deepnote_big_number_comparison_format": "", + "deepnote_big_number_comparison_title": "", + "deepnote_big_number_comparison_type": "", + "deepnote_big_number_comparison_value": "", + "deepnote_big_number_format": "number", + "deepnote_big_number_title": "Einspeisung in kWh", + "deepnote_big_number_value": "pv_feed_in_sum", + "deepnote_cell_type": "big-number", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275932142, + "source_hash": "f8110522" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'{\"comparisonTitle\": \"\", \"comparisonValue\": \"\", \"title\": \"Einspeisung in kWh\", \"value\": \"0\"}'" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\n", + "def __deepnote_big_number__():\n", + " import json\n", + " import jinja2\n", + " from jinja2 import meta\n", + "\n", + " def render_template(template):\n", + " parsed_content = jinja2.Environment().parse(template)\n", + "\n", + " required_variables = meta.find_undeclared_variables(parsed_content)\n", + "\n", + " context = {\n", + " variable_name: globals().get(variable_name)\n", + " for variable_name in required_variables\n", + " }\n", + "\n", + " result = jinja2.Environment().from_string(template).render(context)\n", + "\n", + " return result\n", + "\n", + " rendered_title = render_template(\"Einspeisung in kWh\")\n", + " rendered_comparison_title = render_template(\"\")\n", + "\n", + " return json.dumps({\n", + " \"comparisonTitle\": rendered_comparison_title,\n", + " \"comparisonValue\": \"\",\n", + " \"title\": rendered_title,\n", + " \"value\": f\"{pv_feed_in_sum}\"\n", + " })\n", + "\n", + "__deepnote_big_number__()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": { + "cell_id": "6a34c92f919146f39cd78db950a45396", + "deepnote_app_block_group_id": "17547582a08344dd874013ef7e8d34eb", + "deepnote_app_block_order": 26, + "deepnote_app_block_visible": true, + "deepnote_big_number_comparison_enabled": false, + "deepnote_big_number_comparison_format": "", + "deepnote_big_number_comparison_title": "", + "deepnote_big_number_comparison_type": "", + "deepnote_big_number_comparison_value": "", + "deepnote_big_number_format": "number", + "deepnote_big_number_title": "Lastspitze in kW {{peak_date}}", + "deepnote_big_number_value": "peak", + "deepnote_cell_type": "big-number", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275932212, + "source_hash": "ad1dac55" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'{\"comparisonTitle\": \"\", \"comparisonValue\": \"\", \"title\": \"Lastspitze in kW None\", \"value\": \"0\"}'" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\n", + "def __deepnote_big_number__():\n", + " import json\n", + " import jinja2\n", + " from jinja2 import meta\n", + "\n", + " def render_template(template):\n", + " parsed_content = jinja2.Environment().parse(template)\n", + "\n", + " required_variables = meta.find_undeclared_variables(parsed_content)\n", + "\n", + " context = {\n", + " variable_name: globals().get(variable_name)\n", + " for variable_name in required_variables\n", + " }\n", + "\n", + " result = jinja2.Environment().from_string(template).render(context)\n", + "\n", + " return result\n", + "\n", + " rendered_title = render_template(\"Lastspitze in kW {{peak_date}}\")\n", + " rendered_comparison_title = render_template(\"\")\n", + "\n", + " return json.dumps({\n", + " \"comparisonTitle\": rendered_comparison_title,\n", + " \"comparisonValue\": \"\",\n", + " \"title\": rendered_title,\n", + " \"value\": f\"{peak}\"\n", + " })\n", + "\n", + "__deepnote_big_number__()\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "38f67458f12e4fe2bb73ee3aacf945a2", + "deepnote_app_block_group_id": "17547582a08344dd874013ef7e8d34eb", + "deepnote_app_block_order": 28, + "deepnote_app_block_visible": true, + "deepnote_cell_type": "text-cell-p", + "formattedRanges": [] + }, + "source": [] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "ca2ebe3b8b2248b0ad8212beb457f499", + "deepnote_app_block_group_id": "17547582a08344dd874013ef7e8d34eb", + "deepnote_app_block_order": 27, + "deepnote_app_block_visible": true, + "deepnote_cell_type": "text-cell-p", + "formattedRanges": [] + }, + "source": [] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "902aac301a7a45e9a7cad7b85d64eabc", + "deepnote_app_block_group_id": null, + "deepnote_app_block_order": 31, + "deepnote_app_block_visible": true, + "deepnote_cell_type": "text-cell-p", + "formattedRanges": [ + { + "fromCodePoint": 0, + "marks": { + "bold": true + }, + "toCodePoint": 32, + "type": "marks" + } + ] + }, + "source": [ + "Lastgang im zeitlichen Verlauf: \n", + "Hier werden die PV-Gesamtleistung, der Verbrauch (Last), der Netzbezug, die (PV) Einspeisung und die Batterie-Gesamtleistung im ausgewählten Zeitraum dargestellt." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": { + "cell_id": "cfea1dcfd8014df3a5b4d22aef90f632", + "deepnote_app_is_code_hidden": true, + "deepnote_cell_type": "code", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 1, + "execution_start": 1758275932261, + "is_code_hidden": true, + "source_hash": "42f2c682" + }, + "outputs": [], + "source": [ + "if run_notebook:\n", + " overview_df = mapper.to_display(overview_df)\n", + " fig = plot_time_series(overview_df, time_col=\"Zeitpunkt\", cols=None, title=\"Lastgang Übersicht\")\n", + " fig.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "303ba6d74b12493595de283e5ecc0e57", + "deepnote_app_block_group_id": null, + "deepnote_app_block_order": 36, + "deepnote_app_block_visible": true, + "deepnote_cell_type": "text-cell-p", + "formattedRanges": [ + { + "fromCodePoint": 0, + "marks": { + "bold": true + }, + "toCodePoint": 12, + "type": "marks" + } + ] + }, + "source": [ + "Energiebezug\n", + "Hier sehen Sie das Verhältnis von Eigenverbrauch und Netzbezug in dem von Ihnen gewählten Zeitraum an." + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "cell_id": "ac107001d49d43c59f912f3058c4eea5", + "deepnote_app_block_group_id": null, + "deepnote_app_block_order": 38, + "deepnote_app_block_visible": true, + "deepnote_app_is_code_hidden": true, + "deepnote_app_is_output_hidden": false, + "deepnote_cell_type": "code", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 1, + "execution_start": 1758275932311, + "is_code_hidden": true, + "source_hash": "c131d14e" + }, + "outputs": [], + "source": [ + "if run_notebook:\n", + " energy_summary_df = energy_summary_df.rename(columns={\"Energy Source\": \"Energiebezug\", \"Energy [kWh]\": \"Energie [kWh]\"})\n", + " pl.plot_energy_pie_chart(energy_summary_df)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "32e429c1767c4afaa148565792d08b1f", + "deepnote_app_block_group_id": null, + "deepnote_app_block_order": 39, + "deepnote_app_block_visible": true, + "deepnote_cell_type": "text-cell-h2", + "formattedRanges": [], + "is_collapsed": false + }, + "source": [ + "## PV" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "cell_id": "faa99db587294e108ebff11e49a868ca", + "deepnote_app_block_group_id": null, + "deepnote_app_block_order": 40, + "deepnote_app_block_visible": true, + "deepnote_app_is_code_hidden": true, + "deepnote_app_is_output_hidden": false, + "deepnote_cell_type": "code", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 1, + "execution_start": 1758275932371, + "is_code_hidden": true, + "source_hash": "f4691930" + }, + "outputs": [], + "source": [ + "if run_notebook:\n", + " print(f'{datetime.strftime(start_date.astimezone(ZoneInfo(\"CET\")).date(), \"%d.%m.%Y\")} - {datetime.strftime(end_date.astimezone(ZoneInfo(\"CET\")).date(), \"%d.%m.%Y\")}')" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": { + "cell_id": "6c76fc3d902044368bb8ac62106f5cd1", + "deepnote_app_block_group_id": "77790c88af014d8e95f12134a403cb13", + "deepnote_app_block_order": 41, + "deepnote_app_block_visible": true, + "deepnote_big_number_comparison_enabled": false, + "deepnote_big_number_comparison_format": "", + "deepnote_big_number_comparison_title": "", + "deepnote_big_number_comparison_type": "", + "deepnote_big_number_comparison_value": "", + "deepnote_big_number_format": "number", + "deepnote_big_number_title": "PV Gesamterzeugung in kWh", + "deepnote_big_number_value": "pv_production_sum", + "deepnote_cell_type": "big-number", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275932422, + "source_hash": "74535792" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'{\"comparisonTitle\": \"\", \"comparisonValue\": \"\", \"title\": \"PV Gesamterzeugung in kWh\", \"value\": \"0\"}'" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\n", + "def __deepnote_big_number__():\n", + " import json\n", + " import jinja2\n", + " from jinja2 import meta\n", + "\n", + " def render_template(template):\n", + " parsed_content = jinja2.Environment().parse(template)\n", + "\n", + " required_variables = meta.find_undeclared_variables(parsed_content)\n", + "\n", + " context = {\n", + " variable_name: globals().get(variable_name)\n", + " for variable_name in required_variables\n", + " }\n", + "\n", + " result = jinja2.Environment().from_string(template).render(context)\n", + "\n", + " return result\n", + "\n", + " rendered_title = render_template(\"PV Gesamterzeugung in kWh\")\n", + " rendered_comparison_title = render_template(\"\")\n", + "\n", + " return json.dumps({\n", + " \"comparisonTitle\": rendered_comparison_title,\n", + " \"comparisonValue\": \"\",\n", + " \"title\": rendered_title,\n", + " \"value\": f\"{pv_production_sum}\"\n", + " })\n", + "\n", + "__deepnote_big_number__()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "cell_id": "ff2600513d724190a2cdd0a7e82656ef", + "deepnote_app_block_group_id": "77790c88af014d8e95f12134a403cb13", + "deepnote_app_block_order": 42, + "deepnote_app_block_visible": true, + "deepnote_big_number_comparison_enabled": false, + "deepnote_big_number_comparison_format": "", + "deepnote_big_number_comparison_title": "", + "deepnote_big_number_comparison_type": "", + "deepnote_big_number_comparison_value": "", + "deepnote_big_number_format": "number", + "deepnote_big_number_title": "PV Eigenverbrauch in kWh", + "deepnote_big_number_value": "pv_self_consumption_sum", + "deepnote_cell_type": "big-number", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 1, + "execution_start": 1758275932481, + "source_hash": "24149a25" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'{\"comparisonTitle\": \"\", \"comparisonValue\": \"\", \"title\": \"PV Eigenverbrauch in kWh\", \"value\": \"0\"}'" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\n", + "def __deepnote_big_number__():\n", + " import json\n", + " import jinja2\n", + " from jinja2 import meta\n", + "\n", + " def render_template(template):\n", + " parsed_content = jinja2.Environment().parse(template)\n", + "\n", + " required_variables = meta.find_undeclared_variables(parsed_content)\n", + "\n", + " context = {\n", + " variable_name: globals().get(variable_name)\n", + " for variable_name in required_variables\n", + " }\n", + "\n", + " result = jinja2.Environment().from_string(template).render(context)\n", + "\n", + " return result\n", + "\n", + " rendered_title = render_template(\"PV Eigenverbrauch in kWh\")\n", + " rendered_comparison_title = render_template(\"\")\n", + "\n", + " return json.dumps({\n", + " \"comparisonTitle\": rendered_comparison_title,\n", + " \"comparisonValue\": \"\",\n", + " \"title\": rendered_title,\n", + " \"value\": f\"{pv_self_consumption_sum}\"\n", + " })\n", + "\n", + "__deepnote_big_number__()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": { + "cell_id": "d3b4783e50734053857ebb807fefd3a2", + "deepnote_app_block_group_id": "77790c88af014d8e95f12134a403cb13", + "deepnote_app_block_order": 43, + "deepnote_app_block_visible": true, + "deepnote_big_number_comparison_enabled": false, + "deepnote_big_number_comparison_format": "", + "deepnote_big_number_comparison_title": "", + "deepnote_big_number_comparison_type": "", + "deepnote_big_number_comparison_value": "", + "deepnote_big_number_format": "percent", + "deepnote_big_number_title": "PV Eigenverbrauch in %", + "deepnote_big_number_value": "pv_self_consumption_share", + "deepnote_cell_type": "big-number", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275932532, + "source_hash": "35efbce9" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'{\"comparisonTitle\": \"\", \"comparisonValue\": \"\", \"title\": \"PV Eigenverbrauch in %\", \"value\": \"0\"}'" + ] + }, + "execution_count": 27, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\n", + "def __deepnote_big_number__():\n", + " import json\n", + " import jinja2\n", + " from jinja2 import meta\n", + "\n", + " def render_template(template):\n", + " parsed_content = jinja2.Environment().parse(template)\n", + "\n", + " required_variables = meta.find_undeclared_variables(parsed_content)\n", + "\n", + " context = {\n", + " variable_name: globals().get(variable_name)\n", + " for variable_name in required_variables\n", + " }\n", + "\n", + " result = jinja2.Environment().from_string(template).render(context)\n", + "\n", + " return result\n", + "\n", + " rendered_title = render_template(\"PV Eigenverbrauch in %\")\n", + " rendered_comparison_title = render_template(\"\")\n", + "\n", + " return json.dumps({\n", + " \"comparisonTitle\": rendered_comparison_title,\n", + " \"comparisonValue\": \"\",\n", + " \"title\": rendered_title,\n", + " \"value\": f\"{pv_self_consumption_share}\"\n", + " })\n", + "\n", + "__deepnote_big_number__()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": { + "cell_id": "b31d2458f8b64dcf8f0a3ea5511a77e0", + "deepnote_app_block_group_id": "77790c88af014d8e95f12134a403cb13", + "deepnote_app_block_order": 44, + "deepnote_app_block_visible": true, + "deepnote_big_number_comparison_enabled": false, + "deepnote_big_number_comparison_format": "", + "deepnote_big_number_comparison_title": "", + "deepnote_big_number_comparison_type": "", + "deepnote_big_number_comparison_value": "", + "deepnote_big_number_format": "percent", + "deepnote_big_number_title": "PV-Anteil Gesamtvb. (%)", + "deepnote_big_number_value": "pv_total_consumption_share", + "deepnote_cell_type": "big-number", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 1, + "execution_start": 1758275932591, + "source_hash": "8c0870fe" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "'{\"comparisonTitle\": \"\", \"comparisonValue\": \"\", \"title\": \"PV-Anteil Gesamtvb. (%)\", \"value\": \"0\"}'" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\n", + "def __deepnote_big_number__():\n", + " import json\n", + " import jinja2\n", + " from jinja2 import meta\n", + "\n", + " def render_template(template):\n", + " parsed_content = jinja2.Environment().parse(template)\n", + "\n", + " required_variables = meta.find_undeclared_variables(parsed_content)\n", + "\n", + " context = {\n", + " variable_name: globals().get(variable_name)\n", + " for variable_name in required_variables\n", + " }\n", + "\n", + " result = jinja2.Environment().from_string(template).render(context)\n", + "\n", + " return result\n", + "\n", + " rendered_title = render_template(\"PV-Anteil Gesamtvb. (%)\")\n", + " rendered_comparison_title = render_template(\"\")\n", + "\n", + " return json.dumps({\n", + " \"comparisonTitle\": rendered_comparison_title,\n", + " \"comparisonValue\": \"\",\n", + " \"title\": rendered_title,\n", + " \"value\": f\"{pv_total_consumption_share}\"\n", + " })\n", + "\n", + "__deepnote_big_number__()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": { + "cell_id": "262c822a54a74a4b96dfed7ad46b41ac", + "deepnote_app_block_group_id": null, + "deepnote_app_block_order": 45, + "deepnote_app_block_visible": true, + "deepnote_app_is_code_hidden": true, + "deepnote_cell_type": "code", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275932641, + "is_code_hidden": true, + "source_hash": "bbc108c" + }, + "outputs": [], + "source": [ + "if run_notebook:\n", + " _ = display_pv_energy(energy_report_df, resolution)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "19abe1e70d964c74ac88dd5faff57369", + "deepnote_app_block_group_id": null, + "deepnote_app_block_order": 46, + "deepnote_app_block_visible": true, + "deepnote_cell_type": "text-cell-p", + "formattedRanges": [ + { + "fromCodePoint": 0, + "marks": { + "bold": true + }, + "toCodePoint": 12, + "type": "marks" + } + ] + }, + "source": [ + "PV-Leistung \n", + "Hier sehen Sie sowohl die kumulierte Leistung der PV-Anlagen als auch die gestapelten Einzelleistungen der jeweiligen PV-Anlagen. Die Netzanschluss-Kurve zeigt an, wie viel Stromüberschüsse ins Netz eingespeist wurden (Kurve im negativen Bereich). Beziehungsweise wieviel Strom vom Netz bezogen wurde (Kurve im positiven Bereich).\n", + "Um einzelne PV-Anlagen zu filtern muss zunächst \"Alle\" aus dem Filter entfernt und dann die gewünschten PV IDs ausgewählt werden." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": { + "cell_id": "9a0b2460997d4b8bb06b2b7a3bb6b58b", + "deepnote_app_block_group_id": null, + "deepnote_app_block_order": 47, + "deepnote_app_block_visible": true, + "deepnote_app_is_code_hidden": true, + "deepnote_cell_type": "code", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275932692, + "is_code_hidden": true, + "source_hash": "f26c86bf" + }, + "outputs": [], + "source": [ + "if run_notebook:\n", + " pv_columns = [c for c in energy_report_df.columns if \"PV #\" in c]\n", + " if 'pv' in component_types:\n", + " pv_grid_filter_options = [\"PV und Netzanschluss\", \"Nur PV\", \"Nur Netzanschluss\"]\n", + " pv_filter_options = ['Alle'] + [pv[3:] for pv in pv_columns]" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "cell_id": "0d452449413546edb8a2c37be14b6fb0", + "deepnote_allow_multiple_values": true, + "deepnote_app_block_group_id": "e55c5644842748669bb303ed58013f38", + "deepnote_app_block_order": 49, + "deepnote_app_block_visible": true, + "deepnote_cell_type": "input-select", + "deepnote_input_label": "PV Anlage Filter", + "deepnote_variable_custom_options": [ + "Option 1", + "Option 2" + ], + "deepnote_variable_name": "pv_filter", + "deepnote_variable_options": [ + "Alle", + "#259" + ], + "deepnote_variable_select_type": "from-variable", + "deepnote_variable_selected_variable": "pv_filter_options", + "deepnote_variable_value": [ + "Alle" + ], + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 1, + "execution_start": 1758275932751, + "source_hash": "78b17b07" + }, + "outputs": [], + "source": [ + "pv_filter = ['Alle']" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "1726c9ed9ef6434e839c2ac01c389093", + "deepnote_app_block_group_id": "e55c5644842748669bb303ed58013f38", + "deepnote_app_block_order": 50, + "deepnote_app_block_visible": true, + "deepnote_button_behavior": "set_variable", + "deepnote_button_color_scheme": "neutral", + "deepnote_button_title": "Filtern", + "deepnote_cell_type": "button", + "deepnote_variable_name": "Filter_pv", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275932802, + "source_hash": "9488b98" + }, + "source": [ + "Filter_pv = False" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "cell_id": "4a220645a33c47d2a5379f9ca8359490", + "deepnote_app_is_code_hidden": true, + "deepnote_cell_type": "code", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 1, + "execution_start": 1758275932851, + "is_code_hidden": true, + "source_hash": "f9c3319a" + }, + "outputs": [ + { + "data": { + "application/vnd.deepnote.dataframe.v3+json": { + "column_count": 0, + "columns": [], + "preview_row_count": 0, + "row_count": 0, + "rows": [], + "type": "dataframe" + }, + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
" + ], + "text/plain": [ + "Empty DataFrame\n", + "Columns: []\n", + "Index: []" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "if run_notebook or Filter_pv:\n", + " pv_analyse_df = pd.DataFrame()\n", + " pv_filter = [\"All\" if x == \"Alle\" else x for x in pv_filter]\n", + "\n", + " if 'pv' in component_types and 'timestamp' in energy_report_df.columns:\n", + " pv_analyse_df = build_pv_analysis_df(energy_report_df, pv_filter)\n", + " pv_analyse_df = mapper.to_display(pv_analyse_df)\n", + "\n", + "else:\n", + " pv_analyse_df = pd.DataFrame()\n", + "pv_analyse_df" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "cell_id": "669a57d9f95c4b0d87d628cfc37f2d5a", + "deepnote_app_is_code_hidden": true, + "deepnote_cell_type": "code", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275932902, + "is_code_hidden": true, + "source_hash": "6d501105" + }, + "outputs": [], + "source": [ + "if run_notebook:\n", + " if \"pv\" in component_types:\n", + " fig = plot_time_series(pv_analyse_df, time_col=\"Zeitpunkt\", cols=[\"Netzanschluss\", \"PV Einspeisung\"], title=\"PV Leistung\")\n", + " fig.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "8fa85e393ed9444389be47eac15d1566", + "deepnote_app_block_group_id": null, + "deepnote_app_block_order": 53, + "deepnote_app_block_visible": true, + "deepnote_cell_type": "text-cell-p", + "formattedRanges": [], + "is_collapsed": false + }, + "source": [] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "b7e1968fa65e42cc80634da9252b564e", + "deepnote_app_block_group_id": null, + "deepnote_app_block_order": 53, + "deepnote_app_block_visible": true, + "deepnote_cell_type": "text-cell-h2", + "formattedRanges": [], + "is_collapsed": false + }, + "source": [ + "## Batterie" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": { + "cell_id": "46cb212074a24598aac8172c2c202821", + "deepnote_app_block_group_id": null, + "deepnote_app_block_order": 54, + "deepnote_app_block_visible": true, + "deepnote_app_is_code_hidden": true, + "deepnote_app_is_output_hidden": false, + "deepnote_cell_type": "code", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275932952, + "is_code_hidden": true, + "source_hash": "f4691930" + }, + "outputs": [], + "source": [ + "if run_notebook:\n", + " print(f'{datetime.strftime(start_date.astimezone(ZoneInfo(\"CET\")).date(), \"%d.%m.%Y\")} - {datetime.strftime(end_date.astimezone(ZoneInfo(\"CET\")).date(), \"%d.%m.%Y\")}')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "7d5428b364a344639ea6f100af6a3f4f", + "deepnote_app_block_group_id": null, + "deepnote_app_block_order": 55, + "deepnote_app_block_visible": true, + "deepnote_cell_type": "text-cell-p", + "formattedRanges": [ + { + "fromCodePoint": 0, + "marks": { + "bold": true + }, + "toCodePoint": 23, + "type": "marks" + } + ] + }, + "source": [ + "Batterieleistungskurve \n", + "Die Batterieleistungs-Kurve zeigt kumuliert für alle Batterien an, wie viel Strom aus der Batterie bezogen wurde (Kurve im negativen Bereich). Beziehungsweise wieviel Strom in die Batterie eingespeist wurde (Kurve im positiven Bereich). \n", + "Um einzelne Batterien zu filtern muss zunächst \"Alle\" aus dem Filter entfernt und dann die gewünschten Batterie IDs ausgewählt werden." + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": { + "cell_id": "536da8e56f0e4956aaadf599947f485a", + "deepnote_app_block_group_id": null, + "deepnote_app_block_order": 56, + "deepnote_app_block_visible": true, + "deepnote_app_is_code_hidden": true, + "deepnote_app_is_output_hidden": false, + "deepnote_cell_type": "code", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275933012, + "is_code_hidden": true, + "source_hash": "41df19b9" + }, + "outputs": [], + "source": [ + "if run_notebook:\n", + " if 'battery' in component_types:\n", + " battery_filter_options = ['Alle'] + [f'#{i}' for i in mcfg.component_type_ids('battery')]" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "allow_embed": false, + "cell_id": "40e60f95f8d242eda8c94858bf357b2e", + "deepnote_allow_multiple_values": true, + "deepnote_app_block_group_id": "b333a9f757a4456d9c54f6e259453c62", + "deepnote_app_block_order": 57, + "deepnote_app_block_visible": true, + "deepnote_cell_type": "input-select", + "deepnote_input_label": "Batterie Filter", + "deepnote_variable_custom_options": [ + "Option 1", + "Option 2" + ], + "deepnote_variable_name": "bat_filter", + "deepnote_variable_options": [ + "Alle", + "#260", + "#263", + "#266", + "#269", + "#272", + "#275", + "#278", + "#281", + "#284", + "#287", + "#290", + "#293", + "#296" + ], + "deepnote_variable_select_type": "from-variable", + "deepnote_variable_selected_variable": "battery_filter_options", + "deepnote_variable_value": [ + "Alle" + ], + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275933072, + "source_hash": "916271a" + }, + "outputs": [], + "source": [ + "bat_filter = ['Alle']" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "678668530c264079959af52086f0f393", + "deepnote_app_block_group_id": "b333a9f757a4456d9c54f6e259453c62", + "deepnote_app_block_order": 58, + "deepnote_app_block_visible": true, + "deepnote_button_behavior": "set_variable", + "deepnote_button_color_scheme": "neutral", + "deepnote_button_title": "Filtern", + "deepnote_cell_type": "button", + "deepnote_variable_name": "filter_battery", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275933122, + "source_hash": "2c7548d2" + }, + "source": [ + "filter_battery = False" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "b15644e72758409fa721e1cd2c5b0f71", + "deepnote_app_block_group_id": "b333a9f757a4456d9c54f6e259453c62", + "deepnote_app_block_order": 59, + "deepnote_app_block_visible": true, + "deepnote_cell_type": "text-cell-p", + "formattedRanges": [] + }, + "source": [] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "cell_id": "57e1fcac9d2c491ca6aabff41e4fc1f7", + "deepnote_app_is_code_hidden": true, + "deepnote_cell_type": "code", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 1, + "execution_start": 1758275933172, + "is_code_hidden": true, + "source_hash": "70d0c912" + }, + "outputs": [], + "source": [ + "if run_notebook or filter_battery:\n", + " if \"battery\" in {str(x).lower() for x in component_types}:\n", + " bat_filter = [\"All\" if x == \"Alle\" else x for x in bat_filter]\n", + " bat_analyse_df = build_component_analysis(energy_report_df, bat_filter, 'Battery', 'Batterie Durchsatz')\n", + " bat_analyse_df = mapper.to_display(bat_analyse_df)\n", + " fig = plot_time_series(bat_analyse_df, time_col=\"Zeitpunkt\", cols=[\"Batterie Durchsatz\"], title=\"Batterie Durchsatz\", legend_title=None)\n", + " fig.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "fd55e172942748c5be2eb2bf363a806c", + "deepnote_app_block_group_id": null, + "deepnote_app_block_order": 62, + "deepnote_app_block_visible": true, + "deepnote_cell_type": "text-cell-h2", + "formattedRanges": [], + "is_collapsed": false + }, + "source": [ + "## Netzbezug" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "allow_embed": false, + "cell_id": "d5d69e9bfbc948159feaa75e7977c91f", + "deepnote_app_block_group_id": null, + "deepnote_app_block_order": 63, + "deepnote_app_block_visible": true, + "deepnote_app_is_code_hidden": true, + "deepnote_app_is_output_hidden": false, + "deepnote_cell_type": "code", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275933222, + "is_code_hidden": true, + "source_hash": "f4691930" + }, + "outputs": [], + "source": [ + "if run_notebook:\n", + " print(f'{datetime.strftime(start_date.astimezone(ZoneInfo(\"CET\")).date(), \"%d.%m.%Y\")} - {datetime.strftime(end_date.astimezone(ZoneInfo(\"CET\")).date(), \"%d.%m.%Y\")}')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "792851ccf6564cc2b733a3f2bfe491cc", + "deepnote_app_block_group_id": null, + "deepnote_app_block_order": 64, + "deepnote_app_block_visible": true, + "deepnote_cell_type": "text-cell-p", + "formattedRanges": [ + { + "fromCodePoint": 0, + "marks": { + "bold": true + }, + "toCodePoint": 11, + "type": "marks" + } + ] + }, + "source": [ + "Bezugskurve\n", + "Die Bezugskurve zeigt wie viel Strom aus dem Stromnetz bezogen wurde." + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": { + "cell_id": "a0160f11409e46d1bd4bc0263e5abb32", + "deepnote_app_is_code_hidden": true, + "deepnote_cell_type": "code", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275933272, + "is_code_hidden": true, + "source_hash": "dde45d39" + }, + "outputs": [], + "source": [ + "if run_notebook:\n", + " fig = plot_time_series(overview_df, time_col=\"Zeitpunkt\", cols=[\"Netzbezug\"], title=\"Netzbezug\")\n", + " fig.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "f35eb0d9cf6544e99221a37afe68cc10", + "deepnote_cell_type": "text-cell-h1", + "formattedRanges": [] + }, + "source": [ + "# CHP" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": { + "cell_id": "68bae17f70dd4712b2cdfd90655792e7", + "deepnote_app_is_code_hidden": true, + "deepnote_cell_type": "code", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275933322, + "is_code_hidden": true, + "source_hash": "71a4bce9" + }, + "outputs": [], + "source": [ + "if run_notebook:\n", + " if 'chp' in component_types:\n", + " chp_filter_options = ['Alle'] + [f'#{i}' for i in mcfg.component_type_ids('chp')]" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": { + "allow_embed": false, + "cell_id": "ef4d6cbba2b8428a9264e367787022b6", + "deepnote_allow_multiple_values": true, + "deepnote_app_block_visible": true, + "deepnote_cell_type": "input-select", + "deepnote_input_label": "CHP Filter", + "deepnote_variable_custom_options": [ + "Option 1", + "Option 2" + ], + "deepnote_variable_default_value": "", + "deepnote_variable_name": "chp_filter", + "deepnote_variable_options": [ + "Alle", + "#258" + ], + "deepnote_variable_select_type": "from-variable", + "deepnote_variable_selected_variable": "chp_filter_options", + "deepnote_variable_value": [ + "Alle" + ], + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275933372, + "source_hash": "44b433ed" + }, + "outputs": [], + "source": [ + "chp_filter = ['Alle']" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "77673dee82c341debc6765249a29df85", + "deepnote_button_behavior": "set_variable", + "deepnote_button_color_scheme": "neutral", + "deepnote_button_title": "Filtern", + "deepnote_cell_type": "button", + "deepnote_variable_name": "filter_chp", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 1, + "execution_start": 1758275933431, + "source_hash": "d5d54beb" + }, + "source": [ + "filter_chp = False" + ] + }, + { + "cell_type": "code", + "execution_count": 45, + "metadata": { + "cell_id": "e0125381471740b19a806dc20dc30d53", + "deepnote_app_is_code_hidden": true, + "deepnote_cell_type": "code", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275933492, + "is_code_hidden": true, + "source_hash": "19a550d5" + }, + "outputs": [], + "source": [ + "if run_notebook or filter_chp:\n", + " if \"chp\" in {str(x).lower() for x in component_types}:\n", + " chp_filter = [\"All\" if x == \"Alle\" else x for x in chp_filter]\n", + " chp_analyse_df = build_component_analysis(energy_report_df, chp_filter, 'CHP', 'CHP Usage')\n", + " chp_analyse_df = mapper.to_display(chp_analyse_df)\n", + " fig = plot_time_series(chp_analyse_df, time_col=\"Zeitpunkt\", cols=[\"CHP Usage\"], title=\"CHP\", legend_title=None)\n", + " fig.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "db94528695284b41b98322ae1b776e5e", + "deepnote_cell_type": "text-cell-h1", + "formattedRanges": [] + }, + "source": [ + "# EV" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": { + "cell_id": "c9ce293960c8433d88ed6782fa72d0aa", + "deepnote_app_is_code_hidden": true, + "deepnote_cell_type": "code", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275933553, + "is_code_hidden": true, + "source_hash": "b52555c1" + }, + "outputs": [], + "source": [ + "if run_notebook:\n", + " if 'ev' in component_types:\n", + " ev_filter_options = ['Alle'] + [f'#{i}' for i in mcfg.component_type_ids('ev')]" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": { + "allow_embed": false, + "cell_id": "b8d53a5b1b6c488db55ab567212dc808", + "deepnote_allow_multiple_values": true, + "deepnote_app_block_visible": true, + "deepnote_cell_type": "input-select", + "deepnote_input_label": "EV Filter", + "deepnote_variable_custom_options": [ + "Option 1", + "Option 2" + ], + "deepnote_variable_default_value": "", + "deepnote_variable_name": "ev_filter", + "deepnote_variable_options": [ + "Alle", + "#528" + ], + "deepnote_variable_select_type": "from-variable", + "deepnote_variable_selected_variable": "ev_filter_options", + "deepnote_variable_value": "Alle", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275933612, + "source_hash": "c8b2f31e" + }, + "outputs": [], + "source": [ + "ev_filter = ['Alle']" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "allow_embed": false, + "cell_id": "69b1250c2de346f6a5db54dbe40e1624", + "deepnote_button_behavior": "set_variable", + "deepnote_button_color_scheme": "neutral", + "deepnote_button_title": "Filtern", + "deepnote_cell_type": "button", + "deepnote_variable_name": "filter_ev", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275933662, + "source_hash": "5621a508" + }, + "source": [ + "filter_ev = False" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": { + "cell_id": "8a83df8b97c841bf9f11e4c20aad18e6", + "deepnote_app_is_code_hidden": true, + "deepnote_cell_type": "code", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 1, + "execution_start": 1758275933721, + "is_code_hidden": true, + "source_hash": "c874ed2a" + }, + "outputs": [], + "source": [ + "if run_notebook or filter_ev:\n", + " if \"ev\" in {str(x).lower() for x in component_types}:\n", + " ev_filter = [\"All\" if x == \"Alle\" else x for x in ev_filter]\n", + " ev_analyse_df = build_component_analysis(energy_report_df, ev_filter, 'EV', 'EV Usage')\n", + " ev_analyse_df = mapper.to_display(ev_analyse_df)\n", + " fig = plot_time_series(ev_analyse_df, time_col=\"Zeitpunkt\", cols=[\"EV Usage\"], title=\"EV\", legend_title=None)\n", + " fig.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "06102019e90441b4bf67f051758d9628", + "deepnote_app_block_group_id": null, + "deepnote_app_block_order": 66, + "deepnote_app_block_visible": true, + "deepnote_cell_type": "text-cell-h2", + "formattedRanges": [] + }, + "source": [ + "## Daten" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "metadata": { + "cell_id": "bd479eb15e014a2ab3b26dc39993239f", + "deepnote_app_block_group_id": null, + "deepnote_app_block_order": 67, + "deepnote_app_block_visible": true, + "deepnote_app_is_code_hidden": true, + "deepnote_app_is_output_hidden": false, + "deepnote_cell_type": "code", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275933782, + "is_code_hidden": true, + "source_hash": "f4691930" + }, + "outputs": [], + "source": [ + "if run_notebook:\n", + " print(f'{datetime.strftime(start_date.astimezone(ZoneInfo(\"CET\")).date(), \"%d.%m.%Y\")} - {datetime.strftime(end_date.astimezone(ZoneInfo(\"CET\")).date(), \"%d.%m.%Y\")}')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "2035d1353f764f5691d7e01b237111ad", + "deepnote_app_block_group_id": null, + "deepnote_app_block_order": 69, + "deepnote_app_block_visible": true, + "deepnote_cell_type": "text-cell-h3", + "formattedRanges": [] + }, + "source": [ + "### Daten exportieren" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "60e17e2af6b2441090cbb5759d33bea6", + "deepnote_app_block_group_id": null, + "deepnote_app_block_order": 70, + "deepnote_app_block_visible": true, + "deepnote_cell_type": "text-cell-p", + "formattedRanges": [ + { + "fromCodePoint": 124, + "ranges": [], + "toCodePoint": 137, + "type": "link", + "url": "google.com" + } + ] + }, + "source": [ + "Die Daten werden unter angegebenem Dateinamen in den Deepnote Files gespeichert und können von dort heruntergeladen werden.\n", + "Mehr erfahren" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": { + "cell_id": "d9c37e107665477984792ed5a9ca8edf", + "deepnote_app_block_group_id": "cd6f5514d1bd4d4c875db036488ed3b7", + "deepnote_app_block_order": 71, + "deepnote_app_block_visible": true, + "deepnote_cell_type": "input-text", + "deepnote_input_label": "Dateiname (.csv oder .xlsx)", + "deepnote_variable_name": "output", + "deepnote_variable_value": "", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 1, + "execution_start": 1758275933831, + "source_hash": "f512bf09" + }, + "outputs": [], + "source": [ + "output = ''" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "55a2edf436f1487ca215c35118aca21e", + "deepnote_app_block_group_id": "cd6f5514d1bd4d4c875db036488ed3b7", + "deepnote_app_block_order": 72, + "deepnote_app_block_visible": true, + "deepnote_button_behavior": "run", + "deepnote_button_color_scheme": "blue", + "deepnote_button_title": "Daten exportieren", + "deepnote_cell_type": "button", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275933892, + "source_hash": "b623e53d" + }, + "source": [] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "97ec1dd98b7e4bd4b07c19d0ec415da0", + "deepnote_app_block_group_id": "cd6f5514d1bd4d4c875db036488ed3b7", + "deepnote_app_block_order": 73, + "deepnote_app_block_visible": true, + "deepnote_cell_type": "text-cell-p", + "formattedRanges": [] + }, + "source": [] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": { + "cell_id": "4008a18973d24e39a8cebf909cf540dd", + "deepnote_app_block_group_id": null, + "deepnote_app_block_order": 74, + "deepnote_app_block_visible": true, + "deepnote_app_is_code_hidden": true, + "deepnote_cell_type": "code", + "execution_context_id": "9ed01f74-bc5f-48b0-bf64-544988d2275c", + "execution_millis": 0, + "execution_start": 1758275933941, + "is_code_hidden": true, + "source_hash": "2d6cfdc" + }, + "outputs": [], + "source": [ + "if output:\n", + " if os.path.exists(output):\n", + " print(\"Fehler: Die Daten wurden nicht exportiert. Es gibt bereits eine Datei mit diesem Namen.\")\n", + " elif output.endswith(\".csv\"):\n", + " master_df['Zeitpunkt'] = master_df['Zeitpunkt'].dt.strftime(\"%d.%m.%Y %H:%M:%S\")\n", + " master_df.to_csv(output, index=False)\n", + " print(f\"Die Daten wurden erfolgreich in {output} exportiert.\")\n", + " elif output.endswith(\".xlsx\"):\n", + " master_df['Zeitpunkt'] = master_df['Zeitpunkt'].dt.strftime(\"%d.%m.%Y %H:%M:%S\")\n", + " master_df.to_excel(output, index=False)\n", + " print(f\"Die Daten wurden erfolgreich in {output} exportiert.\")\n", + " else:\n", + " print(\"Fehler: Die Daten wurden nicht exportiert. Output muss .csv oder .xlsx sein.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cell_id": "b9a60a2bbc2d4e66b2f68f5a3ffce6ed", + "deepnote_cell_type": "text-cell-p", + "formattedRanges": [] + }, + "source": [] + }, + { + "cell_type": "markdown", + "metadata": { + "created_in_deepnote_cell": true, + "deepnote_cell_type": "markdown" + }, + "source": [ + "\n", + "Created in deepnote.com \n", + "Created in Deepnote" + ] + } + ], + "metadata": { + "deepnote_app_clear_outputs": false, + "deepnote_app_embed_enabled": true, + "deepnote_app_hide_all_code_blocks_enabled": false, + "deepnote_app_layout": "powerful-article", + "deepnote_app_reactivity_enabled": true, + "deepnote_app_run_on_input_enabled": false, + "deepnote_app_run_on_load_enabled": false, + "deepnote_app_table_of_contents_enabled": true, + "deepnote_notebook_id": "7651f1df2ac94b7b97b7b494e8f08a84", + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/pyproject.toml b/pyproject.toml index 774c793e..f2096604 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,8 @@ dependencies = [ "kaleido >= 0.2.1, < 1.1.0", "frequenz-client-reporting >= 0.19.0, < 0.20.0", "frequenz-client-weather >= 0.2.3, < 0.3.0", + "pyyaml>=6.0.2", + "types-pyyaml>=6.0.12.20250915", ] dynamic = ["version"] diff --git a/src/frequenz/lib/notebooks/reporting/data_processing.py b/src/frequenz/lib/notebooks/reporting/data_processing.py new file mode 100644 index 00000000..d9da2ffd --- /dev/null +++ b/src/frequenz/lib/notebooks/reporting/data_processing.py @@ -0,0 +1,115 @@ +# License: MIT +# Copyright © 2025 Frequenz Energy-as-a-Service GmbH + +"""Microgrid Reporting DataFrame Construction. + +This module constructs normalized energy-report DataFrames from +raw microgrid telemetry by harmonizing timestamps and column naming, +enriching PV flows, adding grid KPIs, and surfacing component-specific +metrics used downstream for dashboards. + +Key Features +------------ +- Energy Report DataFrame Construction + - :func:`create_energy_report_df`: Builds a normalized energy report table with + unified naming, timezone conversion, grid import calculation, and + component renaming based on a MicrogridConfig. + +Usage +----- +Use create_energy_report_dfs() inside reporting pipelines or notebooks to +transform raw microgrid exports into localized, labeled, and analysis-ready +tables for KPIs, dashboards, and stakeholder reporting. +""" + + +from typing import List + +import pandas as pd + +from frequenz.data.microgrid.config import MicrogridConfig +from frequenz.lib.notebooks.reporting.utils.column_mapper import ColumnMapper +from frequenz.lib.notebooks.reporting.utils.helpers import ( + _add_pv_energy_flows, + _convert_timezone, + add_net_grid_import, + get_energy_report_columns, + label_component_columns, +) + + +# pylint: disable=too-many-arguments, disable=too-many-locals +def create_energy_report_dfs( + df: pd.DataFrame, + component_types: List[str], + mcfg: MicrogridConfig, + mapper: ColumnMapper, + *, + tz_name: str = "Europe/Berlin", + assume_tz: str = "UTC", +) -> pd.DataFrame: + """Create a normalized Energy Report DataFrame with selected columns. + + Makes a copy of the input, converts the timestamp column to the configured + timezone, renames standard columns to unified names, adds the net import + column, renames numeric component IDs to labeled names, and returns a + reduced DataFrame containing only relevant columns. + + Args: + df: Raw input table containing energy data. + component_types: Component types to include in the Energy Report DataFrame + (e.g., ``battery``, ``pv``). + mcfg: Configuration object used to resolve component IDs. + mapper: Column Mapper object to standardize the column names. + tz_name: Target timezone name for timestamp conversion (default: "Europe/Berlin"). + assume_tz: Timezone to assume for naive datetimes before conversion (default: "UTC"). + + Returns: + The Energy Report DataFrame with standardized and selected columns. + + Notes: + Component IDs are renamed to labeled names via ``label_component_columns()``. + """ + energy_report_df = df.copy() + + # Only reset index if it's a datetime or period index and 'timestamp' column is missing + if isinstance(energy_report_df.index, (pd.DatetimeIndex, pd.PeriodIndex)): + if "timestamp" not in energy_report_df.columns: + energy_report_df = energy_report_df.reset_index(names="timestamp") + + # Add PV energy flow columns + energy_report_df = _add_pv_energy_flows(energy_report_df) + + # Standardize column names (from raw to canonical) + energy_report_df = mapper.to_canonical(energy_report_df) + + # Convert timezone + energy_report_df = _convert_timezone( + energy_report_df, + column_timestamp="timestamp", + target_tz=tz_name, + assume_tz=assume_tz, + ) + + # Add grid consumption column + energy_report_df = add_net_grid_import( + energy_report_df, + column_grid="grid", + column_net_import="net_import", + ) + + # Helper to rename numeric component IDs to labeled names like PV #250, Battery #219 + energy_report_df, single_components = label_component_columns( + energy_report_df, + mcfg, + column_battery="battery", + column_pv="pv", + ) + + energy_report_df_cols = get_energy_report_columns( + component_types, single_components + ) + + # Select only the relevant columns + energy_report_df = energy_report_df[energy_report_df_cols] + return energy_report_df diff --git a/src/frequenz/lib/notebooks/reporting/utils/__init__.py b/src/frequenz/lib/notebooks/reporting/utils/__init__.py new file mode 100644 index 00000000..60996680 --- /dev/null +++ b/src/frequenz/lib/notebooks/reporting/utils/__init__.py @@ -0,0 +1,4 @@ +# License: MIT +# Copyright © 2025 Frequenz Energy-as-a-Service GmbH + +"""Initialise the utility helper modules.""" diff --git a/src/frequenz/lib/notebooks/reporting/utils/helpers.py b/src/frequenz/lib/notebooks/reporting/utils/helpers.py new file mode 100644 index 00000000..5a330c28 --- /dev/null +++ b/src/frequenz/lib/notebooks/reporting/utils/helpers.py @@ -0,0 +1,329 @@ +# License: MIT +# Copyright © 2025 Frequenz Energy-as-a-Service GmbH +"""Helper function for Microgrid Data Processing Utilities. + +This module provides utility functions for preprocessing and analyzing microgrid +data represented in pandas DataFrames. It standardizes column names, handles +timezone conversions, computes grid imports, derives photovoltaic (PV) energy flows, +and renames component-specific columns based on a MicrogridConfig. + +Key Features +------------ +- Timezone Conversion + Ensures all timestamps are consistently localized + (default: UTC → Europe/Berlin). + +- Grid Data Processing + Extracts net grid import by filtering positive values + from grid connection signals. + +- PV Energy Flow Calculations + Derives PV production, excess, self-consumption, battery charging, and + grid feed-in metrics, including PV self-consumption share. + +- Component Renaming + Maps numeric string component IDs to human-readable labels + (e.g., "Battery #14", "PV #7") using the provided MicrogridConfig. + +- Reporting Column Assembly + Builds the column sets required for downstream energy reports + based on the available component types. + +Usage +----- +These functions serve as building blocks for energy reporting, data pipelines, +and dashboards that analyze microgrid performance, particularly in hybrid systems +with PV, batteries, and grid interactions. +""" + +from typing import Any, Dict, List, Tuple + +import pandas as pd +import yaml + +from frequenz.data.microgrid.config import MicrogridConfig + + +def load_config(path: str) -> Dict[str, Any]: + """ + Load a YAML config file and return it as a dictionary. + + Args: + path: Path to the YAML file. + + Returns: + Configuration values as a dictionary. + + Raises: + TypeError: If the YAML root element is not a mapping (dict). + """ + with open(path, "r", encoding="utf-8") as f: + data = yaml.safe_load(f) + + if not isinstance(data, dict): + raise TypeError("YAML root must be a mapping (dict).") + + return data + + +def _fmt_de(x: float) -> str: + """Format a number using German-style decimal and thousands separators. + + The function formats the number with two decimal places, using a comma + as the decimal separator and a dot as the thousands separator. + + Args: + x: The number to format. + + Returns: + The formatted string with German number formatting applied. + + Example: + >>> _fmt_de(12345.6789) + '12.345,68' + """ + return f"{x:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".") + + +def _convert_timezone( + df: pd.DataFrame, + column_timestamp: str, + target_tz: str = "Europe/Berlin", + assume_tz: str = "UTC", +) -> pd.DataFrame: + """Convert a datetime column in a DataFrame to a target timezone. + + If the column contains timezone-naive datetimes, they are first localized to + ``assume_tz`` before being converted to ``target_tz``. + + Args: + df: Input DataFrame containing the datetime column. + column_timestamp: Name of the datetime column in ``df`` to convert. + target_tz: Timezone name to convert the column to. + Defaults to ``"Europe/Berlin"``. + assume_tz: Timezone to assume for naive datetimes. + Defaults to ``"UTC"``. + + Returns: + pd.DataFrame: A copy of the DataFrame with the converted datetime column. + + Raises: + ValueError: If ``column_timestamp`` is not present in ``df``. + """ + if column_timestamp not in df: + raise ValueError(f"{column_timestamp} column not in df") + + ts = df[column_timestamp] + + if ts.dt.tz is None: + # Assume naïve datetimes are in `assume_tz` + ts = ts.dt.tz_localize(assume_tz) + + df[column_timestamp] = ts.dt.tz_convert(target_tz) + return df + + +def add_net_grid_import( + df: pd.DataFrame, + column_grid: str, + column_net_import: str, +) -> pd.DataFrame: + """Calculate grid consumption and add it as ``column_net_import``. + + Grid consumption is defined as the positive part of ``column_grid``. + Negative values are replaced with 0. + + Args: + df: Input DataFrame containing the grid data. + column_grid: Name of the column in ``df`` that contains grid values. + column_net_import: Name of the output column to store the computed + net import values. + + Returns: + pd.DataFrame: The DataFrame with a new or updated ``column_net_import`` column. + + Raises: + ValueError: If ``column_grid`` is not present in ``df``. + """ + if column_grid not in df: + raise ValueError(f"{column_grid} column not in df") + + df[column_net_import] = df[column_grid].apply(lambda x: x if x > 0 else 0) + return df + + +# pylint: disable=too-many-arguments, too-many-positional-arguments +def label_component_columns( + df: pd.DataFrame, + mcfg: MicrogridConfig, + column_battery: str = "battery", + column_pv: str = "pv", + column_chp: str = "chp", + column_ev: str = "ev", +) -> Tuple[pd.DataFrame, List[str]]: + """Rename numeric single-component columns to labeled names. + + Numeric string column names like ``"14"`` are converted to + ``"Battery #14"``, ``"PV #14"``, ``"CHP #14"`` or ``"EV #14"`` based on + the component IDs provided by ``mcfg.component_type_ids(...)`` + + Args: + df: Input DataFrame with numeric string column names. + mcfg: Configuration with ``_component_types_cfg`` mapping component types to a + ``meter`` iterable of numeric IDs. + column_battery: Key name for battery component type. + column_pv: Key name for PV component type. + column_chp: Key name for CHP component type. + column_ev: Key name for EV component type + Returns: + Tuple containing the renamed DataFrame and the list of applied labels + """ + # Numeric component columns present in df + single_components = [str(c) for c in df.columns if str(c).isdigit()] + available_types = set(mcfg.component_types()) + + # From config (empty set if missing) + def ids_if_available(t: str) -> set[str]: + return ( + {str(x) for x in mcfg.component_type_ids(t)} + if t in available_types + else set() + ) + + battery_ids = ids_if_available(column_battery) + pv_ids = ids_if_available(column_pv) + chp_ids = ids_if_available(column_chp) + ev_ids = ids_if_available(column_ev) + + rename: Dict[str, str] = {} + rename.update( + { + c: f"{column_battery.capitalize()} #{c}" + for c in single_components + if c in battery_ids + } + ) + rename.update( + {c: f"{column_pv.upper()} #{c}" for c in single_components if c in pv_ids} + ) + rename.update( + {c: f"{column_ev.upper()} #{c}" for c in single_components if c in ev_ids} + ) + rename.update( + {c: f"{column_chp.upper()} #{c}" for c in single_components if c in chp_ids} + ) + + return df.rename(columns=rename), list(rename.values()) + + +def _add_pv_energy_flows(df: pd.DataFrame) -> pd.DataFrame: + """Add PV-related energy flow columns to ``df`` if PV data is present. + + Derives photovoltaic (PV) energy-flow metrics from existing columns. If no PV + signal is present (i.e., the negative PV column is missing or all zeros), the + DataFrame is returned unchanged. + + Args: + df: Input DataFrame. If present, uses columns ``pv_neg``, + ``consumption``, and ``COLUMN_BATTERY_POS``. Missing columns are + treated as zeros. + + Returns: + The DataFrame with added PV flow columns (or unchanged if no PV signal). + + Notes: + Newly created/updated columns: + - ``COLUMN_PV_PROD``: PV production as a positive series (negated/clipped from + ``pv_neg``). + - ``COLUMN_PV_EXCESS``: Excess PV after subtracting household consumption. + - ``COLUMN_PV_BAT``: Portion of PV excess routed into the battery (bounded by + battery charge). + - ``COLUMN_PV_FEEDIN``: PV fed into the grid after battery charging. + - ``COLUMN_PV_SELF``: Self-consumed PV (production minus excess). + - ``COLUMN_PV_SHARE``: Share of consumption covered by self-consumed PV (NaN + when consumption is 0). + """ + # Safe inputs (0 if missing) + df_with_pv_flows = df.copy() + zeros = pd.Series(0, index=df_with_pv_flows.index) + pv_neg = df_with_pv_flows.get("pv_neg", zeros) + consumption = df_with_pv_flows.get("consumption", zeros) + battery_pos = df_with_pv_flows.get("battery_pos", zeros) + + # Only compute PV features if there is any PV signal + has_pv = isinstance(pv_neg, pd.Series) and (pv_neg != 0).any() + if not has_pv: + return df_with_pv_flows + + df_with_pv_flows["pv_prod"] = (-pv_neg).clip(lower=0) + df_with_pv_flows["pv_excess"] = (df_with_pv_flows["pv_prod"] - consumption).clip( + lower=0 + ) + + # This naturally becomes 0 when there's no battery_pos column + df_with_pv_flows["pv_bat"] = pd.concat( + [df_with_pv_flows["pv_excess"], battery_pos], axis=1 + ).min(axis=1) + + df_with_pv_flows["pv_feedin"] = ( + df_with_pv_flows["pv_excess"] - df_with_pv_flows["pv_bat"] + ) + df_with_pv_flows["pv_self"] = ( + df_with_pv_flows["pv_prod"] - df_with_pv_flows["pv_excess"] + ).clip(lower=0) + + denom = consumption.replace(0, pd.NA) + df_with_pv_flows["pv_share"] = df_with_pv_flows["pv_self"] / denom + + return df_with_pv_flows + + +def get_energy_report_columns( + component_types: List[str], single_components: List[str] +) -> List[str]: + """Build the list of dataframe columns for the energy report. + + The selected columns depend on the available component types. + + Args: + component_types: List of component types (e.g. ["pv", "battery"]) + single_components: Extra component columns to always include. + + Returns: + The full list of dataframe columns. + """ + # Base columns + energy_report_df_cols = [ + "timestamp", + "grid", + "net_import", + "net_consumption", + ] + single_components + + # Map component types to the columns they enable + component_column_map = { + "battery": ["battery_throughput"], + "pv": [ + "pv_throughput", + "pv_prod", + "pv_self", + "pv_feedin", + ], + } + + # Define columns that require both PV and Battery + pv_battery_cols = [ + "pv_in_bat", + "pv_share", + ] + + # Add component-specific columns + for component, columns in component_column_map.items(): + if component in component_types: + energy_report_df_cols.extend(columns) + + # Add combined PV + Battery columns + if "pv" in component_types and "battery" in component_types: + energy_report_df_cols.extend(pv_battery_cols) + + return energy_report_df_cols diff --git a/src/frequenz/lib/notebooks/reporting/utils/reporting_nb_functions.py b/src/frequenz/lib/notebooks/reporting/utils/reporting_nb_functions.py new file mode 100644 index 00000000..cab07824 --- /dev/null +++ b/src/frequenz/lib/notebooks/reporting/utils/reporting_nb_functions.py @@ -0,0 +1,490 @@ +# License: MIT +# Copyright © 2025 Frequenz Energy-as-a-Service GmbH + +"""Microgrid Reporting and Analysis Utilities for the notebook. + +This module provides helpers to compute energy summaries, normalize per-component +timeseries to tidy (long) tables, and derive high-level KPIs. + +Key features +------------- +1) compute_energy_summary(df, resolution) -> DataFrame + Aggregates PV and grid energy over the time step and returns a compact table with: + - "Energy Source" (PV / Grid) + - "Energy [kWh]" (sum over period) + - "Energy %" (share of total) + - "Power [kW]" (avg power = energy / hours) + Looks for "net_import" (grid) and "pv_self" (self-consumed PV). Missing series + are treated as zero; rows are ordered PV first (if present), then Grid. + +2) display_pv_energy(energy_report_df, resolution, label_contains="PV #", …) -> dict + Prints per-component PV energies in kWh (supports German formatting) and returns + a dict of {component_label: kWh}. Optionally prints and returns "PV Total". + Uses the convention that PV power is negative and multiplies by -1. + +3) build_pv_analysis_df(energy_report_df, pv_filter) -> DataFrame + Creates a tidy, long-form PV table by melting selected "PV #N" columns into: + - "timestamp" + - optional "grid" (evenly divided across selected PVs if present) + - "PV" (component label without the "PV " prefix, e.g., "#1") + - "pv_feedin" (positive production after sign flip) + `pv_filter` accepts explicit IDs like ["#1", "#3"] or ["all"] (case-insensitive). + +4) build_component_analysis(energy_report_df, selection_filter, + component_label, value_col_name) -> DataFrame + Generic version of (3) for any component family, e.g. ("Battery", "battery"). + Returns long form with columns: "timestamp", , . + +5) build_overview_df(energy_report_df, component_types) -> DataFrame + Returns a trimmed view with essential base columns + ["timestamp", "net_import", "net_consumption"] + plus optional groups if requested (e.g., "pv" -> ["pv_prod", "pv_feedin"], + "battery" -> ["battery_throughput"]). Missing columns are safely ignored. + +6) load_microgrid_configs(directory="toml_directory/") -> (dict, list[int]) + Loads all *.toml files via `MicrogridConfig.load_configs(...)`, returning: + - configs: dict[str, MicrogridConfig] + - available_microgrids: sorted list of int IDs + +7) aggregate_pv_metrics(energy_report_df, resolution, + grid_consumption_sum, tz_name="Europe/Berlin") -> dict + Computes PV KPI totals (kWh) and shares: + - pv_feed_in_sum, pv_production_sum, pv_self_consumption_sum, pv_bat_sum + - pv_self_consumption_share = pv_self / pv_production + - pv_total_consumption_share = pv_self / (pv_self + grid_consumption_sum) + - net_site_consumption_sum = ∑ net_consumption * hours + - peak (kW) and peak_date ("DD.MM.YYYY") from "net_import" and "timestamp" + +Usage +----- +These utilities are designed for reporting pipelines and notebooks +that analyze microgrid performance in hybrid systems with PV, batteries, +and grid imports/exports. They serve as a foundation for creating +dashboards, KPIs, and energy-mix summaries in localized formats. +""" + +import os +from datetime import timedelta +from typing import Dict, Iterable, Tuple + +import pandas as pd + +from frequenz.data.microgrid.config import MicrogridConfig +from frequenz.lib.notebooks.reporting.utils.helpers import _fmt_de + + +def compute_energy_summary(df: pd.DataFrame, resolution: timedelta) -> pd.DataFrame: + """Compute an energy-mix summary (PV vs CHP vs. grid). + + Aggregates energy over the given time resolution and reports + totals in kWh, share in percent, and average power in kW. It expects + ``grid_consumption`` (grid import) and optionally ``pv_self_consumption`` + (self-consumed PV) to exist in ``df``. + + Args: + df: DataFrame containing energy data, with ``grid_consumption`` and + optionally ``pv_self_consumption``. + resolution: Row time resolution (e.g., ``"15min"`` or + ``pd.Timedelta("0 days 00:15:00")``). + + Returns: + pd.DataFrame: A summary table with columns: + - ``Energy Source``: Source label ("PV" or "Grid" or "CHP"). + - ``Energy [kWh]``: Total energy per source. + - ``Energy %``: Percentage share of total energy. + - ``Power [kW]``: Average power over the interval. + """ + resolution = pd.to_timedelta(resolution) + hours = resolution.total_seconds() / 3600.0 + pv_kwh = 0.0 + grid_kwh = 0.0 + + if "net_import" in df.columns: + grid_kwh = df["net_import"].sum() * hours + + if "pv_self" in df.columns: + pv_kwh = df["pv_self"].sum() * hours + + # Build rows (PV first if present) + rows = [] + if pv_kwh > 0: + rows.append(("PV", pv_kwh)) + if grid_kwh > 0 or not rows: + rows.append(("Grid", grid_kwh)) + + total_kwh = sum(v for _, v in rows) + denom = total_kwh if total_kwh != 0 else 1.0 # zero-total guard + + data = { + "Energy Source": [name for name, _ in rows], + "Energy [kWh]": [round(val, 2) for _, val in rows], + "Energy %": [round(val / denom * 100, 2) for _, val in rows], + "Power [kW]": [round((val / hours) if hours else 0.0, 2) for _, val in rows], + } + return pd.DataFrame(data) + + +# pylint: disable=too-many-arguments, too-many-positional-arguments +def display_pv_energy( + energy_report_df: pd.DataFrame, + resolution: timedelta, + label_contains: str = "PV #", + include_total: bool = True, + print_empty_message: bool = True, + fmt_to_de: bool = False, +) -> dict[str, float]: + """ + Summarize per-component PV energy from ``energy_report_df`` by column name pattern. + + Searches for columns whose names contain ``label_contains`` + (default: ``"PV #"``). For each matching column, it computes energy in kWh + using the time-step ``resolution``. Values are printed with localized + number formatting (comma as decimal separator). Optionally includes a + total row labeled ``"PV Total"``. + + Args: + energy_report_df: Table with PV component columns (power values per row). + resolution: Time step per row (e.g., 15 minutes). Used to convert power sums to energy. + label_contains: Substring used to identify PV component columns. + include_total: If True, also prints/returns the total across all PV components. + print_empty_message: If True, prints a friendly message when no PV columns are found. + fmt_to_de: If True, use German number formatting (comma as decimal separator). + + Returns: + Mapping of component label to energy in kWh. Includes ``"PV Total"`` when + ``include_total`` is True. + """ + # Find PV columns by name pattern + pv_columns = [c for c in energy_report_df.columns if label_contains in c] + + if not pv_columns: + if print_empty_message: + print("No PV components found.") + return {} + + step_hours = ( + (resolution.total_seconds() / 3600.0) + if hasattr(resolution, "total_seconds") + else (resolution.seconds / 3600.0) + ) + + results = {} + # Convention: PV power is negative; multiply by -1 to report positive energy (kWh) + for pv in pv_columns: + pv_sum_kwh = round(energy_report_df[pv].sum() * step_hours * -1, 2) + results[pv] = pv_sum_kwh + val_str = _fmt_de(pv_sum_kwh) if fmt_to_de else f"{pv_sum_kwh:.2f}" + print(f"{pv:<12}: {val_str} kWh") + + if include_total: + total_kwh = round(sum(results.values()), 2) + results["PV Total"] = total_kwh + val_str = _fmt_de(total_kwh) if fmt_to_de else f"{total_kwh:.2f}" + print(f"{'PV Total':<12}: {val_str} kWh") + + return results + + +def build_pv_analysis_df( + energy_report_df: pd.DataFrame, pv_filter: Iterable[str] +) -> pd.DataFrame: + """ + Build a normalized PV analysis table from ``energy_report_df``. + + Detects PV component columns (those starting with ``"PV #"``), filters them + according to ``pv_filter`` (case-insensitive ``"all"`` selects all), and + unpivots the selected columns into a long table with columns + ``timestamp``, optional ``grid``, ``PV``, and + ``pv_feedin``. If a grid column is present, its value is divided + equally among the selected PV components. PV feed-in values are negated so + production is positive, and the ``"PV "`` prefix is stripped from labels + (e.g., ``"PV #1"`` → ``"#1"``). + + Args: + energy_report_df: Source table containing PV component columns that start with + ``"PV #"`` and, optionally, ``grid`` and + ``timestamp``. + pv_filter: Iterable of PV identifiers matching the suffix after ``"PV #"`` + (e.g., ``"1"``, ``"2"``). If any value equals ``"all"`` (case-insensitive), + all PV columns are selected. + + Returns: + A long-form DataFrame with columns: + - ``timestamp`` + - (optional) ``grid`` (normalized per PV) + - ``PV`` (component label without the ``"PV "`` prefix) + - ``COLUMN_PV_FEEDIN`` (positive production) + + Returns an empty DataFrame if no PV columns are present or none match the filter. + + Examples: + >>> # Assuming columns: 'timestamp', 'grid', + ... # 'PV #1', 'PV #2' in energy_report_df + >>> out = build_pv_analysis_df(energy_report_df, pv_filter=["1"]) + >>> set(out.columns) >= {"timestamp", "PV", "pv_feedin"} + True + >>> out_all = build_pv_analysis_df(energy_report_df, pv_filter=["all"]) + """ + # Find all PV columns in dataframe + all_pv_cols = [c for c in energy_report_df.columns if c.startswith("PV #")] + if not all_pv_cols: + return pd.DataFrame() + + # Determine which PV columns to use + if any(str(x).lower() == "all" for x in pv_filter): + pv_columns = all_pv_cols + else: + requested = [f"PV {pv}" for pv in pv_filter] + pv_columns = [c for c in requested if c in energy_report_df.columns] + + if not pv_columns: + return pd.DataFrame() + + id_vars = ["timestamp"] + if "grid" in energy_report_df.columns: + id_vars.append("grid") + + df = energy_report_df[id_vars + pv_columns].copy() + df = pd.melt( + df, + id_vars=id_vars, + value_vars=pv_columns, + var_name="PV", + value_name="pv_feedin", + ) + + # Adjust grid connection if present + if "grid" in id_vars: + df["grid"] /= len(pv_columns) + + # Common post-processing + df["pv_feedin"] *= -1 + df["PV"] = df["PV"].str[3:] # strip "PV " + + return df + + +def build_component_analysis( + energy_report_df: pd.DataFrame, + selection_filter: Iterable[str], + component_label: str, + value_col_name: str, +) -> pd.DataFrame: + """ + Create a tidy analysis DataFrame for a single component type. + + Args: + energy_report_df: + DataFrame containing columns named like + `" #1"`, `" #2"`, etc. + Example: `"Battery #1"`, `"CHP #2"`, `"EV #3"`. + selection_filter: + - If it contains `"All"` (case-insensitive), all + `" #"` columns are selected. + - Otherwise, should contain component numbers as strings + starting with `"#"`, e.g., `["#1", "#3"]`. + component_label: + The label prefix used in the column names and the melted + identifier column (e.g., `"Battery"`, `"CHP"`, `"EV"`). + value_col_name: + The output value column name for the melted values + (e.g., `battery`, `chp`, + `ev`). + + Returns: + pd.DataFrame: + Long-form DataFrame with columns: + `timestamp`, `component_label`, `value_col_name`. + """ + prefix = f"{component_label} #" + + # Select columns + if any(str(x).lower() == "all" for x in selection_filter): + comp_columns = [ + col for col in energy_report_df.columns if col.startswith(prefix) + ] + else: + comp_columns = [ + f"{component_label} {x}" + for x in selection_filter + if f"{component_label} {x}" in energy_report_df.columns + ] + + if not comp_columns: + return pd.DataFrame(columns=["timestamp", component_label, value_col_name]) + + id_vars = ["timestamp"] + analyse_df = energy_report_df[id_vars + comp_columns].copy() + + # Melt to long form + analyse_df = pd.melt( + analyse_df, + id_vars=id_vars, + value_vars=comp_columns, + var_name=component_label, + value_name=value_col_name, + ) + + # Keep only the number after " " + analyse_df[component_label] = analyse_df[component_label].str.replace( + f"{component_label} ", "", regex=False + ) + + return analyse_df + + +def build_overview_df( + energy_report_df: pd.DataFrame, component_types: Iterable[str] +) -> pd.DataFrame: + """Return a subset of ``energy_report_df`` with relevant columns. + + The selection includes base columns and optional ones depending + on ``component_types``. + + Args: + energy_report_df: Source DataFrame with energy data. + component_types: Iterable of component types to include + (e.g., {"pv", "battery"}). + + Returns: + A subset of ``energy_report_df`` containing only the selected + base and optional columns. + """ + base_cols = ["timestamp", "net_import", "net_consumption"] + + optional_cols = { + "pv": ["pv_prod", "pv_feedin"], + "battery": ["battery_throughput"], + } + + # Collect columns in order + cols = base_cols[:] + for comp, comp_cols in optional_cols.items(): + if comp in component_types: + cols.extend(comp_cols) + + # Safe selection: avoid KeyError if a column is missing + cols = list(pd.Index(cols).intersection(energy_report_df.columns, sort=False)) + + return energy_report_df[cols] + + +def load_microgrid_configs( + directory: str = "toml_directory/", +) -> Tuple[Dict[str, MicrogridConfig], list[int]]: + """Load all .toml microgrid configuration files from a directory. + + Args: + directory: Path to the directory containing .toml files. + + Returns: + Sorted list of available microgrid IDs. + + Raises: + FileNotFoundError: If no .toml files are found in the directory. + """ + toml_files = [ + os.path.join(directory, f) for f in os.listdir(directory) if f.endswith(".toml") + ] + + if not toml_files: + raise FileNotFoundError(f"No .toml files found in {directory}.") + + configs: Dict[str, "MicrogridConfig"] = {} + for toml_file in toml_files: + configs.update(MicrogridConfig.load_configs(toml_file)) + + available_microgrids = sorted(int(x) for x in configs) + return configs, available_microgrids + + +def aggregate_pv_metrics( # pylint: disable=too-many-locals + energy_report_df: pd.DataFrame, + resolution: timedelta, + grid_consumption_sum: float, + *, + tz_name: str = "Europe/Berlin", +) -> dict[str, float | None | str]: + """Compute photovoltaic (PV) summary metrics. + + Aggregates PV-related energy (kWh), shares, site consumption, and peak grid + import from the given DataFrame at the specified time resolution. + + Args: + energy_report_df: Input data. Expected columns (some optional): + - 'pv_feedin' : PV feed-in to the grid + - 'pv_prod' : PV production + - 'pv_self' : PV self-consumption + - 'pv_in_bat' : PV energy into the battery + - 'net_consumption' : Site consumption supplied by the grid (optional) + - 'net_import' : Grid import power (used for peak) + - 'timestamp' : Timestamp for peak-date labeling (optional) + Missing series are treated as zeros. + resolution: Duration represented by each row. Used to convert power to energy. + grid_consumption_sum: Total grid consumption (kWh) over the same period. + tz_name: Target timezone name for timestamp conversion (default: "Europe/Berlin"). + + Returns: + Dictionary containing the following metrics: + - ``pv_feed_in_sum``: PV feed-in energy (kWh). + - ``pv_production_sum``: Total PV production (kWh). + - ``pv_self_consumption_sum``: Self-consumed PV energy (kWh). + - ``pv_bat_sum``: PV energy into the battery (kWh). + - ``pv_self_consumption_share``: Self-consumption / production (0-1). + - ``pv_total_consumption_share``: Self-consumption / total site consumption (0-1). + - ``net_site_consumption_sum``: Site consumption supplied by the grid (kWh). + - ``peak``: Peak grid import (kW). + - ``peak_date``: Date of peak import in ``DD.MM.YYYY`` or ``None``. + """ + hours_factor = resolution.total_seconds() / 3600.0 + + # Always get columns safely (Series of zeros if missing) + zeros = pd.Series(0, index=energy_report_df.index) + pv_feed_in = energy_report_df.get("pv_feedin", zeros) + pv_production = energy_report_df.get("pv_prod", zeros) + pv_self = energy_report_df.get("pv_self", zeros) + pv_bat = energy_report_df.get("pv_in_bat", zeros) + + # Energy sums in kWh + pv_feed_in_sum = (pv_feed_in * hours_factor).sum() + pv_production_sum = (pv_production * hours_factor).sum() + pv_self_consumption_sum = (pv_self * hours_factor).sum() + pv_bat_sum = (pv_bat * hours_factor).sum() + + # Shares + pv_self_consumption_share = ( + pv_self_consumption_sum / pv_production_sum if pv_production_sum > 0 else 0 + ) + total_consumed = pv_self_consumption_sum + grid_consumption_sum + pv_total_consumption_share = ( + pv_self_consumption_sum / total_consumed if total_consumed > 0 else 0 + ) + + # Always compute site consumption + peak + net_site_consumption_sum = float( + (energy_report_df.get("net_consumption", zeros).sum()) * hours_factor + ) + peak_series = energy_report_df.get("net_import", zeros) + peak = float(peak_series.max()) if not peak_series.empty else 0.0 + + peak_date = None + if "net_import" in energy_report_df.columns and not peak_series.empty: + peak_idx = peak_series.idxmax() + if "timestamp" in energy_report_df.columns: + ts_raw = energy_report_df.loc[peak_idx, "timestamp"] + ts = pd.to_datetime(str(ts_raw), utc=True, errors="coerce") + peak_date = ( + ts.tz_convert(tz_name).strftime("%d.%m.%Y") if not pd.isna(ts) else None + ) + + return { + "pv_feed_in_sum": pv_feed_in_sum, + "pv_production_sum": pv_production_sum, + "pv_self_consumption_sum": pv_self_consumption_sum, + "pv_bat_sum": pv_bat_sum, + "pv_self_consumption_share": pv_self_consumption_share, + "pv_total_consumption_share": pv_total_consumption_share, + "net_site_consumption_sum": net_site_consumption_sum, + "peak": peak, + "peak_date": peak_date, + }