diff --git a/docs/index.md b/docs/index.md index 64280e7..4de88e0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,11 +10,27 @@ package for ca_npa_howtopay ## Overview The `npa-howtopay` package provides functionality for analyzing energy costs and project economics under different expense scenarios. +## Scenario Definitions +| Scenario Name | Description | +|--------------------|--------------------------------------------------------------------------------------------------| +| bau | Business-as-usual (BAU): No NPA projects, baseline utility costs and spending. | +| taxpayer | All NPA costs are paid by taxpayers, not by utility customers. | +| gas_capex | Gas utility pays for NPA projects as capital expenditures (added to gas ratebase). | +| gas_opex | Gas utility pays for NPA projects as operating expenses (expensed in year incurred). | +| electric_capex | Electric utility pays for NPA projects as capital expenditures (added to electric ratebase). | +| electric_opex | Electric utility pays for NPA projects as operating expenses (expensed in year incurred). | + +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. + + + ## Core Modules ### Main Package (`npa_howtopay`) - **`run_model`** - Main function to execute the cost analysis model for a single scenario -- **`analyze_scenarios`** - Execute run_model for all scenarios and return results and delta from BAU (no NPAs) dfs. +- **`run_all_scenarios`** - Execute run_model for all scenarios and return all results +- **`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) +- **`return_absolute_values_df`** - Concats dfs from `run_all_scenarios` and filters to selected columns ### Initialize Model If running locally: @@ -54,5 +70,10 @@ from npa_howtopay import run_model, load_scenario_from_yaml scenario_runs = create_scenario_runs(2025, 2050, ["gas", "electric"], ["capex", "opex"]) input_params = load_scenario_from_yaml(run_name) ts_params = load_time_series_params_from_yaml(run_name) - results_df, delta_bau_df = analyze_scenarios(scenario_runs, input_params, ts_params) + # Run model for all scenarios + results_df_all = run_all_scenarios(scenario_runs, input_params, ts_params) + # Single df with delta values + delta_df = create_delta_df(results_df_all, COMPARE_COLS) + # Single df with absolute values + results_df = return_absolute_values_df(results_df_all, COMPARE_COLS) ``` diff --git a/plots/depreciation_accruals_absolute.png b/plots/depreciation_accruals_absolute.png index affbb44..1cd794e 100644 Binary files a/plots/depreciation_accruals_absolute.png and b/plots/depreciation_accruals_absolute.png differ diff --git a/plots/depreciation_accruals_delta.png b/plots/depreciation_accruals_delta.png index d16013a..3c9930c 100644 Binary files a/plots/depreciation_accruals_delta.png and b/plots/depreciation_accruals_delta.png differ diff --git a/plots/ratebase_absolute.png b/plots/ratebase_absolute.png index 908c470..1e8bc5a 100644 Binary files a/plots/ratebase_absolute.png and b/plots/ratebase_absolute.png differ diff --git a/plots/ratebase_delta.png b/plots/ratebase_delta.png index 0a5e2eb..a6e2815 100644 Binary files a/plots/ratebase_delta.png and b/plots/ratebase_delta.png differ diff --git a/plots/return_on_ratebase_as_%_of_revenue_requirement_delta.png b/plots/return_on_ratebase_as_%_of_revenue_requirement_delta.png index dbaaff2..d78a141 100644 Binary files a/plots/return_on_ratebase_as_%_of_revenue_requirement_delta.png and b/plots/return_on_ratebase_as_%_of_revenue_requirement_delta.png differ diff --git a/plots/total_user_bills_delta.png b/plots/total_user_bills_delta.png index ac883d3..9056568 100644 Binary files a/plots/total_user_bills_delta.png and b/plots/total_user_bills_delta.png differ diff --git a/plots/user_bills_and_converts_absolute.png b/plots/user_bills_and_converts_absolute.png index 5c162ea..48bc00f 100644 Binary files a/plots/user_bills_and_converts_absolute.png and b/plots/user_bills_and_converts_absolute.png differ diff --git a/plots/user_bills_and_converts_delta.png b/plots/user_bills_and_converts_delta.png index 8f34f0c..a9e99f5 100644 Binary files a/plots/user_bills_and_converts_delta.png and b/plots/user_bills_and_converts_delta.png differ diff --git a/plots/user_bills_and_nonconverts_absolute.png b/plots/user_bills_and_nonconverts_absolute.png index 786c8fa..5a56220 100644 Binary files a/plots/user_bills_and_nonconverts_absolute.png and b/plots/user_bills_and_nonconverts_absolute.png differ diff --git a/plots/user_bills_and_nonconverts_delta.png b/plots/user_bills_and_nonconverts_delta.png index f73476d..38a45ea 100644 Binary files a/plots/user_bills_and_nonconverts_delta.png and b/plots/user_bills_and_nonconverts_delta.png differ diff --git a/plots/utility_revenue_requirements_absolute.png b/plots/utility_revenue_requirements_absolute.png index 756cb52..6e5f450 100644 Binary files a/plots/utility_revenue_requirements_absolute.png and b/plots/utility_revenue_requirements_absolute.png differ diff --git a/plots/utility_revenue_requirements_delta.png b/plots/utility_revenue_requirements_delta.png index f2d6ff6..55cfe3c 100644 Binary files a/plots/utility_revenue_requirements_delta.png and b/plots/utility_revenue_requirements_delta.png differ diff --git a/plots/volumetric_tariff_absolute.png b/plots/volumetric_tariff_absolute.png index a83a132..da2333c 100644 Binary files a/plots/volumetric_tariff_absolute.png and b/plots/volumetric_tariff_absolute.png differ diff --git a/plots/volumetric_tariff_delta.png b/plots/volumetric_tariff_delta.png index a8063ee..79b0aaf 100644 Binary files a/plots/volumetric_tariff_delta.png and b/plots/volumetric_tariff_delta.png differ diff --git a/run_example.py b/run_example.py index be8f7e1..9fc6cd8 100644 --- a/run_example.py +++ b/run_example.py @@ -1,8 +1,7 @@ # run_example.py -import polars as pl -from npa_howtopay.model import analyze_scenarios, create_scenario_runs +from npa_howtopay.model import create_delta_df, create_scenario_runs, return_absolute_values_df, run_all_scenarios from npa_howtopay.params import ( COMPARE_COLS, load_scenario_from_yaml, @@ -25,10 +24,14 @@ scenario_runs = create_scenario_runs(2025, 2050, ["gas", "electric"], ["capex", "opex"]) input_params = load_scenario_from_yaml("sample") ts_params = load_time_series_params_from_yaml("sample") - results_df, delta_bau_df = analyze_scenarios(scenario_runs, input_params, ts_params) + results_df_all = run_all_scenarios(scenario_runs, input_params, ts_params) + + delta_df = create_delta_df(results_df_all, COMPARE_COLS) + results_df = return_absolute_values_df(results_df_all, COMPARE_COLS) + # EDA plots # For delta values (original behavior) - plt_df_delta = transform_to_long_format(delta_bau_df) + plt_df_delta = transform_to_long_format(delta_df) plot_revenue_requirements(plt_df_delta, show_absolute=False, save_dir="plots") plot_volumetric_tariff(plt_df_delta, show_absolute=False, save_dir="plots") plot_ratebase(plt_df_delta, show_absolute=False, save_dir="plots") @@ -36,19 +39,10 @@ plot_depreciation_accruals(plt_df_delta, show_absolute=False, save_dir="plots") plot_user_bills_converts(plt_df_delta, show_absolute=False, save_dir="plots") plot_user_bills_nonconverts(plt_df_delta, show_absolute=False, save_dir="plots") - # For absolute values - filter results_df to COMPARE_COLS and transform - filtered_results = {} - for scenario_name, scenario_df in results_df.items(): - if scenario_name == "bau": - continue # Skip BAU for absolute value plotting - filtered_results[scenario_name] = scenario_df.select(["year", *COMPARE_COLS]) - - # Concatenate and transform to long format - combined_df = pl.concat( - [df.with_columns(pl.lit(scenario_id).alias("scenario_id")) for scenario_id, df in filtered_results.items()], - how="vertical", - ) - plt_df_absolute = transform_to_long_format(combined_df) + plot_total_bills(plt_df_delta, show_absolute=False, save_dir="plots") + plot_return_on_ratebase_pct(plt_df_delta, show_absolute=False, save_dir="plots") + # For absolute values + plt_df_absolute = transform_to_long_format(results_df) plot_revenue_requirements(plt_df_absolute, show_absolute=True, save_dir="plots") plot_volumetric_tariff(plt_df_absolute, show_absolute=True, save_dir="plots") plot_ratebase(plt_df_absolute, show_absolute=True, save_dir="plots") @@ -57,7 +51,8 @@ plot_user_bills_converts(plt_df_absolute, show_absolute=True, save_dir="plots") plot_user_bills_nonconverts(plt_df_absolute, show_absolute=True, save_dir="plots") - plot_total_bills(delta_bau_df, save_dir="plots") + plot_total_bills(results_df, show_absolute=True, save_dir="plots") + plot_return_on_ratebase_pct(results_df, show_absolute=True, save_dir="plots") # Method 2: Using web parameters (scalar values) web_params = { @@ -78,4 +73,6 @@ input_params2 = load_scenario_from_yaml("sample") # Still load base params from YAML ts_params2 = load_time_series_params_from_web_params(web_params, 2025, 2050) -out2 = analyze_scenarios(scenario_runs, input_params2, ts_params2) +out2 = run_all_scenarios(scenario_runs, input_params2, ts_params2) +delta_df2 = create_delta_df(out2, COMPARE_COLS) +results_df2 = return_absolute_values_df(out2, COMPARE_COLS) diff --git a/src/npa_howtopay/model.py b/src/npa_howtopay/model.py index f218716..119ec7f 100644 --- a/src/npa_howtopay/model.py +++ b/src/npa_howtopay/model.py @@ -667,58 +667,91 @@ def run_model(scenario_params: ScenarioParams, input_params: InputParams, ts_par return results_df -def create_delta_bau_df(results_df: dict[str, pl.DataFrame], compare_cols_all: list[str]) -> pl.DataFrame: - bau_df = results_df["bau"].select(["year"] + compare_cols_all) +def create_delta_df(results_dfs: dict[str, pl.DataFrame], compare_cols_all: list[str]) -> pl.DataFrame: + bau_df = results_dfs["bau"].select(["year"] + compare_cols_all) # Default mapping: most columns compare against themselves in BAU - column_mappings = {col: col for col in compare_cols_all} + column_mappings = {col: (col, "bau") for col in compare_cols_all} - # Override special cases: converts columns compare against nonconverts in BAU + # Override special cases: converts columns compare against nonconverts in same scenario special_cases = { - "converts_total_bill_per_user": "nonconverts_total_bill_per_user", - "electric_converts_bill_per_user": "electric_nonconverts_bill_per_user", - "gas_converts_bill_per_user": "gas_nonconverts_bill_per_user", + "converts_total_bill_per_user": ("nonconverts_total_bill_per_user", "self"), + "electric_converts_bill_per_user": ("electric_nonconverts_bill_per_user", "self"), + "gas_converts_bill_per_user": ("gas_nonconverts_bill_per_user", "self"), } column_mappings.update(special_cases) + # Determine what BAU columns we need + bau_cols_needed = set() + for scenario_col, (baseline_col, baseline_df_name) in column_mappings.items(): + if baseline_df_name == "bau" and scenario_col in compare_cols_all: + bau_cols_needed.add(baseline_col) + # Create comparison DataFrames for each scenario comparison_dfs = {} - for scenario_name, scenario_df in results_df.items(): + for scenario_name, scenario_df in results_dfs.items(): if scenario_name == "bau": - continue # Skip BAU itself - - # Join with BAU columns - bau_cols_to_join = ["year"] + list(set(column_mappings.values())) - bau_renames = {col: f"bau_{col}" for col in set(column_mappings.values())} - - comparison_df = scenario_df.join( - bau_df.select(bau_cols_to_join).rename(bau_renames), - on="year", - ) + continue + + # Do one join with all needed BAU columns + if bau_cols_needed: + bau_cols_to_join = ["year"] + list(bau_cols_needed) + bau_renames = {col: f"bau_{col}" for col in bau_cols_needed} + working_df = scenario_df.join( + bau_df.select(bau_cols_to_join).rename(bau_renames), + on="year", + ) + else: + working_df = scenario_df - # Create comparison columns + # Create all comparison expressions comparison_cols = [] - for scenario_col, bau_col in column_mappings.items(): + for scenario_col, (baseline_col, baseline_df_name) in column_mappings.items(): if scenario_col in compare_cols_all: - comparison_cols.append(pl.col(scenario_col).sub(pl.col(f"bau_{bau_col}"))) + if baseline_df_name == "self": + if baseline_col in scenario_df.columns: + comparison_cols.append(pl.col(scenario_col).sub(pl.col(baseline_col))) + else: # bau + if baseline_col in bau_df.columns: + comparison_cols.append(pl.col(scenario_col).sub(pl.col(f"bau_{baseline_col}"))) # Select final columns - comparison_df = comparison_df.select(["year", *comparison_cols]) - comparison_dfs[scenario_name] = comparison_df + if comparison_cols: + final_df = working_df.select(["year", *comparison_cols]) + else: + final_df = working_df.select(["year"]) + + comparison_dfs[scenario_name] = final_df - delta_bau_df = pl.concat( + delta_df = pl.concat( [df.with_columns(pl.lit(scenario_id).alias("scenario_id")) for scenario_id, df in comparison_dfs.items()], how="vertical", ) - return delta_bau_df + return delta_df -def analyze_scenarios( +def run_all_scenarios( scenario_runs: dict[str, ScenarioParams], input_params: InputParams, ts_params: TimeSeriesParams -) -> tuple[dict[str, pl.DataFrame], pl.DataFrame]: - results_df = {} +) -> dict[str, pl.DataFrame]: + results_dfs = {} for scenario_name, scenario_params in scenario_runs.items(): logger.info(f"Running scenario: {scenario_name}") - results_df[scenario_name] = run_model(scenario_params, input_params, ts_params) + results_dfs[scenario_name] = run_model(scenario_params, input_params, ts_params) + + return results_dfs + + +def return_absolute_values_df(results_dfs: dict[str, pl.DataFrame], compare_cols_all: list[str]) -> pl.DataFrame: + filtered_results = {} + for scenario_name, scenario_df in results_dfs.items(): + filtered_results[scenario_name] = scenario_df.select(["year", *compare_cols_all]) + print(f"Added {scenario_name} with shape {scenario_df.shape}") + + print(f"filtered_results keys: {filtered_results.keys()}") + # Concatenate and transform to long format + combined_df = pl.concat( + [df.with_columns(pl.lit(scenario_id).alias("scenario_id")) for scenario_id, df in filtered_results.items()], + how="vertical", + ) - return results_df, create_delta_bau_df(results_df, COMPARE_COLS) + return combined_df