From 5f9c7302e33b2b9b739de1250a086282bcb88283 Mon Sep 17 00:00:00 2001 From: "j.aschauer" Date: Thu, 4 Apr 2024 16:47:09 +0200 Subject: [PATCH 1/7] Display interest rates in percent Fixes #261 --- app/layout_elements.py | 2 +- app/ptxboa_functions.py | 4 ++++ app/user_data.py | 14 +++++++++++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/app/layout_elements.py b/app/layout_elements.py index 991e41c5..fd684f96 100644 --- a/app/layout_elements.py +++ b/app/layout_elements.py @@ -263,7 +263,7 @@ def display_and_edit_input_data( missing_index_name = "parameter_code" missing_index_value = "interest rate" column_config = { - c: st.column_config.NumberColumn(format="%.3f", min_value=0, max_value=1) + c: st.column_config.NumberColumn(format="%.2f", min_value=0, max_value=100) for c in df.columns } diff --git a/app/ptxboa_functions.py b/app/ptxboa_functions.py index 4bfc61f3..935bcb16 100644 --- a/app/ptxboa_functions.py +++ b/app/ptxboa_functions.py @@ -381,6 +381,10 @@ def get_data_type_from_input_data( if scope in ["Argentina", "Morocco", "South Africa"]: df = select_subregions(df, scope) + if data_type == "interest rate": + # transform data to match unit [%] + df = df * 100 + return df diff --git a/app/user_data.py b/app/user_data.py index 3ddee080..72621abe 100644 --- a/app/user_data.py +++ b/app/user_data.py @@ -57,6 +57,12 @@ def register_user_changes( # Replace the 'id' values with the corresponding index elements from df_tab res[index] = res[index].map(lambda x: df_tab.index[x]) + # convert the interest rate from [%] to [decimals] + res["value"] = res["value"].astype(float) + res.loc[res["parameter_code"] == "interest rate", "value"] = ( + res.loc[res["parameter_code"] == "interest rate", "value"] / 100 + ) + if st.session_state["user_changes_df"] is None: st.session_state["user_changes_df"] = pd.DataFrame( columns=[ @@ -70,7 +76,7 @@ def register_user_changes( # only track the last changes if a duplicate entry is found. st.session_state["user_changes_df"] = pd.concat( - [st.session_state["user_changes_df"], res] + [st.session_state["user_changes_df"].astype(res.dtypes), res] ).drop_duplicates( subset=[ "source_region_code", @@ -95,6 +101,12 @@ def display_user_changes(api): """Display input data changes made by user.""" if st.session_state["user_changes_df"] is not None: df = st.session_state["user_changes_df"].copy() + + # convert the interest rate from [decimals] to [%] + df.loc[df["parameter_code"] == "interest rate", "value"] = ( + df.loc[df["parameter_code"] == "interest rate", "value"] * 100 + ) + parameters = api.get_dimension("parameter") df["Unit"] = df["parameter_code"].map( pd.Series(parameters["unit"].tolist(), index=parameters["parameter_name"]) From de155436e7a008606d2ec5ddede31b0b4ef08b7b Mon Sep 17 00:00:00 2001 From: "j.aschauer" Date: Mon, 13 May 2024 09:26:23 +0200 Subject: [PATCH 2/7] add percent sign to interest rate columns --- app/layout_elements.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/layout_elements.py b/app/layout_elements.py index fd684f96..602d31b9 100644 --- a/app/layout_elements.py +++ b/app/layout_elements.py @@ -263,7 +263,9 @@ def display_and_edit_input_data( missing_index_name = "parameter_code" missing_index_value = "interest rate" column_config = { - c: st.column_config.NumberColumn(format="%.2f", min_value=0, max_value=100) + c: st.column_config.NumberColumn( + format="%.2f %%", min_value=0, max_value=100 + ) for c in df.columns } From 7817b50f8932d4be5f2e2207bdc580d99ae970e9 Mon Sep 17 00:00:00 2001 From: "j.aschauer" Date: Mon, 13 May 2024 14:01:23 +0200 Subject: [PATCH 3/7] display efficiency in % --- app/ptxboa_functions.py | 7 +++++-- app/user_data.py | 16 +++++++++------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/app/ptxboa_functions.py b/app/ptxboa_functions.py index 935bcb16..446ad554 100644 --- a/app/ptxboa_functions.py +++ b/app/ptxboa_functions.py @@ -381,10 +381,13 @@ def get_data_type_from_input_data( if scope in ["Argentina", "Morocco", "South Africa"]: df = select_subregions(df, scope) + # transform data to match unit [%] for 'interest_rate' and 'efficieny' if data_type == "interest rate": - # transform data to match unit [%] df = df * 100 + if "efficiency" in df.columns: + df["efficiency"] = df["efficiency"] * 100 + return df @@ -490,7 +493,7 @@ def get_column_config() -> dict: "CAPEX": st.column_config.NumberColumn(format="%.0f USD/kW", min_value=0), "OPEX (fix)": st.column_config.NumberColumn(format="%.0f USD/kW", min_value=0), "efficiency": st.column_config.NumberColumn( - format="%.2f", min_value=0, max_value=1 + format="%.2f %%", min_value=0, max_value=100 ), "lifetime / amortization period": st.column_config.NumberColumn( format="%.0f a", diff --git a/app/user_data.py b/app/user_data.py index 72621abe..7cbd74a7 100644 --- a/app/user_data.py +++ b/app/user_data.py @@ -59,9 +59,10 @@ def register_user_changes( # convert the interest rate from [%] to [decimals] res["value"] = res["value"].astype(float) - res.loc[res["parameter_code"] == "interest rate", "value"] = ( - res.loc[res["parameter_code"] == "interest rate", "value"] / 100 - ) + for param_code in ["interest rate", "efficiency"]: + res.loc[res["parameter_code"] == param_code, "value"] = ( + res.loc[res["parameter_code"] == param_code, "value"] / 100 + ) if st.session_state["user_changes_df"] is None: st.session_state["user_changes_df"] = pd.DataFrame( @@ -102,10 +103,11 @@ def display_user_changes(api): if st.session_state["user_changes_df"] is not None: df = st.session_state["user_changes_df"].copy() - # convert the interest rate from [decimals] to [%] - df.loc[df["parameter_code"] == "interest rate", "value"] = ( - df.loc[df["parameter_code"] == "interest rate", "value"] * 100 - ) + # convert the values for 'interest rate' and 'efficiency' from [decimals] to [%] + for param_code in ["interest rate", "efficiency"]: + df.loc[df["parameter_code"] == param_code, "value"] = ( + df.loc[df["parameter_code"] == param_code, "value"] * 100 + ) parameters = api.get_dimension("parameter") df["Unit"] = df["parameter_code"].map( From ec25ea5d7d2e7baf12aa83d57e6f17065cc69b89 Mon Sep 17 00:00:00 2001 From: "j.aschauer" Date: Wed, 15 May 2024 09:37:24 +0200 Subject: [PATCH 4/7] display interest rates in percent in the map also minor changes for hover data --- app/plot_functions.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/app/plot_functions.py b/app/plot_functions.py index 2c8e01b7..67542ac2 100644 --- a/app/plot_functions.py +++ b/app/plot_functions.py @@ -127,13 +127,13 @@ def plot_input_data_on_map( user_data=st.session_state["user_changes_df"], ) - units = {"CAPEX": "USD/kW", "full load hours": "h/a", "interest rate": ""} + units = {"CAPEX": "USD/kW", "full load hours": "h/a", "interest rate": "%"} if data_type == "interest rate": assert color_col == "interest rate" columns = "parameter_code" process_code = [""] - custom_data_func_kwargs = {"float_precision": 3} + custom_data_func_kwargs = {"float_precision": 2} else: assert color_col in [ "PV tilted", @@ -163,6 +163,10 @@ def plot_input_data_on_map( values="value", ) + # transform data to % for interest rate + if data_type == "interest rate": + input_data = input_data * 100 + if scope == "world": # Create a choropleth world map: fig = _choropleth_map_world( @@ -318,15 +322,18 @@ def _make_inputs_hoverdata(df, data_type, map_variable, unit, float_precision): custom_hover_data = [] if data_type == "interest rate": for idx, row in df.iterrows(): - hover = f"{idx} | {data_type}

