2323import pandas as pd
2424
2525from frequenz .lib .notebooks .reporting .metrics .reporting_metrics import (
26+ asset_production ,
2627 grid_feed_in ,
2728 production_excess ,
2829 production_excess_in_bat ,
@@ -43,11 +44,14 @@ def _get_numeric_series(df: pd.DataFrame, col: str | None) -> pd.Series:
4344 col: Column name to retrieve. If None or missing, zeros are returned.
4445
4546 Returns:
46- A float64 Series with non-negative values, matching the input index.
47+ A float64 Series aligned to the input index.
4748 """
4849 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 )
50+ series = pd .Series (0.0 , index = df .index , dtype = "float64" )
51+ else :
52+ raw = df .reindex (columns = [col ], fill_value = 0 )[col ]
53+ series = pd .to_numeric (raw , errors = "coerce" ).fillna (0.0 ).astype ("float64" )
54+ return series
5155
5256
5357def _sum_cols (df : pd .DataFrame , cols : list [str ] | None ) -> pd .Series :
@@ -67,32 +71,45 @@ def _sum_cols(df: pd.DataFrame, cols: list[str] | None) -> pd.Series:
6771 if not cols :
6872 return pd .Series (0.0 , index = df .index , dtype = "float64" )
6973
70- # Safely extract each column as a numeric, non-negative Series, then sum row-wise
74+ # Safely extract each column as a numeric Series then sum row-wise
7175 series_list = [_get_numeric_series (df , c ) for c in cols ]
7276 return pd .concat (series_list , axis = 1 ).sum (axis = 1 ).astype ("float64" )
7377
7478
75- # pylint: disable=too-many-arguments, too-many-locals
79+ def _column_has_data (df : pd .DataFrame , col : str | None ) -> bool :
80+ """Return True when the column exists and has at least one non-zero value."""
81+ if col is None or col not in df .columns :
82+ return False
83+
84+ series = pd .to_numeric (df [col ], errors = "coerce" ).fillna (0.0 ).astype ("float64" )
85+ if series .empty or not series .notna ().any ():
86+ return False
87+
88+ return not series .fillna (0 ).eq (0 ).all ()
89+
90+
91+ # pylint: disable=too-many-arguments, too-many-locals, too-many-positional-arguments
7692def add_energy_flows (
7793 df : pd .DataFrame ,
7894 production_cols : list [str ] | None = None ,
7995 consumption_cols : list [str ] | None = None ,
80- battery_charge_col : str | None = None ,
96+ battery_cols : list [ str ] | None = None ,
8197 production_is_positive : bool = False ,
8298) -> pd .DataFrame :
8399 """Compute and add derived energy flow metrics to the DataFrame.
84100
85101 This function aggregates production and consumption data, derives energy flow
86102 relationships such as grid feed-in, battery charging, and self-consumption,
87- and appends these computed columns to the given DataFrame.
103+ and appends these computed columns to the given DataFrame. Columns that are
104+ specified but missing or contain only null/zero values are ignored.
88105
89106 Args:
90107 df: Input DataFrame containing production, consumption, and optionally
91- battery charge data.
108+ battery power data.
92109 production_cols: list of column names representing production sources.
93110 consumption_cols: list of column names representing consumption sources.
94- battery_charge_col : optional column name for battery charging power. If None,
95- battery-related flows are set to zero .
111+ battery_cols : optional column names representing signed battery power.
112+ Positive values indicate charging, negative values indicate discharging .
96113 production_is_positive: Whether production values are already positive.
97114 If False, `production` is inverted before clipping.
98115
@@ -106,47 +123,83 @@ def add_energy_flows(
106123 """
107124 df_flows = df .copy ()
108125
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 )
126+ # Normalize production, consumption and battery columns by removing None entries
127+ resolved_production_cols = [
128+ col for col in (production_cols or []) if _column_has_data (df_flows , col )
129+ ]
130+ resolved_consumption_cols = [
131+ col for col in (consumption_cols or []) if _column_has_data (df_flows , col )
132+ ]
133+ resolved_battery_cols = [
134+ col for col in (battery_cols or []) if _column_has_data (df_flows , col )
135+ ]
136+
137+ battery_power_series = _sum_cols (df_flows , resolved_battery_cols )
138+ battery_charge_series = (
139+ battery_power_series .reindex (df_flows .index ).fillna (0.0 ).clip (lower = 0.0 )
140+ )
141+
142+ # Compute total asset production
143+ asset_production_cols : list [str ] = []
144+ for col in resolved_production_cols :
145+ series = _get_numeric_series (
146+ df_flows ,
147+ col ,
148+ )
149+ asset_series = asset_production (
150+ series ,
151+ production_is_positive = production_is_positive ,
152+ )
153+ asset_col_name = f"{ col } _asset_production"
154+ df_flows [asset_col_name ] = asset_series
155+ asset_production_cols .append (asset_col_name )
156+
157+ df_flows ["production_total" ] = _sum_cols (df_flows , asset_production_cols )
112158
113- # Surplus vs. consumption
159+ # Compute total consumption
160+ consumption_series_cols : list [str ] = []
161+ for col in resolved_consumption_cols :
162+ df_flows [col ] = _get_numeric_series (df_flows , col )
163+ consumption_series_cols .append (col )
164+
165+ df_flows ["consumption_total" ] = _sum_cols (df_flows , consumption_series_cols )
166+
167+ # Surplus vs. consumption (production is already positive because of the above cleaning)
114168 df_flows ["production_excess" ] = production_excess (
115169 df_flows ["production_total" ],
116170 df_flows ["consumption_total" ],
117- production_is_positive = production_is_positive ,
171+ production_is_positive = True ,
118172 )
119173
120174 # Battery charging power (optional)
121- bat_in = _get_numeric_series (df_flows , battery_charge_col )
122175 df_flows ["production_excess_in_bat" ] = production_excess_in_bat (
123176 df_flows ["production_total" ],
124177 df_flows ["consumption_total" ],
125- bat_in ,
126- production_is_positive = production_is_positive ,
178+ battery = battery_charge_series ,
179+ production_is_positive = True ,
127180 )
128181
129182 # Split excess into battery vs. grid
130183 df_flows ["grid_feed_in" ] = grid_feed_in (
131184 df_flows ["production_total" ],
132185 df_flows ["consumption_total" ],
133- bat_in ,
134- production_is_positive = production_is_positive ,
186+ battery = battery_charge_series ,
187+ production_is_positive = True ,
135188 )
136189
137190 # If no production columns exist, set self-consumption metrics to zero
138- if production_cols :
191+ if asset_production_cols :
139192 # Use total production for self-consumption instead of asset_production
140193 # (which may not exist)
141194 df_flows ["production_self_use" ] = production_self_consumption (
142195 df_flows ["production_total" ],
143196 df_flows ["consumption_total" ],
144- production_is_positive = production_is_positive ,
197+ production_is_positive = True ,
145198 )
146199 df_flows ["production_self_share" ] = production_self_share (
147200 df_flows ["production_total" ],
148201 df_flows ["consumption_total" ],
149- production_is_positive = production_is_positive ,
202+ production_is_positive = True ,
150203 )
151204 else :
152205 df_flows ["production_self_use" ] = 0.0
0 commit comments