Skip to content

Commit 9fa4ccc

Browse files
committed
Merge branch 'main' into 41-revenue-sharing-mechanism
2 parents e66d0ad + 7cf816b commit 9fa4ccc

File tree

4 files changed

+116
-52
lines changed

4 files changed

+116
-52
lines changed

docs/index.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,27 @@ package for ca_npa_howtopay
1010
## Overview
1111
The `npa-howtopay` package provides functionality for analyzing energy costs and project economics under different expense scenarios.
1212

13+
## Scenario Definitions
14+
| Scenario Name | Description |
15+
|--------------------|--------------------------------------------------------------------------------------------------|
16+
| bau | Business-as-usual (BAU): No NPA projects, baseline utility costs and spending. |
17+
| taxpayer | All NPA costs are paid by taxpayers, not by utility customers. |
18+
| gas_capex | Gas utility pays for NPA projects as capital expenditures (added to gas ratebase). |
19+
| gas_opex | Gas utility pays for NPA projects as operating expenses (expensed in year incurred). |
20+
| electric_capex | Electric utility pays for NPA projects as capital expenditures (added to electric ratebase). |
21+
| electric_opex | Electric utility pays for NPA projects as operating expenses (expensed in year incurred). |
22+
23+
Each scenario specifies who pays for NPA projects (gas utility, electric utility, or taxpayers) and whether costs are treated as capital (capex) or operating (opex) expenses.
24+
25+
26+
1327
## Core Modules
1428

1529
### Main Package (`npa_howtopay`)
1630
- **`run_model`** - Main function to execute the cost analysis model for a single scenario
17-
- **`analyze_scenarios`** - Execute run_model for all scenarios and return results and delta from BAU (no NPAs) dfs.
31+
- **`run_all_scenarios`** - Execute run_model for all scenarios and return all results
32+
- **`create_delta_df`** - Selects columns of interest from all results and calculates difference from BAU (expect for converter bills which are compared to non-converter bill in each scenario)
33+
- **`return_absolute_values_df`** - Concats dfs from `run_all_scenarios` and filters to selected columns
1834

