Skip to content

Commit 4d4bdb9

Browse files
fix: missing production and consumption column
Signed-off-by: Mohammad Tayyab <[email protected]>
1 parent f7e0f1e commit 4d4bdb9

File tree

1 file changed

+95
-19
lines changed
  • src/frequenz/lib/notebooks/reporting/utils

1 file changed

+95
-19
lines changed

src/frequenz/lib/notebooks/reporting/utils/helpers.py

Lines changed: 95 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import pandas as pd
2424

2525
from 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,
@@ -31,7 +33,9 @@
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,24 +72,30 @@ 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

70-
# 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]
85+
# Safely extract each column as a numeric Series
86+
# (clipped to non-negative if clip_non_negative=True), then sum row-wise
87+
series_list = [
88+
_get_numeric_series(df, c, clip_non_negative=clip_non_negative) for c in cols
89+
]
7290
return pd.concat(series_list, axis=1).sum(axis=1).astype("float64")
7391

7492

75-
# pylint: disable=too-many-arguments, too-many-locals
93+
# pylint: disable=too-many-arguments, too-many-locals, too-many-positional-arguments
7694
def add_energy_flows(
7795
df: pd.DataFrame,
7896
production_cols: list[str] | None = None,
7997
consumption_cols: list[str] | None = None,
98+
grid_cols: list[str] | None = None,
8099
battery_charge_col: str | None = None,
81100
production_is_positive: bool = False,
82101
) -> pd.DataFrame:
@@ -91,6 +110,7 @@ def add_energy_flows(
91110
battery charge data.
92111
production_cols: list of column names representing production sources.
93112
consumption_cols: list of column names representing consumption sources.
113+
grid_cols: list of column names representing grid import/export.
94114
battery_charge_col: optional column name for battery charging power. If None,
95115
battery-related flows are set to zero.
96116
production_is_positive: Whether production values are already positive.
@@ -106,15 +126,71 @@ def add_energy_flows(
106126
"""
107127
df_flows = df.copy()
108128

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
129+
# Normalize production, grid and consumption columns by removing None entries
130+
resolved_production_cols = [
131+
col for col in (production_cols or []) if col is not None
132+
]
133+
resolved_consumption_cols = [
134+
col for col in (consumption_cols or []) if col is not None
135+
]
136+
resolved_grid_cols = [col for col in (grid_cols or []) if col is not None]
137+
138+
# Compute total asset production
139+
asset_production_cols: list[str] = []
140+
for col in resolved_production_cols:
141+
series = _get_numeric_series(
142+
df_flows,
143+
col,
144+
)
145+
if col not in df_flows:
146+
df_flows[col] = series
147+
asset_series = asset_production(
148+
series,
149+
production_is_positive=production_is_positive,
150+
)
151+
asset_col_name = f"{col}_asset_production"
152+
df_flows[asset_col_name] = asset_series
153+
asset_production_cols.append(asset_col_name)
154+
155+
df_flows["production_total"] = _sum_cols(df_flows, asset_production_cols)
156+
157+
# Compute total consumption
158+
if not resolved_consumption_cols:
159+
# Use existing 'consumption' column if present
160+
if "consumption" in df_flows.columns:
161+
resolved_consumption_cols = ["consumption"]
162+
163+
# Infer from grid/production if grid columns exist
164+
elif resolved_grid_cols:
165+
for col in resolved_grid_cols:
166+
if col not in df_flows:
167+
df_flows[col] = _get_numeric_series(
168+
df_flows,
169+
col,
170+
clip_non_negative=True,
171+
)
172+
inferred_consumption = consumption(
173+
df_flows, asset_production_cols, resolved_grid_cols
174+
).astype("float64")
175+
inferred_name = str(inferred_consumption.name or "consumption")
176+
df_flows[inferred_name] = inferred_consumption
177+
resolved_consumption_cols = [inferred_name]
178+
179+
# Fallback — create a zero-filled consumption column
180+
else:
181+
inferred_consumption = pd.Series(
182+
0.0, index=df_flows.index, dtype="float64", name="consumption"
183+
)
184+
df_flows[inferred_consumption.name] = inferred_consumption
185+
resolved_consumption_cols = [str(inferred_consumption.name)]
186+
187+
df_flows["consumption_total"] = _sum_cols(df_flows, resolved_consumption_cols)
188+
189+
# Surplus vs. consumption (production is already positive because of the above cleaning)
114190
df_flows["production_excess"] = production_excess(
115191
df_flows["production_total"],
116192
df_flows["consumption_total"],
117-
production_is_positive=production_is_positive,
193+
production_is_positive=True,
118194
)
119195

120196
# Battery charging power (optional)
@@ -123,15 +199,15 @@ def add_energy_flows(
123199
df_flows["production_total"],
124200
df_flows["consumption_total"],
125201
bat_in,
126-
production_is_positive=production_is_positive,
202+
production_is_positive=True,
127203
)
128204

129205
# Split excess into battery vs. grid
130206
df_flows["grid_feed_in"] = grid_feed_in(
131207
df_flows["production_total"],
132208
df_flows["consumption_total"],
133209
bat_in,
134-
production_is_positive=production_is_positive,
210+
production_is_positive=True,
135211
)
136212

137213
# If no production columns exist, set self-consumption metrics to zero
@@ -141,12 +217,12 @@ def add_energy_flows(
141217
df_flows["production_self_use"] = production_self_consumption(
142218
df_flows["production_total"],
143219
df_flows["consumption_total"],
144-
production_is_positive=production_is_positive,
220+
production_is_positive=True,
145221
)
146222
df_flows["production_self_share"] = production_self_share(
147223
df_flows["production_total"],
148224
df_flows["consumption_total"],
149-
production_is_positive=production_is_positive,
225+
production_is_positive=True,
150226
)
151227
else:
152228
df_flows["production_self_use"] = 0.0

0 commit comments

Comments
 (0)