Skip to content

Commit a1fbe82

Browse files
fix: missing production and consumption column
1 parent f7e0f1e commit a1fbe82

File tree

1 file changed

+92
-17
lines changed
  • src/frequenz/lib/notebooks/reporting/utils

1 file changed

+92
-17
lines changed

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

Lines changed: 92 additions & 17 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,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

Comments
 (0)