1935
### Initialize Model
2036
If running locally:
@@ -54,5 +70,10 @@ from npa_howtopay import run_model, load_scenario_from_yaml
5470
scenario_runs = create_scenario_runs(2025, 2050, ["gas", "electric"], ["capex", "opex"])
5571
input_params = load_scenario_from_yaml(run_name)
5672
ts_params = load_time_series_params_from_yaml(run_name)
57-
results_df, delta_bau_df = analyze_scenarios(scenario_runs, input_params, ts_params)
73+
# Run model for all scenarios
74+
results_df_all = run_all_scenarios(scenario_runs, input_params, ts_params)
75+
# Single df with delta values
76+
delta_df = create_delta_df(results_df_all, COMPARE_COLS)
77+
# Single df with absolute values
78+
results_df = return_absolute_values_df(results_df_all, COMPARE_COLS)
5879
```

run_example.py

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
# run_example.py
22

3-
import polars as pl
43

5-
from npa_howtopay.model import analyze_scenarios, create_scenario_runs
4+
from npa_howtopay.model import create_delta_df, create_scenario_runs, return_absolute_values_df, run_all_scenarios
65
from npa_howtopay.params import (
76
COMPARE_COLS,
87
load_scenario_from_yaml,
@@ -25,30 +24,25 @@
2524
scenario_runs = create_scenario_runs(2025, 2050, ["gas", "electric"], ["capex", "opex"])
2625
input_params = load_scenario_from_yaml("sample")
2726
ts_params = load_time_series_params_from_yaml("sample")
28-
results_df, delta_bau_df = analyze_scenarios(scenario_runs, input_params, ts_params)
27+
results_df_all = run_all_scenarios(scenario_runs, input_params, ts_params)
28+
29+
delta_df = create_delta_df(results_df_all, COMPARE_COLS)
30+
results_df = return_absolute_values_df(results_df_all, COMPARE_COLS)
31+
2932
# EDA plots
3033
# For delta values (original behavior)
31-
plt_df_delta = transform_to_long_format(delta_bau_df)
34+
plt_df_delta = transform_to_long_format(delta_df)
3235
plot_revenue_requirements(plt_df_delta, show_absolute=False, save_dir="plots")
3336
plot_volumetric_tariff(plt_df_delta, show_absolute=False, save_dir="plots")
3437
plot_ratebase(plt_df_delta, show_absolute=False, save_dir="plots")
3538
plot_return_on_ratebase_pct(plt_df_delta, show_absolute=False, save_dir="plots")
3639
plot_depreciation_accruals(plt_df_delta, show_absolute=False, save_dir="plots")
3740
plot_user_bills_converts(plt_df_delta, show_absolute=False, save_dir="plots")
3841
plot_user_bills_nonconverts(plt_df_delta, show_absolute=False, save_dir="plots")
39-
# For absolute values - filter results_df to COMPARE_COLS and transform
40-
filtered_results = {}
41-
for scenario_name, scenario_df in results_df.items():
42-
if scenario_name == "bau":
43-
continue # Skip BAU for absolute value plotting
44-
filtered_results[scenario_name] = scenario_df.select(["year", *COMPARE_COLS])
45-
46-
# Concatenate and transform to long format
47-
combined_df = pl.concat(
48-
[df.with_columns(pl.lit(scenario_id).alias("scenario_id")) for scenario_id, df in filtered_results.items()],
49-
how="vertical",
50-
)
51-
plt_df_absolute = transform_to_long_format(combined_df)
42+
plot_total_bills(plt_df_delta, show_absolute=False, save_dir="plots")
43+
plot_return_on_ratebase_pct(plt_df_delta, show_absolute=False, save_dir="plots")
44+
# For absolute values
45+
plt_df_absolute = transform_to_long_format(results_df)
5246
plot_revenue_requirements(plt_df_absolute, show_absolute=True, save_dir="plots")
5347
plot_volumetric_tariff(plt_df_absolute, show_absolute=True, save_dir="plots")
5448
plot_ratebase(plt_df_absolute, show_absolute=True, save_dir="plots")
@@ -57,7 +51,8 @@
5751
plot_user_bills_converts(plt_df_absolute, show_absolute=True, save_dir="plots")
5852
plot_user_bills_nonconverts(plt_df_absolute, show_absolute=True, save_dir="plots")
5953

60-
plot_total_bills(delta_bau_df, save_dir="plots")
54+
plot_total_bills(results_df, show_absolute=True, save_dir="plots")
55+
plot_return_on_ratebase_pct(results_df, show_absolute=True, save_dir="plots")
6156

6257
# Method 2: Using web parameters (scalar values)
6358
web_params = {
@@ -72,10 +67,14 @@
7267
"gas_fixed_overhead_costs": 100.0,
7368
"electric_fixed_overhead_costs": 100.0,
7469
"gas_bau_lpp_costs_per_year": 100.0,
70+
"npa_year_start": 2025,
71+
"npa_year_end": 2030,
7572
"is_scattershot": False,
7673
}
7774

7875
input_params2 = load_scenario_from_yaml("sample") # Still load base params from YAML
7976
ts_params2 = load_time_series_params_from_web_params(web_params, 2025, 2050)
8077

81-
out2 = analyze_scenarios(scenario_runs, input_params2, ts_params2)
78+
out2 = run_all_scenarios(scenario_runs, input_params2, ts_params2)
79+
delta_df2 = create_delta_df(out2, COMPARE_COLS)
80+
results_df2 = return_absolute_values_df(out2, COMPARE_COLS)

src/npa_howtopay/model.py

Lines changed: 63 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -702,58 +702,91 @@ def run_model(scenario_params: ScenarioParams, input_params: InputParams, ts_par
702702
return results_df
703703

704704

705-
def create_delta_bau_df(results_df: dict[str, pl.DataFrame], compare_cols_all: list[str]) -> pl.DataFrame:
706-
bau_df = results_df["bau"].select(["year"] + compare_cols_all)
705+
def create_delta_df(results_dfs: dict[str, pl.DataFrame], compare_cols_all: list[str]) -> pl.DataFrame:
706+
bau_df = results_dfs["bau"].select(["year"] + compare_cols_all)
707707

708708
# Default mapping: most columns compare against themselves in BAU
709-
column_mappings = {col: col for col in compare_cols_all}
709+
column_mappings = {col: (col, "bau") for col in compare_cols_all}
710710

711-
# Override special cases: converts columns compare against nonconverts in BAU
711+
# Override special cases: converts columns compare against nonconverts in same scenario
712712
special_cases = {
713-
"converts_total_bill_per_user": "nonconverts_total_bill_per_user",
714-
"electric_converts_bill_per_user": "electric_nonconverts_bill_per_user",
715-
"gas_converts_bill_per_user": "gas_nonconverts_bill_per_user",
713+
"converts_total_bill_per_user": ("nonconverts_total_bill_per_user", "self"),
714+
"electric_converts_bill_per_user": ("electric_nonconverts_bill_per_user", "self"),
715+
"gas_converts_bill_per_user": ("gas_nonconverts_bill_per_user", "self"),
716716
}
717717
column_mappings.update(special_cases)
718718

719+
# Determine what BAU columns we need
720+
bau_cols_needed = set()
721+
for scenario_col, (baseline_col, baseline_df_name) in column_mappings.items():
722+
if baseline_df_name == "bau" and scenario_col in compare_cols_all:
723+
bau_cols_needed.add(baseline_col)
724+
719725
# Create comparison DataFrames for each scenario
720726
comparison_dfs = {}
721-
for scenario_name, scenario_df in results_df.items():
727+
for scenario_name, scenario_df in results_dfs.items():
722728
if scenario_name == "bau":
723-
continue # Skip BAU itself
724-
725-
# Join with BAU columns
726-
bau_cols_to_join = ["year"] + list(set(column_mappings.values()))
727-
bau_renames = {col: f"bau_{col}" for col in set(column_mappings.values())}
728-
729-
comparison_df = scenario_df.join(
730-
bau_df.select(bau_cols_to_join).rename(bau_renames),
731-
on="year",
732-
)
729+
continue
730+
731+
# Do one join with all needed BAU columns
732+
if bau_cols_needed:
733+
bau_cols_to_join = ["year"] + list(bau_cols_needed)
734+
bau_renames = {col: f"bau_{col}" for col in bau_cols_needed}
735+
working_df = scenario_df.join(
736+
bau_df.select(bau_cols_to_join).rename(bau_renames),
737+
on="year",
738+
)
739+
else:
740+
working_df = scenario_df
733741

734-
# Create comparison columns
742+
# Create all comparison expressions
735743
comparison_cols = []
736-
for scenario_col, bau_col in column_mappings.items():
744+
for scenario_col, (baseline_col, baseline_df_name) in column_mappings.items():
737745
if scenario_col in compare_cols_all:
738-
comparison_cols.append(pl.col(scenario_col).sub(pl.col(f"bau_{bau_col}")))
746+
if baseline_df_name == "self":
747+
if baseline_col in scenario_df.columns:
748+
comparison_cols.append(pl.col(scenario_col).sub(pl.col(baseline_col)))
749+
else: # bau
750+
if baseline_col in bau_df.columns:
751+
comparison_cols.append(pl.col(scenario_col).sub(pl.col(f"bau_{baseline_col}")))
739752

740753
# Select final columns
741-
comparison_df = comparison_df.select(["year", *comparison_cols])
742-
comparison_dfs[scenario_name] = comparison_df
754+
if comparison_cols:
755+
final_df = working_df.select(["year", *comparison_cols])
756+
else:
757+
final_df = working_df.select(["year"])
758+
759+
comparison_dfs[scenario_name] = final_df
743760

744-
delta_bau_df = pl.concat(
761+
delta_df = pl.concat(
745762
[df.with_columns(pl.lit(scenario_id).alias("scenario_id")) for scenario_id, df in comparison_dfs.items()],
746763
how="vertical",
747764
)
748-
return delta_bau_df
765+
return delta_df
749766

750767

751-
def analyze_scenarios(
768+
def run_all_scenarios(
752769
scenario_runs: dict[str, ScenarioParams], input_params: InputParams, ts_params: TimeSeriesParams
753-
) -> tuple[dict[str, pl.DataFrame], pl.DataFrame]:
754-
results_df = {}
770+
) -> dict[str, pl.DataFrame]:
771+
results_dfs = {}
755772
for scenario_name, scenario_params in scenario_runs.items():
756773
logger.info(f"Running scenario: {scenario_name}")
757-
results_df[scenario_name] = run_model(scenario_params, input_params, ts_params)
774+
results_dfs[scenario_name] = run_model(scenario_params, input_params, ts_params)
775+
776+
return results_dfs
777+
778+
779+
def return_absolute_values_df(results_dfs: dict[str, pl.DataFrame], compare_cols_all: list[str]) -> pl.DataFrame:
780+
filtered_results = {}
781+
for scenario_name, scenario_df in results_dfs.items():
782+
filtered_results[scenario_name] = scenario_df.select(["year", *compare_cols_all])
783+
print(f"Added {scenario_name} with shape {scenario_df.shape}")
784+
785+
print(f"filtered_results keys: {filtered_results.keys()}")
786+
# Concatenate and transform to long format
787+
combined_df = pl.concat(
788+
[df.with_columns(pl.lit(scenario_id).alias("scenario_id")) for scenario_id, df in filtered_results.items()],
789+
how="vertical",
790+
)
758791

759-
return results_df, create_delta_bau_df(results_df, COMPARE_COLS)
792+
return combined_df

src/npa_howtopay/web_params.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ class WebParams:
1717
electric_fixed_overhead_costs: float
1818
gas_bau_lpp_costs_per_year: float
1919
is_scattershot: bool
20+
npa_year_start: Optional[int] = None
21+
npa_year_end: Optional[int] = None
22+
2023
# stuff_for_producing_ratebase_baseline
2124

2225

@@ -101,8 +104,16 @@ def create_time_series_from_web_params(
101104
web_params: WebParams, start_year: int, end_year: int, cost_inflation_rate: float = 0.0
102105
) -> dict[str, pl.DataFrame]:
103106
"""Create all time series DataFrames from web parameters"""
107+
npa_year_end = web_params.npa_year_end if web_params.npa_year_end is not None else end_year
108+
npa_year_start = web_params.npa_year_start if web_params.npa_year_start is not None else start_year
109+
110+
if npa_year_start < start_year:
111+
raise ValueError("npa_year_start must be greater than or equal to SharedParams.start_year")
112+
if npa_year_end > end_year:
113+
raise ValueError("npa_year_end must be less than or equal to npa_end_year")
114+
104115
return {
105-
"npa_projects": create_npa_projects(web_params, start_year, end_year),
116+
"npa_projects": create_npa_projects(web_params, npa_year_start, npa_year_end),
106117
"scattershot_electrification_users_per_year": create_scattershot_electrification_df(
107118
web_params, start_year, end_year
108119
),

0 commit comments

Comments
 (0)