{row['interest rate']}" + hover = ( + f"{idx} | {data_type}

" + f"{row['interest rate']:.{float_precision}f} {unit}" + ) custom_hover_data.append(hover) else: for idx, row in df.iterrows(): hover = f"{idx} | {data_type}
" for i, v in zip(row.index, row): - hover += f"
{i}: {v:.{float_precision}f}{unit}" + hover += f"
{i}: {v:.{float_precision}f} {unit}" if i == map_variable: - hover += " (displayed on map)" + hover += " ← displayed on map" custom_hover_data.append(hover) return [custom_hover_data] From 61cd3ffafd7f724ad5125ec989788beb39b48327 Mon Sep 17 00:00:00 2001 From: "j.aschauer" Date: Wed, 15 May 2024 11:39:00 +0200 Subject: [PATCH 5/7] Use buttons instead of tabs and improve performance Fixes #352 Costs are only calculated when a tab needs them --- app/tab_costs.py | 10 ++- app/tab_deep_dive_countries.py | 6 +- ptxboa_streamlit.py | 131 ++++++++++++++++++++------------- 3 files changed, 93 insertions(+), 54 deletions(-) diff --git a/app/tab_costs.py b/app/tab_costs.py index 1d79ba64..3fda87ba 100644 --- a/app/tab_costs.py +++ b/app/tab_costs.py @@ -78,8 +78,14 @@ def content_costs( with st.container(border=True): display_costs( remove_subregions(api, costs_per_region, st.session_state["country"]), - remove_subregions( - api, costs_per_region_without_user_changes, st.session_state["country"] + ( + remove_subregions( + api, + costs_per_region_without_user_changes, + st.session_state["country"], + ) + if st.session_state["user_changes_df"] is not None + else None ), "region", "Costs by region", diff --git a/app/tab_deep_dive_countries.py b/app/tab_deep_dive_countries.py index 87c47801..c9988741 100644 --- a/app/tab_deep_dive_countries.py +++ b/app/tab_deep_dive_countries.py @@ -53,7 +53,11 @@ def content_deep_dive_countries( display_costs( select_subregions(costs_per_region, ddc), - select_subregions(costs_per_region_without_user_changes, ddc), + ( + select_subregions(costs_per_region_without_user_changes, ddc) + if st.session_state["user_changes_df"] is not None + else None + ), key="region", titlestring="Costs per subregion", key_suffix=ddc, diff --git a/ptxboa_streamlit.py b/ptxboa_streamlit.py index b43269a2..bca2592c 100644 --- a/ptxboa_streamlit.py +++ b/ptxboa_streamlit.py @@ -153,14 +153,14 @@ if st.session_state["tab_key"] not in st.session_state: st.session_state[st.session_state["tab_key"]] = "Costs" -sac.tabs( - [sac.TabsItem(label=i, icon=tabs_icons.get(i, None)) for i in tabs], +sac.buttons( + [sac.ButtonsItem(label=i, icon=tabs_icons.get(i, None)) for i in tabs], index=tabs.index(st.session_state[st.session_state["tab_key"]]), format_func="title", align="center", key=st.session_state["tab_key"], ) - +st.divider() # create sidebar: make_sidebar(api) @@ -169,56 +169,85 @@ colors = pd.read_csv("data/Agora_Industry_Colours.csv") st.session_state["colors"] = colors["Hex Code"].to_list() -# calculate results over different data dimensions: -costs_per_region = calculate_results_list( - api, parameter_to_change="region", parameter_list=None -) -costs_per_scenario = calculate_results_list( - api, - parameter_to_change="scenario", - parameter_list=None, -) -costs_per_res_gen = calculate_results_list( - api, - parameter_to_change="res_gen", - # TODO: here we remove PV tracking manually, this needs to be fixed in data - parameter_list=[ - x for x in api.get_dimension("res_gen").index.to_list() if x != "PV tracking" - ], -) -costs_per_chain = calculate_results_list( - api, - parameter_to_change="chain", - parameter_list=None, - override_session_state={"output_unit": "USD/MWh"}, -) +if st.session_state[st.session_state["tab_key"]] in [ + "Costs", + "Market scanning", + "Input data", + "Deep-dive countries", +]: + # calculate results over different data dimensions: + costs_per_region = calculate_results_list( + api, parameter_to_change="region", parameter_list=None + ) + costs_per_scenario = calculate_results_list( + api, + parameter_to_change="scenario", + parameter_list=None, + ) + costs_per_res_gen = calculate_results_list( + api, + parameter_to_change="res_gen", + # TODO: here we remove PV tracking manually, this needs to be fixed in data + parameter_list=[ + x + for x in api.get_dimension("res_gen").index.to_list() + if x != "PV tracking" + ], + ) + costs_per_chain = calculate_results_list( + api, + parameter_to_change="chain", + parameter_list=None, + override_session_state={"output_unit": "USD/MWh"}, + ) -# calculate results over different data dimensions (without user changes): -costs_per_region_without_user_changes = calculate_results_list( - api, parameter_to_change="region", parameter_list=None, apply_user_data=False -) -costs_per_scenario_without_user_changes = calculate_results_list( - api, parameter_to_change="scenario", parameter_list=None, apply_user_data=False -) -costs_per_res_gen_without_user_changes = calculate_results_list( - api, - parameter_to_change="res_gen", - # TODO: here we remove PV tracking manually, this needs to be fixed in data - parameter_list=[ - x for x in api.get_dimension("res_gen").index.to_list() if x != "PV tracking" - ], - apply_user_data=False, -) -costs_per_chain_without_user_changes = calculate_results_list( - api, - parameter_to_change="chain", - parameter_list=None, - override_session_state={"output_unit": "USD/MWh"}, - apply_user_data=False, -) + if st.session_state["user_changes_df"] is not None: + # calculate results over different data dimensions (without user changes): + costs_per_region_without_user_changes = calculate_results_list( + api, + parameter_to_change="region", + parameter_list=None, + apply_user_data=False, + ) + costs_per_scenario_without_user_changes = calculate_results_list( + api, + parameter_to_change="scenario", + parameter_list=None, + apply_user_data=False, + ) + costs_per_res_gen_without_user_changes = calculate_results_list( + api, + parameter_to_change="res_gen", + # TODO: here we remove PV tracking manually, this needs to be fixed in data + parameter_list=[ + x + for x in api.get_dimension("res_gen").index.to_list() + if x != "PV tracking" + ], + apply_user_data=False, + ) + costs_per_chain_without_user_changes = calculate_results_list( + api, + parameter_to_change="chain", + parameter_list=None, + override_session_state={"output_unit": "USD/MWh"}, + apply_user_data=False, + ) + else: + costs_per_region_without_user_changes = None + costs_per_scenario_without_user_changes = None + costs_per_res_gen_without_user_changes = None + costs_per_chain_without_user_changes = None -# import context data: -cd = load_context_data() +if st.session_state[st.session_state["tab_key"]] in [ + "Country fact sheets", + "Certification schemes", + "Sustainability", + "Literature", + "Market scanning", +]: + # import context data: + cd = load_context_data() # costs: if st.session_state[st.session_state["tab_key"]] == "Costs": From f0905f74f2700db524894501a66cfe7f6d51b7b6 Mon Sep 17 00:00:00 2001 From: "j.aschauer" Date: Wed, 15 May 2024 11:40:28 +0200 Subject: [PATCH 6/7] add smoke test for each tab individually --- tests/test_ptxboa_streamlit.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/test_ptxboa_streamlit.py b/tests/test_ptxboa_streamlit.py index 8bbb37cf..59e482aa 100644 --- a/tests/test_ptxboa_streamlit.py +++ b/tests/test_ptxboa_streamlit.py @@ -16,3 +16,30 @@ def running_app(): def test_app_smoke(running_app): """Test if the app starts up without errors.""" assert not running_app.exception + + +@pytest.fixture( + params=( + "Info", + "Costs", + "Market scanning", + "Input data", + "Deep-dive countries", + "Country fact sheets", + "Certification schemes", + "Sustainability", + "Literature", + "Optimization", + ) +) +def running_app_on_tab(request): + tab = request.param + at = AppTest.from_file("ptxboa_streamlit.py") + at.session_state["tab_key"] = "tab_key_0" + at.session_state[at.session_state["tab_key"]] = tab + at.run(timeout=60) + return at + + +def test_tabs_smoke(running_app_on_tab): + assert not running_app_on_tab.exception From eb7ccc5c507c48ff3e65cf3a8d72df53f31d232e Mon Sep 17 00:00:00 2001 From: "j.aschauer" Date: Wed, 15 May 2024 13:44:33 +0200 Subject: [PATCH 7/7] change unit to % in yaxis label of boxplot for interest rate --- app/tab_input_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/tab_input_data.py b/app/tab_input_data.py index fcdc72db..0b3450ed 100644 --- a/app/tab_input_data.py +++ b/app/tab_input_data.py @@ -70,7 +70,7 @@ def content_input_data(api: PtxboaAPI) -> None: if data_selection == "full load hours": ylabel = "full load hours (h/a)" if data_selection == "interest rate": - ylabel = "interest rate (per unit)" + ylabel = "interest rate (%)" fig = px.box(df) fig.update_layout(xaxis_title=None, yaxis_title=ylabel) st.plotly_chart(fig, use_container_width=True)