2323import pandas as pd
2424
2525from frequenz .lib .notebooks .reporting .metrics .reporting_metrics import (
26+ asset_production ,
27+ consumption ,
2628 grid_feed_in ,
2729 production_excess ,
2830 production_excess_in_bat ,
3133)
3234
3335
34- def _get_numeric_series (df : pd .DataFrame , col : str | None ) -> pd .Series :
36+ def _get_numeric_series (
37+ df : pd .DataFrame , col : str | None , * , clip_non_negative : bool = False
38+ ) -> pd .Series :
3539 """Safely extract a numeric Series or return zeros if missing.
3640
3741 Ensures consistent numeric handling even when the requested column
@@ -41,16 +45,25 @@ def _get_numeric_series(df: pd.DataFrame, col: str | None) -> pd.Series:
4145 Args:
4246 df: Input DataFrame from which to extract the column.
4347 col: Column name to retrieve. If None or missing, zeros are returned.
48+ clip_non_negative: Clip the resulting series at zero (lower bound).
4449
4550 Returns:
46- A float64 Series with non-negative values, matching the input index.
51+ A float64 Series aligned to the input index, optionally clipped to be
52+ non-negative.
4753 """
4854 if col is None :
49- return pd .Series (0.0 , index = df .index , dtype = "float64" )
50- return df .reindex (columns = [col ], fill_value = 0 )[col ].astype ("float64" ).clip (lower = 0 )
55+ series = pd .Series (0.0 , index = df .index , dtype = "float64" )
56+ else :
57+ series = df .reindex (columns = [col ], fill_value = 0 )[col ].astype ("float64" )
58+
59+ if clip_non_negative :
60+ series = series .clip (lower = 0 )
61+ return series
5162
5263
53- def _sum_cols (df : pd .DataFrame , cols : list [str ] | None ) -> pd .Series :
64+ def _sum_cols (
65+ df : pd .DataFrame , cols : list [str ] | None , * , clip_non_negative : bool = False
66+ ) -> pd .Series :
5467 """Safely sum multiple numeric columns into a single Series.
5568
5669 Ensures robust aggregation even when some columns are missing or None.
@@ -59,16 +72,20 @@ def _sum_cols(df: pd.DataFrame, cols: list[str] | None) -> pd.Series:
5972 Args:
6073 df: Input DataFrame containing the columns to be summed.
6174 cols: list of column names to sum. If empty, returns a zero-filled Series.
75+ clip_non_negative: Clip individual series at zero before summing.
6276
6377 Returns:
6478 A float64 Series representing the elementwise sum of all specified columns.
65- Missing or invalid columns are treated as zeros.
79+ Missing or invalid columns are treated as zeros. The result is
80+ non-negative when ``clip_non_negative`` is True.
6681 """
6782 if not cols :
6883 return pd .Series (0.0 , index = df .index , dtype = "float64" )
6984
7085 # Safely extract each column as a numeric, non-negative Series, then sum row-wise
71- series_list = [_get_numeric_series (df , c ) for c in cols ]
86+ series_list = [
87+ _get_numeric_series (df , c , clip_non_negative = clip_non_negative ) for c in cols
88+ ]
7289 return pd .concat (series_list , axis = 1 ).sum (axis = 1 ).astype ("float64" )
7390
7491
@@ -77,6 +94,7 @@ def add_energy_flows(
7794 df : pd .DataFrame ,
7895 production_cols : list [str ] | None = None ,
7996 consumption_cols : list [str ] | None = None ,
97+ grid_cols : list [str ] | None = None ,
8098 battery_charge_col : str | None = None ,
8199 production_is_positive : bool = False ,
82100) -> pd .DataFrame :
@@ -91,6 +109,7 @@ def add_energy_flows(
91109 battery charge data.
92110 production_cols: list of column names representing production sources.
93111 consumption_cols: list of column names representing consumption sources.
112+ grid_cols: list of column names representing grid import/export.
94113 battery_charge_col: optional column name for battery charging power. If None,
95114 battery-related flows are set to zero.
96115 production_is_positive: Whether production values are already positive.
@@ -106,15 +125,71 @@ def add_energy_flows(
106125 """
107126 df_flows = df .copy ()
108127
109- # Total production and consumption (returns pandas series with 0.0 for missing cols)
110- df_flows ["production_total" ] = _sum_cols (df_flows , production_cols )
111- df_flows ["consumption_total" ] = _sum_cols (df_flows , consumption_cols )
112-
113- # Surplus vs. consumption
128+ # Normalize production, grid and consumption columns by removing None entries
129+ resolved_production_cols = [
130+ col for col in (production_cols or []) if col is not None
131+ ]
132+ resolved_consumption_cols = [
133+ col for col in (consumption_cols or []) if col is not None
134+ ]
135+ resolved_grid_cols = [col for col in (grid_cols or []) if col is not None ]
136+
137+ # Compute total asset production
138+ asset_production_cols : list [str ] = []
139+ for col in resolved_production_cols :
140+ series = _get_numeric_series (
141+ df_flows ,
142+ col ,
143+ )
144+ if col not in df_flows :
145+ df_flows [col ] = series
146+ asset_series = asset_production (
147+ series ,
148+ production_is_positive = production_is_positive ,
149+ )
150+ asset_col_name = f"{ col } _asset_production"
151+ df_flows [asset_col_name ] = asset_series
152+ asset_production_cols .append (asset_col_name )
153+
154+ df_flows ["production_total" ] = _sum_cols (df_flows , asset_production_cols )
155+
156+ # Compute total consumption
157+ if not resolved_consumption_cols :
158+ # Use existing 'consumption' column if present
159+ if "consumption" in df_flows .columns :
160+ resolved_consumption_cols = ["consumption" ]
161+
162+ # Infer from grid/production if grid columns exist
163+ elif resolved_grid_cols :
164+ for col in resolved_grid_cols :
165+ if col not in df_flows :
166+ df_flows [col ] = _get_numeric_series (
167+ df_flows ,
168+ col ,
169+ clip_non_negative = True ,
170+ )
171+ inferred_consumption = consumption (
172+ df_flows , resolved_production_cols , resolved_grid_cols
173+ ).astype ("float64" )
174+ inferred_name = inferred_consumption .name or "consumption"
175+ df_flows [inferred_name ] = inferred_consumption
176+ resolved_consumption_cols = [inferred_name ]
177+
178+ # Fallback — create a zero-filled consumption column
179+ else :
180+ inferred_consumption = pd .Series (
181+ 0.0 , index = df_flows .index , dtype = "float64" , name = "consumption"
182+ )
183+ df_flows [inferred_consumption .name ] = inferred_consumption
184+ resolved_consumption_cols = [inferred_consumption .name ]
185+
186+ df_flows ["consumption_total" ] = _sum_cols (df_flows , resolved_consumption_cols )
187+
188+ # Surplus vs. consumption (production is already positive because of the above cleaning)
114189 df_flows ["production_excess" ] = production_excess (
115190 df_flows ["production_total" ],
116191 df_flows ["consumption_total" ],
117- production_is_positive = production_is_positive ,
192+ production_is_positive = True ,
118193 )
119194
120195 # Battery charging power (optional)
@@ -123,15 +198,15 @@ def add_energy_flows(
123198 df_flows ["production_total" ],
124199 df_flows ["consumption_total" ],
125200 bat_in ,
126- production_is_positive = production_is_positive ,
201+ production_is_positive = True ,
127202 )
128203
129204 # Split excess into battery vs. grid
130205 df_flows ["grid_feed_in" ] = grid_feed_in (
131206 df_flows ["production_total" ],
132207 df_flows ["consumption_total" ],
133208 bat_in ,
134- production_is_positive = production_is_positive ,
209+ production_is_positive = True ,
135210 )
136211
137212 # If no production columns exist, set self-consumption metrics to zero
@@ -141,12 +216,12 @@ def add_energy_flows(
141216 df_flows ["production_self_use" ] = production_self_consumption (
142217 df_flows ["production_total" ],
143218 df_flows ["consumption_total" ],
144- production_is_positive = production_is_positive ,
219+ production_is_positive = True ,
145220 )
146221 df_flows ["production_self_share" ] = production_self_share (
147222 df_flows ["production_total" ],
148223 df_flows ["consumption_total" ],
149- production_is_positive = production_is_positive ,
224+ production_is_positive = True ,
150225 )
151226 else :
152227 df_flows ["production_self_use" ] = 0.0
0 commit comments