From 7a428b861f9bb2fb740e043b6ec5052c3793fe01 Mon Sep 17 00:00:00 2001 From: "j.aschauer" Date: Wed, 15 Nov 2023 16:27:26 +0100 Subject: [PATCH 01/10] move sidebar creation to module, move reset_input_data and color loading to main file --- app/ptxboa_functions.py | 165 ---------------------------------------- app/sidebar.py | 165 ++++++++++++++++++++++++++++++++++++++++ ptxboa_streamlit.py | 11 ++- 3 files changed, 175 insertions(+), 166 deletions(-) create mode 100644 app/sidebar.py diff --git a/app/ptxboa_functions.py b/app/ptxboa_functions.py index c583a087..58e23e5f 100644 --- a/app/ptxboa_functions.py +++ b/app/ptxboa_functions.py @@ -124,171 +124,6 @@ def aggregate_costs(res_details: pd.DataFrame) -> pd.DataFrame: # Settings: -def create_sidebar(api: PtxboaAPI): - st.sidebar.subheader("Main settings:") - include_subregions = False - if include_subregions: - region_list = api.get_dimension("region").index - else: - region_list = ( - api.get_dimension("region") - .loc[api.get_dimension("region")["subregion_code"] == ""] - .index - ) - - st.session_state["region"] = st.sidebar.selectbox( - "Supply country / region:", - region_list, - help=( - "One supply country or region can be selected here, " - " and detailed settings can be selected for this region below " - "(RE source, mode of transportation). For other regions, " - "default settings will be used." - ), - ) - include_subregions = st.sidebar.toggle( - "Include subregions", - help=( - "For three deep-dive countries (Argentina, Morocco, and South Africa) " - "the app calculates costs for subregions as well. Activate this switch" - "if you want to chose one of these subregions as a supply region. " - ), - ) - st.session_state["country"] = st.sidebar.selectbox( - "Demand country:", - api.get_dimension("country").index, - help=( - "The country you aim to export to. Some key info on the demand country you " - "choose here are displayed in the info box." - ), - ) - # get chain as combination of product, electrolyzer type and reconversion option: - c1, c2 = st.sidebar.columns(2) - with c1: - product = st.selectbox( - "Product:", - [ - "Ammonia", - "Green Iron", - "Hydrogen", - "LOHC", - "Methane", - "Methanol", - "Ft e-fuels", - ], - help="The product you want to export.", - ) - with c2: - ely = st.selectbox( - "Electrolyzer type:", - [ - "AEL", - "PEM", - "SEOC", - ], - help="The electrolyzer type you wish to use.", - ) - if product in ["Ammonia", "Methane"]: - use_reconversion = st.sidebar.toggle( - "Include reconversion to H2", - help=( - "If activated, account for costs of " - "reconverting product to H2 in demand country." - ), - ) - else: - use_reconversion = False - - st.session_state["chain"] = f"{product} ({ely})" - if use_reconversion: - st.session_state["chain"] = f"{st.session_state['chain']} + reconv. to H2" - - st.session_state["res_gen"] = st.sidebar.selectbox( - "Renewable electricity source (for selected supply region):", - api.get_dimension("res_gen").index, - help=( - "The source of electricity for the selected source country. For all " - "other countries Wind-PV hybrid systems will be used (an optimized mixture " - "of PV and wind onshore plants)" - ), - ) - - # get scenario as combination of year and cost assumption: - c1, c2 = st.sidebar.columns(2) - with c1: - data_year = st.radio( - "Data year:", - [2030, 2040], - index=1, - help=( - "To cover parameter uncertainty and development over time, we provide " - "cost reduction pathways (high / medium / low) for 2030 and 2040." - ), - horizontal=True, - ) - with c2: - cost_scenario = st.radio( - "Cost assumptions:", - ["high", "medium", "low"], - index=1, - help=( - "To cover parameter uncertainty and development over time, we provide " - "cost reduction pathways (high / medium / low) for 2030 and 2040." - ), - horizontal=True, - ) - st.session_state["scenario"] = f"{data_year} ({cost_scenario})" - - st.sidebar.subheader("Additional settings:") - st.session_state["secproc_co2"] = st.sidebar.radio( - "Carbon source:", - api.get_dimension("secproc_co2").index, - horizontal=True, - help="Help text", - ) - st.session_state["secproc_water"] = st.sidebar.radio( - "Water source:", - api.get_dimension("secproc_water").index, - horizontal=True, - help="Help text", - ) - st.session_state["transport"] = st.sidebar.radio( - "Mode of transportation (for selected supply country):", - api.get_dimension("transport").index, - horizontal=True, - help="Help text", - ) - if st.session_state["transport"] == "Ship": - st.session_state["ship_own_fuel"] = st.sidebar.toggle( - "For shipping option: Use the product as own fuel?", - help="Help text", - ) - st.session_state["output_unit"] = st.sidebar.radio( - "Unit for delivered costs:", - api.get_dimension("output_unit").index, - horizontal=True, - help="Help text", - ) - - st.sidebar.toggle( - "Edit input data", - help="""Activate this to enable editing of input data. -Currently, your changes will be stored, but they will not be -used in calculation and they will not be displayed in figures. - -Disable this setting to reset user data to default values.""", - value=False, - key="edit_input_data", - ) - - if st.session_state["edit_input_data"] is False: - reset_user_changes() - - # import agora color scale: - if "colors" not in st.session_state: - colors = pd.read_csv("data/Agora_Industry_Colours.csv") - st.session_state["colors"] = colors["Hex Code"].to_list() - return def create_world_map(api: PtxboaAPI, res_costs: pd.DataFrame): diff --git a/app/sidebar.py b/app/sidebar.py new file mode 100644 index 00000000..ad419f00 --- /dev/null +++ b/app/sidebar.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- +"""Sidebar creation.""" +import streamlit as st + +from ptxboa.api import PtxboaAPI + + +def make_sidebar(api: PtxboaAPI): + st.sidebar.subheader("Main settings:") + include_subregions = False + if include_subregions: + region_list = api.get_dimension("region").index + else: + region_list = ( + api.get_dimension("region") + .loc[api.get_dimension("region")["subregion_code"] == ""] + .index + ) + + st.session_state["region"] = st.sidebar.selectbox( + "Supply country / region:", + region_list, + help=( + "One supply country or region can be selected here, " + " and detailed settings can be selected for this region below " + "(RE source, mode of transportation). For other regions, " + "default settings will be used." + ), + ) + include_subregions = st.sidebar.toggle( + "Include subregions", + help=( + "For three deep-dive countries (Argentina, Morocco, and South Africa) " + "the app calculates costs for subregions as well. Activate this switch" + "if you want to chose one of these subregions as a supply region. " + ), + ) + st.session_state["country"] = st.sidebar.selectbox( + "Demand country:", + api.get_dimension("country").index, + help=( + "The country you aim to export to. Some key info on the demand country you " + "choose here are displayed in the info box." + ), + ) + # get chain as combination of product, electrolyzer type and reconversion option: + c1, c2 = st.sidebar.columns(2) + with c1: + product = st.selectbox( + "Product:", + [ + "Ammonia", + "Green Iron", + "Hydrogen", + "LOHC", + "Methane", + "Methanol", + "Ft e-fuels", + ], + help="The product you want to export.", + ) + with c2: + ely = st.selectbox( + "Electrolyzer type:", + [ + "AEL", + "PEM", + "SEOC", + ], + help="The electrolyzer type you wish to use.", + ) + if product in ["Ammonia", "Methane"]: + use_reconversion = st.sidebar.toggle( + "Include reconversion to H2", + help=( + "If activated, account for costs of " + "reconverting product to H2 in demand country." + ), + ) + else: + use_reconversion = False + + st.session_state["chain"] = f"{product} ({ely})" + if use_reconversion: + st.session_state["chain"] = f"{st.session_state['chain']} + reconv. to H2" + + st.session_state["res_gen"] = st.sidebar.selectbox( + "Renewable electricity source (for selected supply region):", + api.get_dimension("res_gen").index, + help=( + "The source of electricity for the selected source country. For all " + "other countries Wind-PV hybrid systems will be used (an optimized mixture " + "of PV and wind onshore plants)" + ), + ) + + # get scenario as combination of year and cost assumption: + c1, c2 = st.sidebar.columns(2) + with c1: + data_year = st.radio( + "Data year:", + [2030, 2040], + index=1, + help=( + "To cover parameter uncertainty and development over time, we provide " + "cost reduction pathways (high / medium / low) for 2030 and 2040." + ), + horizontal=True, + ) + with c2: + cost_scenario = st.radio( + "Cost assumptions:", + ["high", "medium", "low"], + index=1, + help=( + "To cover parameter uncertainty and development over time, we provide " + "cost reduction pathways (high / medium / low) for 2030 and 2040." + ), + horizontal=True, + ) + st.session_state["scenario"] = f"{data_year} ({cost_scenario})" + + st.sidebar.subheader("Additional settings:") + st.session_state["secproc_co2"] = st.sidebar.radio( + "Carbon source:", + api.get_dimension("secproc_co2").index, + horizontal=True, + help="Help text", + ) + st.session_state["secproc_water"] = st.sidebar.radio( + "Water source:", + api.get_dimension("secproc_water").index, + horizontal=True, + help="Help text", + ) + st.session_state["transport"] = st.sidebar.radio( + "Mode of transportation (for selected supply country):", + api.get_dimension("transport").index, + horizontal=True, + help="Help text", + ) + if st.session_state["transport"] == "Ship": + st.session_state["ship_own_fuel"] = st.sidebar.toggle( + "For shipping option: Use the product as own fuel?", + help="Help text", + ) + st.session_state["output_unit"] = st.sidebar.radio( + "Unit for delivered costs:", + api.get_dimension("output_unit").index, + horizontal=True, + help="Help text", + ) + + st.sidebar.toggle( + "Edit input data", + help="""Activate this to enable editing of input data. +Currently, your changes will be stored, but they will not be +used in calculation and they will not be displayed in figures. + +Disable this setting to reset user data to default values.""", + value=False, + key="edit_input_data", + ) + + return diff --git a/ptxboa_streamlit.py b/ptxboa_streamlit.py index 72ca181c..5cf42568 100644 --- a/ptxboa_streamlit.py +++ b/ptxboa_streamlit.py @@ -4,6 +4,7 @@ import streamlit as st import app.ptxboa_functions as pf +from app.sidebar import make_sidebar from ptxboa.api import PtxboaAPI # app layout: @@ -46,7 +47,15 @@ api = st.cache_resource(PtxboaAPI)() # create sidebar: -pf.create_sidebar(api) +make_sidebar(api) + +if st.session_state["edit_input_data"] is False: + pf.reset_user_changes() + +# import agora color scale: +if "colors" not in st.session_state: + colors = pd.read_csv("data/Agora_Industry_Colours.csv") + st.session_state["colors"] = colors["Hex Code"].to_list() # calculate results: res_costs = pf.calculate_results_list( From 074bd22405d6f6c60f95938c8e0c4263ca18766d Mon Sep 17 00:00:00 2001 From: "j.aschauer" Date: Wed, 15 Nov 2023 17:38:42 +0100 Subject: [PATCH 02/10] move that show context data to individual modules --- app/context_data.py | 30 +++ app/ptxboa_functions.py | 356 ------------------------------- app/tab_certification_schemes.py | 77 +++++++ app/tab_country_fact_sheets.py | 127 +++++++++++ app/tab_disclaimer.py | 17 ++ app/tab_literature.py | 39 ++++ app/tab_sustainability.py | 93 ++++++++ ptxboa_streamlit.py | 20 +- 8 files changed, 395 insertions(+), 364 deletions(-) create mode 100644 app/context_data.py create mode 100644 app/tab_certification_schemes.py create mode 100644 app/tab_country_fact_sheets.py create mode 100644 app/tab_disclaimer.py create mode 100644 app/tab_literature.py create mode 100644 app/tab_sustainability.py diff --git a/app/context_data.py b/app/context_data.py new file mode 100644 index 00000000..4cbe05ed --- /dev/null +++ b/app/context_data.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +"""Module for loading context data.""" +import pandas as pd +import streamlit as st + + +@st.cache_data() +def load_context_data(): + """Import context data from excel file.""" + filename = "data/context_data.xlsx" + cd = {} + cd["demand_countries"] = pd.read_excel( + filename, sheet_name="demand_countries", skiprows=1 + ) + cd["certification_schemes_countries"] = pd.read_excel( + filename, sheet_name="certification_schemes_countries" + ) + cd["certification_schemes"] = pd.read_excel( + filename, sheet_name="certification_schemes", skiprows=1 + ) + cd["sustainability"] = pd.read_excel(filename, sheet_name="sustainability") + cd["supply"] = pd.read_excel(filename, sheet_name="supply", skiprows=1) + cd["literature"] = pd.read_excel(filename, sheet_name="literature") + cd["infobox"] = pd.read_excel( + filename, + sheet_name="infobox", + usecols="A:F", + skiprows=1, + ).set_index("country_name") + return cd diff --git a/app/ptxboa_functions.py b/app/ptxboa_functions.py index 58e23e5f..4fba7ef3 100644 --- a/app/ptxboa_functions.py +++ b/app/ptxboa_functions.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- """Utility functions for streamlit app.""" -from urllib.parse import urlparse -import numpy as np import pandas as pd import plotly.express as px import plotly.graph_objects as go @@ -972,360 +970,6 @@ def write_info(info): write_info(info4) -def import_context_data(): - """Import context data from excel file.""" - filename = "data/context_data.xlsx" - cd = {} - cd["demand_countries"] = pd.read_excel( - filename, sheet_name="demand_countries", skiprows=1 - ) - cd["certification_schemes_countries"] = pd.read_excel( - filename, sheet_name="certification_schemes_countries" - ) - cd["certification_schemes"] = pd.read_excel( - filename, sheet_name="certification_schemes", skiprows=1 - ) - cd["sustainability"] = pd.read_excel(filename, sheet_name="sustainability") - cd["supply"] = pd.read_excel(filename, sheet_name="supply", skiprows=1) - cd["literature"] = pd.read_excel(filename, sheet_name="literature") - cd["infobox"] = pd.read_excel( - filename, - sheet_name="infobox", - usecols="A:F", - skiprows=1, - ).set_index("country_name") - return cd - - -def create_fact_sheet_demand_country(context_data: dict): - # select country: - country_name = st.session_state["country"] - with st.expander("What is this?"): - st.markdown( - """ -**Country fact sheets** - -This sheet provides you with additional information on the production and import of - hydrogen and derivatives in all PTX BOA supply and demand countries. -For each selected supply and demand country pair, you will find detailed - country profiles. - - For demand countries, we cover the following aspects: - country-specific projected hydrogen demand, - target sectors for hydrogen use, - hydrogen-relevant policies and competent authorities, - certification and regulatory frameworks, - and country-specific characteristics as defined in the demand countries' - hydrogen strategies. - - For the supplying countries, we cover the country-specific technical potential - for renewables (based on various data sources), - LNG export and import infrastructure, - CCS potentials, - availability of an H2 strategy - and wholesale electricity prices. - """ - ) - df = context_data["demand_countries"] - data = df.loc[df["country_name"] == country_name].iloc[0].to_dict() - - flags_to_country_names = { - "France": ":flag-fr:", - "Germany": ":flag-de:", - "Netherlands": ":flag-nl:", - "Spain": ":flag-es:", - "China": ":flag-cn:", - "India": ":flag-in:", - "Japan": ":flag-jp:", - "South Korea": ":flag-kr:", - "USA": ":flag-us:", - } - - st.subheader( - f"{flags_to_country_names[country_name]} Fact sheet for {country_name}" - ) - with st.expander("**Demand**"): - c1, c2, c3 = st.columns(3) - with c1: - st.markdown("**Projected H2 demand in 2030:**") - st.markdown(data["h2_demand_2030"]) - st.markdown(f"*Source: {data['source_h2_demand_2030']}*") - with c2: - st.markdown("**Targeted sectors (main):**") - st.markdown(data["demand_targeted_sectors_main"]) - st.markdown(f"*Source: {data['source_targeted_sectors_main']}*") - with c3: - st.markdown("**Targeted sectors (secondary):**") - st.markdown(data["demand_targeted_sectors_secondary"]) - st.markdown(f"*Source: {data['source_targeted_sectors_secondary']}*") - - with st.expander("**Hydrogen strategy**"): - st.markdown("**Documents:**") - st.markdown(data["h2_strategy_documents"]) - - st.markdown("**Authorities:**") - st.markdown(data["h2_strategy_authorities"]) - - with st.expander("**Hydrogen trade characteristics**"): - st.markdown(data["h2_trade_characteristics"]) - st.markdown(f"*Source: {data['source_h2_trade_characteristics']}*") - - with st.expander("**Infrastructure**"): - st.markdown("**LNG import terminals:**") - st.markdown(data["lng_import_terminals"]) - st.markdown(f"*Source: {data['source_lng_import_terminals']}*") - - st.markdown("**H2 pipeline projects:**") - st.markdown(data["h2_pipeline_projects"]) - st.markdown(f"*Source: {data['source_h2_pipeline_projects']}*") - - if data["certification_info"] != "": - with st.expander("**Certification schemes**"): - st.markdown(data["certification_info"]) - st.markdown(f"*Source: {data['source_certification_info']}*") - - -def create_fact_sheet_supply_country(context_data: dict): - """Display information on a chosen supply country.""" - # select country: - country_name = st.session_state["region"] - df = context_data["supply"] - data = df.loc[df["country_name"] == country_name].iloc[0].to_dict() - - st.subheader(f"Fact sheet for {country_name}") - text = ( - "**Technical potential for renewable electricity generation:**\n" - f"- {data['source_re_tech_pot_EWI']}: " - f"\t{data['re_tech_pot_EWI']:.0f} TWh/a\n" - f"- {data['source_re_tech_pot_PTXAtlas']}: " - f"\t{data['re_tech_pot_PTXAtlas']:.0f} TWh/a\n" - ) - - st.markdown(text) - - text = ( - "**LNG infrastructure:**\n" - f"- {data['lng_export']} export terminals\n" - f"- {data['lng_import']} import terminals.\n\n" - f"*Source: {data['source_lng']}*" - ) - - st.markdown(text) - - st.write("TODO: CCS pot, elec prices, H2 strategy") - - -def create_fact_sheet_certification_schemes(context_data: dict): - with st.expander("What is this?"): - st.markdown( - """ -**Get supplementary information on H2-relevant certification frameworks** - -This sheet provides you with an overview of current governmental regulations -and voluntary standards for H2 products. - """ - ) - df = context_data["certification_schemes"] - helptext = "Select the certification scheme you want to know more about." - scheme_name = st.selectbox("Select scheme:", df["name"], help=helptext) - data = df.loc[df["name"] == scheme_name].iloc[0].to_dict() - - # replace na with "not specified": - for key in data: - if data[key] is np.nan: - data[key] = "not specified" - - st.markdown(data["description"]) - - with st.expander("**Characteristics**"): - st.markdown( - f"- **Relation to other standards:** {data['relation_to_other_standards']}" - ) - st.markdown(f"- **Geographic scope:** {data['geographic_scope']}") - st.markdown(f"- **PTXBOA demand countries:** {data['ptxboa_demand_countries']}") - st.markdown(f"- **Labels:** {data['label']}") - st.markdown(f"- **Lifecycle scope:** {data['lifecycle_scope']}") - - st.markdown( - """ -**Explanations:** - -- Info on "Geographical scope": - - This field provides an answer to the question: if you want to address a specific - country of demand, which regulations and/or standards exist in this country - that require or allow proof of a specific product property? -- Info on "Lifecycle scope": - - Well-to-gate: GHG emissions are calculated up to production. - - Well-to-wheel: GHG emissions are calculated up to the time of use. - - Further information on the life cycle scopes can be found in -IRENA & RMI (2023): Creating a global hydrogen market: certification to enable trade, - pp. 15-19 -""" - ) - - with st.expander("**Scope**"): - if data["scope_emissions"] != "not specified": - st.markdown("- **Emissions:**") - st.markdown(data["scope_emissions"]) - - if data["scope_electricity"] != "not specified": - st.markdown("- **Electricity:**") - st.markdown(data["scope_electricity"]) - - if data["scope_water"] != "not specified": - st.markdown("- **Water:**") - st.markdown(data["scope_water"]) - - if data["scope_biodiversity"] != "not specified": - st.markdown("- **Biodiversity:**") - st.markdown(data["scope_biodiversity"]) - - if data["scope_other"] != "not specified": - st.markdown("- **Other:**") - st.markdown(data["scope_other"]) - - with st.expander("**Sources**"): - st.markdown(data["sources"]) - - -def create_content_sustainability(context_data: dict): - with st.expander("What is this?"): - st.markdown( - """ -**Get supplementary information on PTX-relevant sustainability issues** - -Hydrogen is not sustainable by nature. -And sustainability goes far beyond the CO2-footprint of a product. -It also includes other environmental as well as socio-economic dimensions. - -This is why we provide you with a set of questions that will help you assess your plans -for PTX production and export from a comprehensive sustainability perspective. -Please note that this list does not claim to be exhaustive, -but only serves for an orientation on the topic. - """ - ) - df = context_data["sustainability"] - - c1, c2 = st.columns([2, 1]) - with c1: - st.image("static/sustainability.png") - captiontext = ( - "Source: https://ptx-hub.org/wp-content/uploads/2022/05/" - "PtX-Hub-PtX.Sustainability-Dimensions-and-Concerns-Scoping-Paper.pdf" - ) - st.caption(captiontext) - with c2: - st.markdown( - """ -**Dimensions of sustainability** - -**What sustainability aspects should be considered for PTX products, - production and policies?** - -**What questions should be asked before and during project development?** - -In this tab we aim to provide a basic approach to these questions. - To the left, you can see the framework along which the compilation - of sustainability aspects in this tab is structured. It is based on the EESG framework - as elaborated by the PtX Hub and sustainability criteria developed by the Öko-Institut. - -**The framework distinguishes four key sustainability dimensions - Environmental, - Economic, Social and Governance - from which you can select below.** - - Within each of these dimensions there are different clusters of sustainability aspects - that we address in a set of questions. We differentiate between questions indicating - guardrails and questions suggesting goals. - -With this compilation, we aim to provide a general overview of the sustainability - issues that may be relevant in the context of PTX production. Of course, - different aspects are more or less important depending on the project, - product and country. - -**Take a look for yourself to see which dimensions are most important - from where you are coming from.** - """ - ) - st.divider() - - c1, c2 = st.columns(2) - with c1: - helptext = "helptext" - dimension = st.selectbox( - "Select dimension:", df["dimension"].unique(), help=helptext - ) - with c2: - helptext = """ -We understand **guardrails** as guidelines which can help you to produce green -PTX products that are sustainable also beyond their greenhouse gas emission intensity. - -**Goals** are guidelines which can help link PTX production to improving local - ecological and socio-economic circumstances in the supply country. -They act as additional to guardrails which should be fulfilled in the first place - to meet basic sustainability needs. -""" - question_type = st.radio( - "Guardrails or goals?", - ["Guardrails", "Goals"], - help=helptext, - horizontal=True, - ) - data = df.loc[(df["dimension"] == dimension) & (df["type"] == question_type)] - - for topic in data["topic"].unique(): - with st.expander(f"**{topic}**"): - data_select = data.loc[data["topic"] == topic] - for _ind, row in data_select.iterrows(): - st.markdown(f"- {row['question']}") - - -def is_valid_url(url: str) -> bool: - """Check if a string is a valid url.""" - if not isinstance(url, str): - return False - - try: - result = urlparse(url) - # Check if result.scheme and result.netloc are non-empty - return all([result.scheme, result.netloc]) - except ValueError: - return False - - -def create_content_literature(context_data: dict): - with st.expander("What is this?"): - st.markdown( - """ -**List of references** - -This tab contains a list of references used in this app. - """ - ) - df = context_data["literature"] - markdown_text = "" - for _ind, row in df.iterrows(): - if is_valid_url(row["url"]): - text = f"- {row['long_name']}: [Link]({row['url']})\n" - else: - text = f"- {row['long_name']}\n" - markdown_text = markdown_text + text - - st.markdown(markdown_text) - - -def content_disclaimer(): - with st.expander("What is this?"): - st.markdown( - """ -**Disclaimer** - -Information on product details of the PTX Business Opportunity Analyser - including a citation suggestion of the tool. - """ - ) - st.image("static/disclaimer.png") - st.image("static/disclaimer_2.png") - - def config_number_columns(df: pd.DataFrame, **kwargs) -> {}: """Create number column config info for st.dataframe() or st.data_editor.""" column_config_all = {} diff --git a/app/tab_certification_schemes.py b/app/tab_certification_schemes.py new file mode 100644 index 00000000..c1e9bbc7 --- /dev/null +++ b/app/tab_certification_schemes.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +"""Content of certification schemes tab.""" +import numpy as np +import streamlit as st + + +def content_certification_schemes(context_data: dict): + with st.expander("What is this?"): + st.markdown( + """ +**Get supplementary information on H2-relevant certification frameworks** + +This sheet provides you with an overview of current governmental regulations +and voluntary standards for H2 products. + """ + ) + df = context_data["certification_schemes"] + helptext = "Select the certification scheme you want to know more about." + scheme_name = st.selectbox("Select scheme:", df["name"], help=helptext) + data = df.loc[df["name"] == scheme_name].iloc[0].to_dict() + + # replace na with "not specified": + for key in data: + if data[key] is np.nan: + data[key] = "not specified" + + st.markdown(data["description"]) + + with st.expander("**Characteristics**"): + st.markdown( + f"- **Relation to other standards:** {data['relation_to_other_standards']}" + ) + st.markdown(f"- **Geographic scope:** {data['geographic_scope']}") + st.markdown(f"- **PTXBOA demand countries:** {data['ptxboa_demand_countries']}") + st.markdown(f"- **Labels:** {data['label']}") + st.markdown(f"- **Lifecycle scope:** {data['lifecycle_scope']}") + + st.markdown( + """ +**Explanations:** + +- Info on "Geographical scope": + - This field provides an answer to the question: if you want to address a specific + country of demand, which regulations and/or standards exist in this country + that require or allow proof of a specific product property? +- Info on "Lifecycle scope": + - Well-to-gate: GHG emissions are calculated up to production. + - Well-to-wheel: GHG emissions are calculated up to the time of use. + - Further information on the life cycle scopes can be found in +IRENA & RMI (2023): Creating a global hydrogen market: certification to enable trade, + pp. 15-19 +""" + ) + + with st.expander("**Scope**"): + if data["scope_emissions"] != "not specified": + st.markdown("- **Emissions:**") + st.markdown(data["scope_emissions"]) + + if data["scope_electricity"] != "not specified": + st.markdown("- **Electricity:**") + st.markdown(data["scope_electricity"]) + + if data["scope_water"] != "not specified": + st.markdown("- **Water:**") + st.markdown(data["scope_water"]) + + if data["scope_biodiversity"] != "not specified": + st.markdown("- **Biodiversity:**") + st.markdown(data["scope_biodiversity"]) + + if data["scope_other"] != "not specified": + st.markdown("- **Other:**") + st.markdown(data["scope_other"]) + + with st.expander("**Sources**"): + st.markdown(data["sources"]) diff --git a/app/tab_country_fact_sheets.py b/app/tab_country_fact_sheets.py new file mode 100644 index 00000000..50053750 --- /dev/null +++ b/app/tab_country_fact_sheets.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- +"""Content of country fact sheets tab and functions to create it.""" +import streamlit as st + + +def _create_fact_sheet_demand_country(context_data: dict): + # select country: + country_name = st.session_state["country"] + with st.expander("What is this?"): + st.markdown( + """ +**Country fact sheets** + +This sheet provides you with additional information on the production and import of + hydrogen and derivatives in all PTX BOA supply and demand countries. +For each selected supply and demand country pair, you will find detailed + country profiles. + + For demand countries, we cover the following aspects: + country-specific projected hydrogen demand, + target sectors for hydrogen use, + hydrogen-relevant policies and competent authorities, + certification and regulatory frameworks, + and country-specific characteristics as defined in the demand countries' + hydrogen strategies. + + For the supplying countries, we cover the country-specific technical potential + for renewables (based on various data sources), + LNG export and import infrastructure, + CCS potentials, + availability of an H2 strategy + and wholesale electricity prices. + """ + ) + df = context_data["demand_countries"] + data = df.loc[df["country_name"] == country_name].iloc[0].to_dict() + + flags_to_country_names = { + "France": ":flag-fr:", + "Germany": ":flag-de:", + "Netherlands": ":flag-nl:", + "Spain": ":flag-es:", + "China": ":flag-cn:", + "India": ":flag-in:", + "Japan": ":flag-jp:", + "South Korea": ":flag-kr:", + "USA": ":flag-us:", + } + + st.subheader( + f"{flags_to_country_names[country_name]} Fact sheet for {country_name}" + ) + with st.expander("**Demand**"): + c1, c2, c3 = st.columns(3) + with c1: + st.markdown("**Projected H2 demand in 2030:**") + st.markdown(data["h2_demand_2030"]) + st.markdown(f"*Source: {data['source_h2_demand_2030']}*") + with c2: + st.markdown("**Targeted sectors (main):**") + st.markdown(data["demand_targeted_sectors_main"]) + st.markdown(f"*Source: {data['source_targeted_sectors_main']}*") + with c3: + st.markdown("**Targeted sectors (secondary):**") + st.markdown(data["demand_targeted_sectors_secondary"]) + st.markdown(f"*Source: {data['source_targeted_sectors_secondary']}*") + + with st.expander("**Hydrogen strategy**"): + st.markdown("**Documents:**") + st.markdown(data["h2_strategy_documents"]) + + st.markdown("**Authorities:**") + st.markdown(data["h2_strategy_authorities"]) + + with st.expander("**Hydrogen trade characteristics**"): + st.markdown(data["h2_trade_characteristics"]) + st.markdown(f"*Source: {data['source_h2_trade_characteristics']}*") + + with st.expander("**Infrastructure**"): + st.markdown("**LNG import terminals:**") + st.markdown(data["lng_import_terminals"]) + st.markdown(f"*Source: {data['source_lng_import_terminals']}*") + + st.markdown("**H2 pipeline projects:**") + st.markdown(data["h2_pipeline_projects"]) + st.markdown(f"*Source: {data['source_h2_pipeline_projects']}*") + + if data["certification_info"] != "": + with st.expander("**Certification schemes**"): + st.markdown(data["certification_info"]) + st.markdown(f"*Source: {data['source_certification_info']}*") + + +def _create_fact_sheet_supply_country(context_data: dict): + """Display information on a chosen supply country.""" + # select country: + country_name = st.session_state["region"] + df = context_data["supply"] + data = df.loc[df["country_name"] == country_name].iloc[0].to_dict() + + st.subheader(f"Fact sheet for {country_name}") + text = ( + "**Technical potential for renewable electricity generation:**\n" + f"- {data['source_re_tech_pot_EWI']}: " + f"\t{data['re_tech_pot_EWI']:.0f} TWh/a\n" + f"- {data['source_re_tech_pot_PTXAtlas']}: " + f"\t{data['re_tech_pot_PTXAtlas']:.0f} TWh/a\n" + ) + + st.markdown(text) + + text = ( + "**LNG infrastructure:**\n" + f"- {data['lng_export']} export terminals\n" + f"- {data['lng_import']} import terminals.\n\n" + f"*Source: {data['source_lng']}*" + ) + + st.markdown(text) + + st.write("TODO: CCS pot, elec prices, H2 strategy") + + +def content_country_fact_sheets(context_data): + _create_fact_sheet_demand_country(context_data) + st.divider() + _create_fact_sheet_supply_country(context_data) diff --git a/app/tab_disclaimer.py b/app/tab_disclaimer.py new file mode 100644 index 00000000..2cf53bb6 --- /dev/null +++ b/app/tab_disclaimer.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +"""Disclaimer tab.""" +import streamlit as st + + +def content_disclaimer(): + with st.expander("What is this?"): + st.markdown( + """ +**Disclaimer** + +Information on product details of the PTX Business Opportunity Analyser + including a citation suggestion of the tool. + """ + ) + st.image("static/disclaimer.png") + st.image("static/disclaimer_2.png") diff --git a/app/tab_literature.py b/app/tab_literature.py new file mode 100644 index 00000000..fe0eebf0 --- /dev/null +++ b/app/tab_literature.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +"""Content and halper functions for literature tab.""" +from urllib.parse import urlparse + +import streamlit as st + + +def _is_valid_url(url: str) -> bool: + """Check if a string is a valid url.""" + if not isinstance(url, str): + return False + + try: + result = urlparse(url) + # Check if result.scheme and result.netloc are non-empty + return all([result.scheme, result.netloc]) + except ValueError: + return False + + +def content_literature(context_data: dict): + with st.expander("What is this?"): + st.markdown( + """ +**List of references** + +This tab contains a list of references used in this app. + """ + ) + df = context_data["literature"] + markdown_text = "" + for _ind, row in df.iterrows(): + if _is_valid_url(row["url"]): + text = f"- {row['long_name']}: [Link]({row['url']})\n" + else: + text = f"- {row['long_name']}\n" + markdown_text = markdown_text + text + + st.markdown(markdown_text) diff --git a/app/tab_sustainability.py b/app/tab_sustainability.py new file mode 100644 index 00000000..30890832 --- /dev/null +++ b/app/tab_sustainability.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +"""Content of sustainability tab.""" +import streamlit as st + + +def content_sustainability(context_data: dict): + with st.expander("What is this?"): + st.markdown( + """ +**Get supplementary information on PTX-relevant sustainability issues** + +Hydrogen is not sustainable by nature. +And sustainability goes far beyond the CO2-footprint of a product. +It also includes other environmental as well as socio-economic dimensions. + +This is why we provide you with a set of questions that will help you assess your plans +for PTX production and export from a comprehensive sustainability perspective. +Please note that this list does not claim to be exhaustive, +but only serves for an orientation on the topic. + """ + ) + df = context_data["sustainability"] + + c1, c2 = st.columns([2, 1]) + with c1: + st.image("static/sustainability.png") + captiontext = ( + "Source: https://ptx-hub.org/wp-content/uploads/2022/05/" + "PtX-Hub-PtX.Sustainability-Dimensions-and-Concerns-Scoping-Paper.pdf" + ) + st.caption(captiontext) + with c2: + st.markdown( + """ +**Dimensions of sustainability** + +**What sustainability aspects should be considered for PTX products, + production and policies?** + +**What questions should be asked before and during project development?** + +In this tab we aim to provide a basic approach to these questions. + To the left, you can see the framework along which the compilation + of sustainability aspects in this tab is structured. It is based on the EESG framework + as elaborated by the PtX Hub and sustainability criteria developed by the Öko-Institut. + +**The framework distinguishes four key sustainability dimensions - Environmental, + Economic, Social and Governance - from which you can select below.** + + Within each of these dimensions there are different clusters of sustainability aspects + that we address in a set of questions. We differentiate between questions indicating + guardrails and questions suggesting goals. + +With this compilation, we aim to provide a general overview of the sustainability + issues that may be relevant in the context of PTX production. Of course, + different aspects are more or less important depending on the project, + product and country. + +**Take a look for yourself to see which dimensions are most important + from where you are coming from.** + """ + ) + st.divider() + + c1, c2 = st.columns(2) + with c1: + helptext = "helptext" + dimension = st.selectbox( + "Select dimension:", df["dimension"].unique(), help=helptext + ) + with c2: + helptext = """ +We understand **guardrails** as guidelines which can help you to produce green +PTX products that are sustainable also beyond their greenhouse gas emission intensity. + +**Goals** are guidelines which can help link PTX production to improving local + ecological and socio-economic circumstances in the supply country. +They act as additional to guardrails which should be fulfilled in the first place + to meet basic sustainability needs. +""" + question_type = st.radio( + "Guardrails or goals?", + ["Guardrails", "Goals"], + help=helptext, + horizontal=True, + ) + data = df.loc[(df["dimension"] == dimension) & (df["type"] == question_type)] + + for topic in data["topic"].unique(): + with st.expander(f"**{topic}**"): + data_select = data.loc[data["topic"] == topic] + for _ind, row in data_select.iterrows(): + st.markdown(f"- {row['question']}") diff --git a/ptxboa_streamlit.py b/ptxboa_streamlit.py index 5cf42568..16ccfe13 100644 --- a/ptxboa_streamlit.py +++ b/ptxboa_streamlit.py @@ -4,7 +4,13 @@ import streamlit as st import app.ptxboa_functions as pf +from app.context_data import load_context_data from app.sidebar import make_sidebar +from app.tab_certification_schemes import content_certification_schemes +from app.tab_country_fact_sheets import content_country_fact_sheets +from app.tab_disclaimer import content_disclaimer +from app.tab_literature import content_literature +from app.tab_sustainability import content_sustainability from ptxboa.api import PtxboaAPI # app layout: @@ -63,7 +69,7 @@ ) # import context data: -cd = st.cache_resource(pf.import_context_data)() +cd = load_context_data() # dashboard: with t_dashboard: @@ -82,18 +88,16 @@ pf.content_deep_dive_countries(api, res_costs) with t_country_fact_sheets: - pf.create_fact_sheet_demand_country(cd) - st.divider() - pf.create_fact_sheet_supply_country(cd) + content_country_fact_sheets(cd) with t_certification_schemes: - pf.create_fact_sheet_certification_schemes(cd) + content_certification_schemes(cd) with t_sustainability: - pf.create_content_sustainability(cd) + content_sustainability(cd) with t_literature: - pf.create_content_literature(cd) + content_literature(cd) with t_disclaimer: - pf.content_disclaimer() + content_disclaimer() From 551d0f8f6a747cd70f8ee733b98cb4156a5d5cd2 Mon Sep 17 00:00:00 2001 From: "j.aschauer" Date: Wed, 15 Nov 2023 17:48:12 +0100 Subject: [PATCH 03/10] move dashboard content to individual module --- app/ptxboa_functions.py | 63 ------------------------------------- app/tab_dashboard.py | 69 +++++++++++++++++++++++++++++++++++++++++ ptxboa_streamlit.py | 3 +- 3 files changed, 71 insertions(+), 64 deletions(-) create mode 100644 app/tab_dashboard.py diff --git a/app/ptxboa_functions.py b/app/ptxboa_functions.py index 4fba7ef3..64315ea1 100644 --- a/app/ptxboa_functions.py +++ b/app/ptxboa_functions.py @@ -5,7 +5,6 @@ import plotly.express as px import plotly.graph_objects as go import streamlit as st -from plotly.subplots import make_subplots from ptxboa.api import PtxboaAPI @@ -121,9 +120,6 @@ def aggregate_costs(res_details: pd.DataFrame) -> pd.DataFrame: return res -# Settings: - - def create_world_map(api: PtxboaAPI, res_costs: pd.DataFrame): """Create world map.""" parameter_to_show_on_map = "Total" @@ -331,45 +327,6 @@ def create_scatter_plot(df_res, settings: dict): st.write(df_res) -def content_dashboard(api, res_costs: dict, context_data: dict): - with st.expander("What is this?"): - st.markdown( - """ -This is the dashboard. It shows key results according to your settings: -- a map and a box plot that show the spread and the -regional distribution of total costs across supply regions -- a split-up of costs by category for your chosen supply region -- key information on your chosen demand country. - -Switch to other tabs to explore data and results in more detail! - """ - ) - - c_1, c_2 = st.columns([2, 1]) - - with c_1: - create_world_map(api, res_costs) - - with c_2: - # create box plot and bar plot: - fig1 = create_box_plot(res_costs) - filtered_data = res_costs[res_costs.index == st.session_state["region"]] - fig2 = create_bar_chart_costs(filtered_data) - doublefig = make_subplots(rows=1, cols=2, shared_yaxes=True) - - for trace in fig1.data: - trace.showlegend = False - doublefig.add_trace(trace, row=1, col=1) - for trace in fig2.data: - doublefig.add_trace(trace, row=1, col=2) - - doublefig.update_layout(barmode="stack") - doublefig.update_layout(title_text="Cost distribution and details:") - st.plotly_chart(doublefig, use_container_width=True) - - create_infobox(context_data) - - def content_market_scanning(api: PtxboaAPI, res_costs: pd.DataFrame) -> None: """Create content for the "market scanning" sheet. @@ -950,26 +907,6 @@ def register_user_changes( ) -def create_infobox(context_data: dict): - data = context_data["infobox"] - st.markdown(f"**Key information on {st.session_state['country']}:**") - demand = data.at[st.session_state["country"], "Projected H2 demand [2030]"] - info1 = data.at[st.session_state["country"], "key_info_1"] - info2 = data.at[st.session_state["country"], "key_info_2"] - info3 = data.at[st.session_state["country"], "key_info_3"] - info4 = data.at[st.session_state["country"], "key_info_4"] - st.markdown(f"* Projected H2 demand in 2030: {demand}") - - def write_info(info): - if isinstance(info, str): - st.markdown(f"* {info}") - - write_info(info1) - write_info(info2) - write_info(info3) - write_info(info4) - - def config_number_columns(df: pd.DataFrame, **kwargs) -> {}: """Create number column config info for st.dataframe() or st.data_editor.""" column_config_all = {} diff --git a/app/tab_dashboard.py b/app/tab_dashboard.py new file mode 100644 index 00000000..e0caa550 --- /dev/null +++ b/app/tab_dashboard.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +"""Content of dashboard tab.""" +import streamlit as st +from plotly.subplots import make_subplots + +from app.ptxboa_functions import ( + create_bar_chart_costs, + create_box_plot, + create_world_map, +) + + +def _create_infobox(context_data: dict): + data = context_data["infobox"] + st.markdown(f"**Key information on {st.session_state['country']}:**") + demand = data.at[st.session_state["country"], "Projected H2 demand [2030]"] + info1 = data.at[st.session_state["country"], "key_info_1"] + info2 = data.at[st.session_state["country"], "key_info_2"] + info3 = data.at[st.session_state["country"], "key_info_3"] + info4 = data.at[st.session_state["country"], "key_info_4"] + st.markdown(f"* Projected H2 demand in 2030: {demand}") + + def write_info(info): + if isinstance(info, str): + st.markdown(f"* {info}") + + write_info(info1) + write_info(info2) + write_info(info3) + write_info(info4) + + +def content_dashboard(api, res_costs: dict, context_data: dict): + with st.expander("What is this?"): + st.markdown( + """ +This is the dashboard. It shows key results according to your settings: +- a map and a box plot that show the spread and the +regional distribution of total costs across supply regions +- a split-up of costs by category for your chosen supply region +- key information on your chosen demand country. + +Switch to other tabs to explore data and results in more detail! + """ + ) + + c_1, c_2 = st.columns([2, 1]) + + with c_1: + create_world_map(api, res_costs) + + with c_2: + # create box plot and bar plot: + fig1 = create_box_plot(res_costs) + filtered_data = res_costs[res_costs.index == st.session_state["region"]] + fig2 = create_bar_chart_costs(filtered_data) + doublefig = make_subplots(rows=1, cols=2, shared_yaxes=True) + + for trace in fig1.data: + trace.showlegend = False + doublefig.add_trace(trace, row=1, col=1) + for trace in fig2.data: + doublefig.add_trace(trace, row=1, col=2) + + doublefig.update_layout(barmode="stack") + doublefig.update_layout(title_text="Cost distribution and details:") + st.plotly_chart(doublefig, use_container_width=True) + + _create_infobox(context_data) diff --git a/ptxboa_streamlit.py b/ptxboa_streamlit.py index 16ccfe13..7402212e 100644 --- a/ptxboa_streamlit.py +++ b/ptxboa_streamlit.py @@ -8,6 +8,7 @@ from app.sidebar import make_sidebar from app.tab_certification_schemes import content_certification_schemes from app.tab_country_fact_sheets import content_country_fact_sheets +from app.tab_dashboard import content_dashboard from app.tab_disclaimer import content_disclaimer from app.tab_literature import content_literature from app.tab_sustainability import content_sustainability @@ -73,7 +74,7 @@ # dashboard: with t_dashboard: - pf.content_dashboard(api, res_costs, cd) + content_dashboard(api, res_costs, cd) with t_market_scanning: pf.content_market_scanning(api, res_costs) From a9b6870d646cb46557de362690320a76bdf9d681 Mon Sep 17 00:00:00 2001 From: "j.aschauer" Date: Wed, 15 Nov 2023 17:54:45 +0100 Subject: [PATCH 04/10] move market scanning tab to individual module --- app/ptxboa_functions.py | 77 ---------------------------------- app/tab_market_scanning.py | 85 ++++++++++++++++++++++++++++++++++++++ ptxboa_streamlit.py | 3 +- 3 files changed, 87 insertions(+), 78 deletions(-) create mode 100644 app/tab_market_scanning.py diff --git a/app/ptxboa_functions.py b/app/ptxboa_functions.py index 64315ea1..8ae72e53 100644 --- a/app/ptxboa_functions.py +++ b/app/ptxboa_functions.py @@ -327,83 +327,6 @@ def create_scatter_plot(df_res, settings: dict): st.write(df_res) -def content_market_scanning(api: PtxboaAPI, res_costs: pd.DataFrame) -> None: - """Create content for the "market scanning" sheet. - - Parameters - ---------- - api : :class:`~ptxboa.api.PtxboaAPI` - an instance of the api class - res_costs : pd.DataFrame - Results. - """ - with st.expander("What is this?"): - st.markdown( - """ -**Market scanning: Get an overview of competing PTX BOA supply countries - and potential demand countries.** - -This sheet helps you to better evaluate your country's competitive position - as well as your options on the emerging global H2 market. - - """ - ) - - # get input data: - input_data = api.get_input_data(st.session_state["scenario"]) - - # filter shipping and pipeline distances: - distances = input_data.loc[ - (input_data["parameter_code"].isin(["shipping distance", "pipeline distance"])) - & (input_data["target_country_code"] == st.session_state["country"]), - ["source_region_code", "parameter_code", "value"], - ] - distances = distances.pivot_table( - index="source_region_code", - columns="parameter_code", - values="value", - aggfunc="sum", - ) - - # merge costs and distances: - df_plot = pd.DataFrame() - df_plot["total costs"] = res_costs["Total"] - df_plot = df_plot.merge(distances, left_index=True, right_index=True) - - # do not show subregions: - df_plot = remove_subregions(api, df_plot, st.session_state["country"]) - - # create plot:st.session_state - [c1, c2] = st.columns([1, 5]) - with c1: - # select which distance to show: - selected_distance = st.radio( - "Select parameter:", - ["shipping distance", "pipeline distance"], - ) - with c2: - fig = px.scatter( - df_plot, - x=selected_distance, - y="total costs", - title="Costs and transportation distances", - height=600, - ) - # Add text above markers - fig.update_traces( - text=df_plot.index, - textposition="top center", - mode="markers+text", - ) - - st.plotly_chart(fig) - - # show data in tabular form: - st.markdown("**Data:**") - column_config = config_number_columns(df_plot, format="%.1f") - st.dataframe(df_plot, use_container_width=True, column_config=column_config) - - def remove_subregions(api: PtxboaAPI, df: pd.DataFrame, country_name: str): """Remove subregions from a dataframe. diff --git a/app/tab_market_scanning.py b/app/tab_market_scanning.py new file mode 100644 index 00000000..14b95828 --- /dev/null +++ b/app/tab_market_scanning.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +"""Content of market scanning tab.""" +import pandas as pd +import plotly.express as px +import streamlit as st + +from app.ptxboa_functions import config_number_columns, remove_subregions +from ptxboa.api import PtxboaAPI + + +def content_market_scanning(api: PtxboaAPI, res_costs: pd.DataFrame) -> None: + """Create content for the "market scanning" sheet. + + Parameters + ---------- + api : :class:`~ptxboa.api.PtxboaAPI` + an instance of the api class + res_costs : pd.DataFrame + Results. + """ + with st.expander("What is this?"): + st.markdown( + """ +**Market scanning: Get an overview of competing PTX BOA supply countries + and potential demand countries.** + +This sheet helps you to better evaluate your country's competitive position + as well as your options on the emerging global H2 market. + + """ + ) + + # get input data: + input_data = api.get_input_data(st.session_state["scenario"]) + + # filter shipping and pipeline distances: + distances = input_data.loc[ + (input_data["parameter_code"].isin(["shipping distance", "pipeline distance"])) + & (input_data["target_country_code"] == st.session_state["country"]), + ["source_region_code", "parameter_code", "value"], + ] + distances = distances.pivot_table( + index="source_region_code", + columns="parameter_code", + values="value", + aggfunc="sum", + ) + + # merge costs and distances: + df_plot = pd.DataFrame() + df_plot["total costs"] = res_costs["Total"] + df_plot = df_plot.merge(distances, left_index=True, right_index=True) + + # do not show subregions: + df_plot = remove_subregions(api, df_plot, st.session_state["country"]) + + # create plot:st.session_state + [c1, c2] = st.columns([1, 5]) + with c1: + # select which distance to show: + selected_distance = st.radio( + "Select parameter:", + ["shipping distance", "pipeline distance"], + ) + with c2: + fig = px.scatter( + df_plot, + x=selected_distance, + y="total costs", + title="Costs and transportation distances", + height=600, + ) + # Add text above markers + fig.update_traces( + text=df_plot.index, + textposition="top center", + mode="markers+text", + ) + + st.plotly_chart(fig) + + # show data in tabular form: + st.markdown("**Data:**") + column_config = config_number_columns(df_plot, format="%.1f") + st.dataframe(df_plot, use_container_width=True, column_config=column_config) diff --git a/ptxboa_streamlit.py b/ptxboa_streamlit.py index 7402212e..a3dc3421 100644 --- a/ptxboa_streamlit.py +++ b/ptxboa_streamlit.py @@ -11,6 +11,7 @@ from app.tab_dashboard import content_dashboard from app.tab_disclaimer import content_disclaimer from app.tab_literature import content_literature +from app.tab_market_scanning import content_market_scanning from app.tab_sustainability import content_sustainability from ptxboa.api import PtxboaAPI @@ -77,7 +78,7 @@ content_dashboard(api, res_costs, cd) with t_market_scanning: - pf.content_market_scanning(api, res_costs) + content_market_scanning(api, res_costs) with t_compare_costs: pf.content_compare_costs(api, res_costs) From 183c9b11f4ea4c2486700fc6a22f4a5735e378f9 Mon Sep 17 00:00:00 2001 From: "j.aschauer" Date: Wed, 15 Nov 2023 18:00:23 +0100 Subject: [PATCH 05/10] move compare costs tab to individual module --- app/ptxboa_functions.py | 95 ---------------------------------- app/tab_compare_costs.py | 107 +++++++++++++++++++++++++++++++++++++++ ptxboa_streamlit.py | 3 +- 3 files changed, 109 insertions(+), 96 deletions(-) create mode 100644 app/tab_compare_costs.py diff --git a/app/ptxboa_functions.py b/app/ptxboa_functions.py index 8ae72e53..0c60b270 100644 --- a/app/ptxboa_functions.py +++ b/app/ptxboa_functions.py @@ -361,101 +361,6 @@ def remove_subregions(api: PtxboaAPI, df: pd.DataFrame, country_name: str): return df -def content_compare_costs(api: PtxboaAPI, res_costs: pd.DataFrame) -> None: - """Create content for the "compare costs" sheet. - - Parameters - ---------- - api : :class:`~ptxboa.api.PtxboaAPI` - an instance of the api class - res_costs : pd.DataFrame - Results. - """ - with st.expander("What is this?"): - st.markdown( - """ -**Compare costs** - -On this sheet, users can analyze total cost and cost components for -different supply countries, scenarios, renewable electricity sources and process chains. -Data is represented as a bar chart and in tabular form. - -Data can be filterend and sorted. - """ - ) - - def display_costs(df_costs: pd.DataFrame, key: str, titlestring: str): - """Display costs as table and bar chart.""" - st.subheader(titlestring) - c1, c2 = st.columns([1, 5]) - with c1: - # filter data: - df_res = df_costs.copy() - - # select filter: - show_which_data = st.radio( - "Select elements to display:", - ["All", "Manual select"], - index=0, - key=f"show_which_data_{key}", - ) - - # apply filter: - if show_which_data == "Manual select": - ind_select = st.multiselect( - "Select regions:", - df_res.index.values, - default=df_res.index.values, - key=f"select_data_{key}", - ) - df_res = df_res.loc[ind_select] - - # sort: - sort_ascending = st.toggle( - "Sort by total costs?", value=True, key=f"sort_data_{key}" - ) - if sort_ascending: - df_res = df_res.sort_values(["Total"], ascending=True) - with c2: - # create graph: - fig = create_bar_chart_costs( - df_res, - current_selection=st.session_state[key], - ) - st.plotly_chart(fig, use_container_width=True) - - with st.expander("**Data**"): - column_config = config_number_columns( - df_res, format=f"%.1f {st.session_state['output_unit']}" - ) - st.dataframe(df_res, use_container_width=True, column_config=column_config) - - res_costs_without_subregions = remove_subregions( - api, res_costs, st.session_state["country"] - ) - display_costs(res_costs_without_subregions, "region", "Costs by region:") - - # Display costs by scenario: - res_scenario = calculate_results_list( - api, "scenario", user_data=st.session_state["user_changes_df"] - ) - display_costs(res_scenario, "scenario", "Costs by data scenario:") - - # Display costs by RE generation: - # TODO: remove PV tracking manually, this needs to be fixed in data - list_res_gen = api.get_dimension("res_gen").index.to_list() - list_res_gen.remove("PV tracking") - res_res_gen = calculate_results_list( - api, - "res_gen", - parameter_list=list_res_gen, - user_data=st.session_state["user_changes_df"], - ) - display_costs(res_res_gen, "res_gen", "Costs by renewable electricity source:") - - # TODO: display costs by chain - - def content_deep_dive_countries(api: PtxboaAPI, res_costs: pd.DataFrame) -> None: """Create content for the "costs by region" sheet. diff --git a/app/tab_compare_costs.py b/app/tab_compare_costs.py new file mode 100644 index 00000000..5c64bdfa --- /dev/null +++ b/app/tab_compare_costs.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +"""Content of compare costs tab.""" +import pandas as pd +import streamlit as st + +from app.ptxboa_functions import ( + calculate_results_list, + config_number_columns, + create_bar_chart_costs, + remove_subregions, +) +from ptxboa.api import PtxboaAPI + + +def content_compare_costs(api: PtxboaAPI, res_costs: pd.DataFrame) -> None: + """Create content for the "compare costs" sheet. + + Parameters + ---------- + api : :class:`~ptxboa.api.PtxboaAPI` + an instance of the api class + res_costs : pd.DataFrame + Results. + """ + with st.expander("What is this?"): + st.markdown( + """ +**Compare costs** + +On this sheet, users can analyze total cost and cost components for +different supply countries, scenarios, renewable electricity sources and process chains. +Data is represented as a bar chart and in tabular form. + +Data can be filterend and sorted. + """ + ) + + def display_costs(df_costs: pd.DataFrame, key: str, titlestring: str): + """Display costs as table and bar chart.""" + st.subheader(titlestring) + c1, c2 = st.columns([1, 5]) + with c1: + # filter data: + df_res = df_costs.copy() + + # select filter: + show_which_data = st.radio( + "Select elements to display:", + ["All", "Manual select"], + index=0, + key=f"show_which_data_{key}", + ) + + # apply filter: + if show_which_data == "Manual select": + ind_select = st.multiselect( + "Select regions:", + df_res.index.values, + default=df_res.index.values, + key=f"select_data_{key}", + ) + df_res = df_res.loc[ind_select] + + # sort: + sort_ascending = st.toggle( + "Sort by total costs?", value=True, key=f"sort_data_{key}" + ) + if sort_ascending: + df_res = df_res.sort_values(["Total"], ascending=True) + with c2: + # create graph: + fig = create_bar_chart_costs( + df_res, + current_selection=st.session_state[key], + ) + st.plotly_chart(fig, use_container_width=True) + + with st.expander("**Data**"): + column_config = config_number_columns( + df_res, format=f"%.1f {st.session_state['output_unit']}" + ) + st.dataframe(df_res, use_container_width=True, column_config=column_config) + + res_costs_without_subregions = remove_subregions( + api, res_costs, st.session_state["country"] + ) + display_costs(res_costs_without_subregions, "region", "Costs by region:") + + # Display costs by scenario: + res_scenario = calculate_results_list( + api, "scenario", user_data=st.session_state["user_changes_df"] + ) + display_costs(res_scenario, "scenario", "Costs by data scenario:") + + # Display costs by RE generation: + # TODO: remove PV tracking manually, this needs to be fixed in data + list_res_gen = api.get_dimension("res_gen").index.to_list() + list_res_gen.remove("PV tracking") + res_res_gen = calculate_results_list( + api, + "res_gen", + parameter_list=list_res_gen, + user_data=st.session_state["user_changes_df"], + ) + display_costs(res_res_gen, "res_gen", "Costs by renewable electricity source:") + + # TODO: display costs by chain diff --git a/ptxboa_streamlit.py b/ptxboa_streamlit.py index a3dc3421..6ecbcd88 100644 --- a/ptxboa_streamlit.py +++ b/ptxboa_streamlit.py @@ -7,6 +7,7 @@ from app.context_data import load_context_data from app.sidebar import make_sidebar from app.tab_certification_schemes import content_certification_schemes +from app.tab_compare_costs import content_compare_costs from app.tab_country_fact_sheets import content_country_fact_sheets from app.tab_dashboard import content_dashboard from app.tab_disclaimer import content_disclaimer @@ -81,7 +82,7 @@ content_market_scanning(api, res_costs) with t_compare_costs: - pf.content_compare_costs(api, res_costs) + content_compare_costs(api, res_costs) with t_input_data: pf.content_input_data(api) From 5e1fe9e0a5cb045537c13b3857e4015a6dc0de2c Mon Sep 17 00:00:00 2001 From: "j.aschauer" Date: Wed, 15 Nov 2023 18:04:59 +0100 Subject: [PATCH 06/10] move input data tab to individual module --- app/ptxboa_functions.py | 151 -------------------------------------- app/tab_input_data.py | 158 ++++++++++++++++++++++++++++++++++++++++ ptxboa_streamlit.py | 3 +- 3 files changed, 160 insertions(+), 152 deletions(-) create mode 100644 app/tab_input_data.py diff --git a/app/ptxboa_functions.py b/app/ptxboa_functions.py index 0c60b270..c76466cb 100644 --- a/app/ptxboa_functions.py +++ b/app/ptxboa_functions.py @@ -457,157 +457,6 @@ def content_deep_dive_countries(api: PtxboaAPI, res_costs: pd.DataFrame) -> None st.plotly_chart(fig, use_container_width=True) -def content_input_data(api: PtxboaAPI) -> None: - """Create content for the "input data" sheet. - - Parameters - ---------- - api : :class:`~ptxboa.api.PtxboaAPI` - an instance of the api class - - Output - ------ - None - """ - with st.expander("What is this?"): - st.markdown( - """ -**Input data** - -This tab gives you an overview of model input data that is country-specific. -This includes full load hours (FLH) and capital expenditures (CAPEX) -of renewable generation technologies, weighted average cost of capital (WACC), -as well as shipping and pipeline distances to the chosen demand country. -The box plots show median, 1st and 3rd quartile as well as the total spread of values. -They also show the data for your country for comparison. - """ - ) - - st.subheader("Region specific data:") - # get input data: - input_data = api.get_input_data( - st.session_state["scenario"], - user_data=st.session_state["user_changes_df"], - ) - - # filter data: - region_list_without_subregions = ( - api.get_dimension("region") - .loc[api.get_dimension("region")["subregion_code"] == ""] - .index.to_list() - ) - input_data_without_subregions = input_data.loc[ - input_data["source_region_code"].isin(region_list_without_subregions) - ] - - list_data_types = ["CAPEX", "full load hours", "interest rate"] - data_selection = st.radio("Select data type", list_data_types, horizontal=True) - if data_selection == "CAPEX": - parameter_code = ["CAPEX"] - process_code = [ - "Wind Onshore", - "Wind Offshore", - "PV tilted", - "Wind-PV-Hybrid", - ] - x = "process_code" - missing_index_name = "parameter_code" - missing_index_value = "CAPEX" - column_config = {"format": "%.0f USD/kW", "min_value": 0} - - if data_selection == "full load hours": - parameter_code = ["full load hours"] - process_code = [ - "Wind Onshore", - "Wind Offshore", - "PV tilted", - "Wind-PV-Hybrid", - ] - x = "process_code" - missing_index_name = "parameter_code" - missing_index_value = "full load hours" - column_config = {"format": "%.0f h/a", "min_value": 0, "max_value": 8760} - - if data_selection == "interest rate": - parameter_code = ["interest rate"] - process_code = [""] - x = "parameter_code" - column_config = {"format": "%.3f", "min_value": 0, "max_value": 1} - missing_index_name = "parameter_code" - missing_index_value = "interest rate" - - c1, c2 = st.columns(2, gap="medium") - with c2: - # show data: - st.markdown("**Data:**") - df = display_and_edit_data_table( - input_data=input_data_without_subregions, - missing_index_name=missing_index_name, - missing_index_value=missing_index_value, - columns=x, - source_region_code=region_list_without_subregions, - parameter_code=parameter_code, - process_code=process_code, - column_config=column_config, - ) - - with c1: - # create plot: - st.markdown("**Figure:**") - fig = px.box(df) - st.plotly_chart(fig, use_container_width=True) - - st.divider() - st.subheader("Data that is identical for all regions:") - - input_data_global = input_data.loc[input_data["source_region_code"] == ""] - - # filter processes: - processes = api.get_dimension("process") - - list_processes_transport = processes.loc[ - processes["is_transport"], "process_name" - ].to_list() - - list_processes_not_transport = processes.loc[ - ~processes["is_transport"], "process_name" - ].to_list() - st.markdown("**Conversion processes:**") - df = display_and_edit_data_table( - input_data_global, - missing_index_name="source_region_code", - missing_index_value=None, - parameter_code=[ - "CAPEX", - "OPEX (fix)", - "lifetime / amortization period", - "efficiency", - ], - process_code=list_processes_not_transport, - index="process_code", - columns="parameter_code", - ) - st.markdown("**Transportation processes:**") - st.markdown("TODO: fix data") - df = display_and_edit_data_table( - input_data_global, - missing_index_name="source_region_code", - missing_index_value=None, - parameter_code=[ - "losses (own fuel, transport)", - "levelized costs", - "lifetime / amortization period", - # FIXME: add bunker fuel consumption - ], - process_code=list_processes_transport, - index="process_code", - columns="parameter_code", - ) - - # If there are user changes, display them: - display_user_changes() - - def reset_user_changes(): """Reset all user changes.""" if st.session_state["user_changes_df"] is not None: diff --git a/app/tab_input_data.py b/app/tab_input_data.py new file mode 100644 index 00000000..190e21e6 --- /dev/null +++ b/app/tab_input_data.py @@ -0,0 +1,158 @@ +# -*- coding: utf-8 -*- +"""Content of input data tab.""" +import plotly.express as px +import streamlit as st + +from app.ptxboa_functions import display_and_edit_data_table, display_user_changes +from ptxboa.api import PtxboaAPI + + +def content_input_data(api: PtxboaAPI) -> None: + """Create content for the "input data" sheet. + + Parameters + ---------- + api : :class:`~ptxboa.api.PtxboaAPI` + an instance of the api class + + Output + ------ + None + """ + with st.expander("What is this?"): + st.markdown( + """ +**Input data** + +This tab gives you an overview of model input data that is country-specific. +This includes full load hours (FLH) and capital expenditures (CAPEX) +of renewable generation technologies, weighted average cost of capital (WACC), +as well as shipping and pipeline distances to the chosen demand country. +The box plots show median, 1st and 3rd quartile as well as the total spread of values. +They also show the data for your country for comparison. + """ + ) + + st.subheader("Region specific data:") + # get input data: + input_data = api.get_input_data( + st.session_state["scenario"], + user_data=st.session_state["user_changes_df"], + ) + + # filter data: + region_list_without_subregions = ( + api.get_dimension("region") + .loc[api.get_dimension("region")["subregion_code"] == ""] + .index.to_list() + ) + input_data_without_subregions = input_data.loc[ + input_data["source_region_code"].isin(region_list_without_subregions) + ] + + list_data_types = ["CAPEX", "full load hours", "interest rate"] + data_selection = st.radio("Select data type", list_data_types, horizontal=True) + if data_selection == "CAPEX": + parameter_code = ["CAPEX"] + process_code = [ + "Wind Onshore", + "Wind Offshore", + "PV tilted", + "Wind-PV-Hybrid", + ] + x = "process_code" + missing_index_name = "parameter_code" + missing_index_value = "CAPEX" + column_config = {"format": "%.0f USD/kW", "min_value": 0} + + if data_selection == "full load hours": + parameter_code = ["full load hours"] + process_code = [ + "Wind Onshore", + "Wind Offshore", + "PV tilted", + "Wind-PV-Hybrid", + ] + x = "process_code" + missing_index_name = "parameter_code" + missing_index_value = "full load hours" + column_config = {"format": "%.0f h/a", "min_value": 0, "max_value": 8760} + + if data_selection == "interest rate": + parameter_code = ["interest rate"] + process_code = [""] + x = "parameter_code" + column_config = {"format": "%.3f", "min_value": 0, "max_value": 1} + missing_index_name = "parameter_code" + missing_index_value = "interest rate" + + c1, c2 = st.columns(2, gap="medium") + with c2: + # show data: + st.markdown("**Data:**") + df = display_and_edit_data_table( + input_data=input_data_without_subregions, + missing_index_name=missing_index_name, + missing_index_value=missing_index_value, + columns=x, + source_region_code=region_list_without_subregions, + parameter_code=parameter_code, + process_code=process_code, + column_config=column_config, + ) + + with c1: + # create plot: + st.markdown("**Figure:**") + fig = px.box(df) + st.plotly_chart(fig, use_container_width=True) + + st.divider() + st.subheader("Data that is identical for all regions:") + + input_data_global = input_data.loc[input_data["source_region_code"] == ""] + + # filter processes: + processes = api.get_dimension("process") + + list_processes_transport = processes.loc[ + processes["is_transport"], "process_name" + ].to_list() + + list_processes_not_transport = processes.loc[ + ~processes["is_transport"], "process_name" + ].to_list() + st.markdown("**Conversion processes:**") + df = display_and_edit_data_table( + input_data_global, + missing_index_name="source_region_code", + missing_index_value=None, + parameter_code=[ + "CAPEX", + "OPEX (fix)", + "lifetime / amortization period", + "efficiency", + ], + process_code=list_processes_not_transport, + index="process_code", + columns="parameter_code", + ) + st.markdown("**Transportation processes:**") + st.markdown("TODO: fix data") + df = display_and_edit_data_table( + input_data_global, + missing_index_name="source_region_code", + missing_index_value=None, + parameter_code=[ + "losses (own fuel, transport)", + "levelized costs", + "lifetime / amortization period", + # FIXME: add bunker fuel consumption + ], + process_code=list_processes_transport, + index="process_code", + columns="parameter_code", + ) + + # If there are user changes, display them: + display_user_changes() diff --git a/ptxboa_streamlit.py b/ptxboa_streamlit.py index 6ecbcd88..9c6e8adf 100644 --- a/ptxboa_streamlit.py +++ b/ptxboa_streamlit.py @@ -11,6 +11,7 @@ from app.tab_country_fact_sheets import content_country_fact_sheets from app.tab_dashboard import content_dashboard from app.tab_disclaimer import content_disclaimer +from app.tab_input_data import content_input_data from app.tab_literature import content_literature from app.tab_market_scanning import content_market_scanning from app.tab_sustainability import content_sustainability @@ -85,7 +86,7 @@ content_compare_costs(api, res_costs) with t_input_data: - pf.content_input_data(api) + content_input_data(api) with t_deep_dive_countries: pf.content_deep_dive_countries(api, res_costs) From b21517231a3feb8cf2b8c40e94cce5851d050874 Mon Sep 17 00:00:00 2001 From: "j.aschauer" Date: Wed, 15 Nov 2023 18:08:48 +0100 Subject: [PATCH 07/10] move deep dive countries tab to individual module --- app/ptxboa_functions.py | 96 ------------------------------ app/tab_deep_dive_countries.py | 104 +++++++++++++++++++++++++++++++++ ptxboa_streamlit.py | 3 +- 3 files changed, 106 insertions(+), 97 deletions(-) create mode 100644 app/tab_deep_dive_countries.py diff --git a/app/ptxboa_functions.py b/app/ptxboa_functions.py index c76466cb..09562b26 100644 --- a/app/ptxboa_functions.py +++ b/app/ptxboa_functions.py @@ -361,102 +361,6 @@ def remove_subregions(api: PtxboaAPI, df: pd.DataFrame, country_name: str): return df -def content_deep_dive_countries(api: PtxboaAPI, res_costs: pd.DataFrame) -> None: - """Create content for the "costs by region" sheet. - - Parameters - ---------- - api : :class:`~ptxboa.api.PtxboaAPI` - an instance of the api class - res_costs : pd.DataFrame - Results. - - Output - ------ - None - """ - with st.expander("What is this?"): - st.markdown( - """ -**Deep-dive countries: Data on country and regional level** - -For the three deep-dive countries (Argentina, Morocco and South Africa) -this tab shows full load hours of renewable generation and total costs -in regional details. - -The box plots show median, 1st and 3rd quartile as well as the total spread of values. -They also show the data for your selected supply country or region for comparison. - """ - ) - - st.markdown("TODO: add country map") - - ddc = st.radio( - "Select country:", ["Argentina", "Morocco", "South Africa"], horizontal=True - ) - - # get input data: - - input_data = api.get_input_data(st.session_state["scenario"]) - - # filter data: - # get list of subregions: - region_list = ( - api.get_dimension("region") - .loc[api.get_dimension("region")["region_name"].str.startswith(ddc)] - .index.to_list() - ) - - # TODO: implement display of total costs - list_data_types = ["full load hours"] - data_selection = st.radio( - "Select data type", - list_data_types, - horizontal=True, - key="sel_data_ddc", - ) - if data_selection == "full load hours": - parameter_code = ["full load hours"] - process_code = [ - "Wind Onshore", - "Wind Offshore", - "PV tilted", - "Wind-PV-Hybrid", - ] - x = "process_code" - missing_index_name = "parameter_code" - missing_index_value = "full load hours" - column_config = {"format": "%.0f h/a", "min_value": 0, "max_value": 8760} - - if data_selection == "total costs": - df = res_costs.copy() - df = res_costs.loc[region_list].rename({"Total": data_selection}, axis=1) - df = df.rename_axis("source_region_code", axis=0) - x = None - st.markdown("TODO: fix surplus countries in data table") - - c1, c2 = st.columns(2, gap="medium") - with c2: - # show data: - st.markdown("**Data:**") - df = display_and_edit_data_table( - input_data=input_data, - missing_index_name=missing_index_name, - missing_index_value=missing_index_value, - columns=x, - source_region_code=region_list, - parameter_code=parameter_code, - process_code=process_code, - column_config=column_config, - key_suffix="_ddc", - ) - with c1: - # create plot: - st.markdown("**Figure:**") - fig = px.box(df) - st.plotly_chart(fig, use_container_width=True) - - def reset_user_changes(): """Reset all user changes.""" if st.session_state["user_changes_df"] is not None: diff --git a/app/tab_deep_dive_countries.py b/app/tab_deep_dive_countries.py new file mode 100644 index 00000000..b5df4bed --- /dev/null +++ b/app/tab_deep_dive_countries.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +"""Content of input data tab.""" +import pandas as pd +import plotly.express as px +import streamlit as st + +from app.ptxboa_functions import display_and_edit_data_table +from ptxboa.api import PtxboaAPI + + +def content_deep_dive_countries(api: PtxboaAPI, res_costs: pd.DataFrame) -> None: + """Create content for the "costs by region" sheet. + + Parameters + ---------- + api : :class:`~ptxboa.api.PtxboaAPI` + an instance of the api class + res_costs : pd.DataFrame + Results. + + Output + ------ + None + """ + with st.expander("What is this?"): + st.markdown( + """ +**Deep-dive countries: Data on country and regional level** + +For the three deep-dive countries (Argentina, Morocco and South Africa) +this tab shows full load hours of renewable generation and total costs +in regional details. + +The box plots show median, 1st and 3rd quartile as well as the total spread of values. +They also show the data for your selected supply country or region for comparison. + """ + ) + + st.markdown("TODO: add country map") + + ddc = st.radio( + "Select country:", ["Argentina", "Morocco", "South Africa"], horizontal=True + ) + + # get input data: + + input_data = api.get_input_data(st.session_state["scenario"]) + + # filter data: + # get list of subregions: + region_list = ( + api.get_dimension("region") + .loc[api.get_dimension("region")["region_name"].str.startswith(ddc)] + .index.to_list() + ) + + # TODO: implement display of total costs + list_data_types = ["full load hours"] + data_selection = st.radio( + "Select data type", + list_data_types, + horizontal=True, + key="sel_data_ddc", + ) + if data_selection == "full load hours": + parameter_code = ["full load hours"] + process_code = [ + "Wind Onshore", + "Wind Offshore", + "PV tilted", + "Wind-PV-Hybrid", + ] + x = "process_code" + missing_index_name = "parameter_code" + missing_index_value = "full load hours" + column_config = {"format": "%.0f h/a", "min_value": 0, "max_value": 8760} + + if data_selection == "total costs": + df = res_costs.copy() + df = res_costs.loc[region_list].rename({"Total": data_selection}, axis=1) + df = df.rename_axis("source_region_code", axis=0) + x = None + st.markdown("TODO: fix surplus countries in data table") + + c1, c2 = st.columns(2, gap="medium") + with c2: + # show data: + st.markdown("**Data:**") + df = display_and_edit_data_table( + input_data=input_data, + missing_index_name=missing_index_name, + missing_index_value=missing_index_value, + columns=x, + source_region_code=region_list, + parameter_code=parameter_code, + process_code=process_code, + column_config=column_config, + key_suffix="_ddc", + ) + with c1: + # create plot: + st.markdown("**Figure:**") + fig = px.box(df) + st.plotly_chart(fig, use_container_width=True) diff --git a/ptxboa_streamlit.py b/ptxboa_streamlit.py index 9c6e8adf..54ee7219 100644 --- a/ptxboa_streamlit.py +++ b/ptxboa_streamlit.py @@ -10,6 +10,7 @@ from app.tab_compare_costs import content_compare_costs from app.tab_country_fact_sheets import content_country_fact_sheets from app.tab_dashboard import content_dashboard +from app.tab_deep_dive_countries import content_deep_dive_countries from app.tab_disclaimer import content_disclaimer from app.tab_input_data import content_input_data from app.tab_literature import content_literature @@ -89,7 +90,7 @@ content_input_data(api) with t_deep_dive_countries: - pf.content_deep_dive_countries(api, res_costs) + content_deep_dive_countries(api, res_costs) with t_country_fact_sheets: content_country_fact_sheets(cd) From 52010d393bcd64dbbc5b4260fac2783189e54526 Mon Sep 17 00:00:00 2001 From: "j.aschauer" Date: Wed, 15 Nov 2023 18:21:17 +0100 Subject: [PATCH 08/10] use `aggregate_costs()` in `calculate_results_list()` --- app/ptxboa_functions.py | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/app/ptxboa_functions.py b/app/ptxboa_functions.py index 09562b26..863fcfdb 100644 --- a/app/ptxboa_functions.py +++ b/app/ptxboa_functions.py @@ -89,26 +89,11 @@ def calculate_results_list( res_list.append(res_single) res_details = pd.concat(res_list) - # Exclude levelized costs: - res = res_details.loc[res_details["cost_type"] != "LC"] - res = res.pivot_table( - index=parameter_to_change, - columns="process_type", - values="values", - aggfunc="sum", - ) - # calculate total costs: - res["Total"] = res.sum(axis=1) - - return res + return aggregate_costs(res_details) -@st.cache_data() def aggregate_costs(res_details: pd.DataFrame) -> pd.DataFrame: - """Aggregate detailed costs. - - TODO: This function will eventually be replaced by ``calculate_results_list`` - """ + """Aggregate detailed costs.""" # Exclude levelized costs: res = res_details.loc[res_details["cost_type"] != "LC"] res = res.pivot_table( From c9cfc2126d29ededcbbe954aa3304183dff3c6f3 Mon Sep 17 00:00:00 2001 From: "j.aschauer" Date: Wed, 15 Nov 2023 18:34:24 +0100 Subject: [PATCH 09/10] refactor subsetting and reshaping of input data to new function --- app/ptxboa_functions.py | 69 +++++++++++++++++++++++++++++++++++------ 1 file changed, 59 insertions(+), 10 deletions(-) diff --git a/app/ptxboa_functions.py b/app/ptxboa_functions.py index 863fcfdb..d7147b0b 100644 --- a/app/ptxboa_functions.py +++ b/app/ptxboa_functions.py @@ -377,16 +377,16 @@ def display_and_edit_data_table( key_suffix: str = "", ) -> pd.DataFrame: """Display selected input data as 2D table, which can also be edited.""" - # filter data: - df = input_data.copy() - if source_region_code is not None: - df = df.loc[df["source_region_code"].isin(source_region_code)] - if parameter_code is not None: - df = df.loc[df["parameter_code"].isin(parameter_code)] - if process_code is not None: - df = df.loc[df["process_code"].isin(process_code)] - - df_tab = df.pivot_table(index=index, columns=columns, values=values, aggfunc="sum") + # filter data and reshape to wide format. + df_tab = subset_and_pivot_input_data( + input_data, + source_region_code, + parameter_code, + process_code, + index, + columns, + values, + ) # if editing is enabled, store modifications in session_state: if st.session_state["edit_input_data"]: @@ -428,6 +428,55 @@ def display_and_edit_data_table( return df_tab +def subset_and_pivot_input_data( + input_data: pd.DataFrame, + source_region_code: list = None, + parameter_code: list = None, + process_code: list = None, + index: str = "source_region_code", + columns: str = "process_code", + values: str = "value", +): + """ + Reshapes and subsets input data. + + Parameters + ---------- + input_data : pd.DataFrame + obtained with :meth:`~ptxboa.api.PtxboaAPI.get_input_data` + source_region_code : list, optional + list for subsetting source regions, by default None + parameter_code : list, optional + list for subsetting parameter_codes, by default None + process_code : list, optional + list for subsetting process_codes, by default None + index : str, optional + index for `pivot_table()`, by default "source_region_code" + columns : str, optional + column for generating new columns in pivot_table, by default "process_code" + values : str, optional + values for `pivot_table()` , by default "value" + + Returns + ------- + _type_ + _description_ + """ + if source_region_code is not None: + input_data = input_data.loc[ + input_data["source_region_code"].isin(source_region_code) + ] + if parameter_code is not None: + input_data = input_data.loc[input_data["parameter_code"].isin(parameter_code)] + if process_code is not None: + input_data = input_data.loc[input_data["process_code"].isin(process_code)] + + reshaped = input_data.pivot_table( + index=index, columns=columns, values=values, aggfunc="sum" + ) + return reshaped + + def register_user_changes( missing_index_name: str, missing_index_value: str, From da773a95d872bd8b3de8470717c09441bdda3d8b Mon Sep 17 00:00:00 2001 From: "j.aschauer" Date: Wed, 15 Nov 2023 18:52:38 +0100 Subject: [PATCH 10/10] move plot functions to individual module --- app/plot_functions.py | 216 +++++++++++++++++++++++++++++ app/ptxboa_functions.py | 286 ++++++--------------------------------- app/tab_compare_costs.py | 2 +- app/tab_dashboard.py | 6 +- 4 files changed, 256 insertions(+), 254 deletions(-) create mode 100644 app/plot_functions.py diff --git a/app/plot_functions.py b/app/plot_functions.py new file mode 100644 index 00000000..951a3277 --- /dev/null +++ b/app/plot_functions.py @@ -0,0 +1,216 @@ +# -*- coding: utf-8 -*- +"""Functions for plotting input data and results (cost_data).""" +import pandas as pd +import plotly.express as px +import plotly.graph_objects as go +import streamlit as st + +from app.ptxboa_functions import remove_subregions +from ptxboa.api import PtxboaAPI + + +def create_world_map(api: PtxboaAPI, res_costs: pd.DataFrame): + """Create world map.""" + parameter_to_show_on_map = "Total" + + # define title: + title_string = ( + f"{parameter_to_show_on_map} cost of exporting" + f"{st.session_state['chain']} to " + f"{st.session_state['country']}" + ) + # define color scale: + color_scale = [ + (0, st.session_state["colors"][0]), # Starting color at the minimum data value + (0.5, st.session_state["colors"][6]), + (1, st.session_state["colors"][9]), # Ending color at the maximum data value + ] + + # remove subregions from deep dive countries (otherwise colorscale is not correct) + res_costs = remove_subregions(api, res_costs, st.session_state["country"]) + + # Create custom hover text: + custom_hover_data = res_costs.apply( + lambda x: f"{x.name}

" + + "
".join( + [ + f"{col}: {x[col]:.1f}" f"{st.session_state['output_unit']}" + for col in res_costs.columns[:-1] + ] + + [ + f"──────────
{res_costs.columns[-1]}: " + f"{x[res_costs.columns[-1]]:.1f}" + f"{st.session_state['output_unit']}" + ] + ), + axis=1, + ) + + # Create a choropleth world map: + fig = px.choropleth( + locations=res_costs.index, # List of country codes or names + locationmode="country names", # Use country names as locations + color=res_costs[parameter_to_show_on_map], # Color values for the countries + custom_data=[custom_hover_data], # Pass custom data for hover information + color_continuous_scale=color_scale, # Choose a color scale + title=title_string, # set title + ) + + # update layout: + fig.update_geos( + showcountries=True, # Show country borders + showcoastlines=True, # Show coastlines + countrycolor="black", # Set default border color for other countries + countrywidth=0.2, # Set border width + coastlinewidth=0.2, # coastline width + coastlinecolor="black", # coastline color + showland=True, # show land areas + landcolor="#f3f4f5", # Set land color to light gray + oceancolor="#e3e4ea", # Optionally, set ocean color slightly darker gray + showocean=True, # show ocean areas + framewidth=0.2, # width of frame around map + ) + + fig.update_layout( + coloraxis_colorbar={"title": st.session_state["output_unit"]}, # colorbar + height=600, # height of figure + margin={"t": 20, "b": 20, "l": 20, "r": 20}, # reduce margin around figure + ) + + # Set the hover template to use the custom data + fig.update_traces(hovertemplate="%{customdata}") # Custom data + + # Display the map: + st.plotly_chart(fig, use_container_width=True) + return + + +def create_bar_chart_costs(res_costs: pd.DataFrame, current_selection: str = None): + """Create bar plot for costs by components, and dots for total costs. + + Parameters + ---------- + res_costs : pd.DataFrame + data for plotting + settings : dict + settings dictionary, like output from create_sidebar() + current_selection : str + bar to highlight with an arrow. must be an element of res_costs.index + + Output + ------ + fig : plotly.graph_objects.Figure + Figure object + """ + if res_costs.empty: # nodata to plot (FIXME: migth not be required later) + return go.Figure() + + fig = px.bar( + res_costs, + x=res_costs.index, + y=res_costs.columns[:-1], + height=500, + color_discrete_sequence=st.session_state["colors"], + ) + + # Add the dot markers for the "total" column using plotly.graph_objects + scatter_trace = go.Scatter( + x=res_costs.index, + y=res_costs["Total"], + mode="markers+text", # Display markers and text + marker={"size": 10, "color": "black"}, + name="Total", + text=res_costs["Total"].apply( + lambda x: f"{x:.2f}" + ), # Use 'total' column values as text labels + textposition="top center", # Position of the text label above the marker + ) + + fig.add_trace(scatter_trace) + + # add highlight for current selection: + if current_selection is not None and current_selection in res_costs.index: + fig.add_annotation( + x=current_selection, + y=1.2 * res_costs.at[current_selection, "Total"], + text="current selection", + showarrow=True, + arrowhead=2, + arrowsize=1, + arrowwidth=2, + ax=0, + ay=-50, + ) + fig.update_layout( + yaxis_title=st.session_state["output_unit"], + ) + return fig + + +def create_box_plot(res_costs: pd.DataFrame): + """Create a subplot with one row and one column. + + Parameters + ---------- + res_costs : pd.DataFrame + data for plotting + settings : dict + settings dictionary, like output from create_sidebar() + + Output + ------ + fig : plotly.graph_objects.Figure + Figure object + """ + fig = go.Figure() + + # Specify the row index of the data point you want to highlight + highlighted_row_index = st.session_state["region"] + # Extract the value from the specified row and column + + if highlighted_row_index: + highlighted_value = res_costs.at[highlighted_row_index, "Total"] + else: + highlighted_value = 0 + + # Add the box plot to the subplot + fig.add_trace(go.Box(y=res_costs["Total"], name="Cost distribution")) + + # Add a scatter marker for the highlighted data point + fig.add_trace( + go.Scatter( + x=["Cost distribution"], + y=[highlighted_value], + mode="markers", + marker={"size": 10, "color": "black"}, + name=highlighted_row_index, + text=f"Value: {highlighted_value}", # Add a text label + ) + ) + + # Customize the layout as needed + fig.update_layout( + title="Cost distribution for all supply countries", + xaxis={"title": ""}, + yaxis={"title": st.session_state["output_unit"]}, + height=500, + ) + + return fig + + +def create_scatter_plot(df_res, settings: dict): + df_res["Country"] = "Other countries" + df_res.at[st.session_state["region"], "Country"] = st.session_state["region"] + + fig = px.scatter( + df_res, + y="Total", + x="tr_dst_sd", + color="Country", + text=df_res.index, + color_discrete_sequence=["blue", "red"], + ) + fig.update_traces(texttemplate="%{text}", textposition="top center") + st.plotly_chart(fig) + st.write(df_res) diff --git a/app/ptxboa_functions.py b/app/ptxboa_functions.py index d7147b0b..14ddb74d 100644 --- a/app/ptxboa_functions.py +++ b/app/ptxboa_functions.py @@ -2,8 +2,6 @@ """Utility functions for streamlit app.""" import pandas as pd -import plotly.express as px -import plotly.graph_objects as go import streamlit as st from ptxboa.api import PtxboaAPI @@ -105,211 +103,52 @@ def aggregate_costs(res_details: pd.DataFrame) -> pd.DataFrame: return res -def create_world_map(api: PtxboaAPI, res_costs: pd.DataFrame): - """Create world map.""" - parameter_to_show_on_map = "Total" - - # define title: - title_string = ( - f"{parameter_to_show_on_map} cost of exporting" - f"{st.session_state['chain']} to " - f"{st.session_state['country']}" - ) - # define color scale: - color_scale = [ - (0, st.session_state["colors"][0]), # Starting color at the minimum data value - (0.5, st.session_state["colors"][6]), - (1, st.session_state["colors"][9]), # Ending color at the maximum data value - ] - - # remove subregions from deep dive countries (otherwise colorscale is not correct) - res_costs = remove_subregions(api, res_costs, st.session_state["country"]) - - # Create custom hover text: - custom_hover_data = res_costs.apply( - lambda x: f"{x.name}

" - + "
".join( - [ - f"{col}: {x[col]:.1f}" f"{st.session_state['output_unit']}" - for col in res_costs.columns[:-1] - ] - + [ - f"──────────
{res_costs.columns[-1]}: " - f"{x[res_costs.columns[-1]]:.1f}" - f"{st.session_state['output_unit']}" - ] - ), - axis=1, - ) - - # Create a choropleth world map: - fig = px.choropleth( - locations=res_costs.index, # List of country codes or names - locationmode="country names", # Use country names as locations - color=res_costs[parameter_to_show_on_map], # Color values for the countries - custom_data=[custom_hover_data], # Pass custom data for hover information - color_continuous_scale=color_scale, # Choose a color scale - title=title_string, # set title - ) - - # update layout: - fig.update_geos( - showcountries=True, # Show country borders - showcoastlines=True, # Show coastlines - countrycolor="black", # Set default border color for other countries - countrywidth=0.2, # Set border width - coastlinewidth=0.2, # coastline width - coastlinecolor="black", # coastline color - showland=True, # show land areas - landcolor="#f3f4f5", # Set land color to light gray - oceancolor="#e3e4ea", # Optionally, set ocean color slightly darker gray - showocean=True, # show ocean areas - framewidth=0.2, # width of frame around map - ) - - fig.update_layout( - coloraxis_colorbar={"title": st.session_state["output_unit"]}, # colorbar - height=600, # height of figure - margin={"t": 20, "b": 20, "l": 20, "r": 20}, # reduce margin around figure - ) - - # Set the hover template to use the custom data - fig.update_traces(hovertemplate="%{customdata}") # Custom data - - # Display the map: - st.plotly_chart(fig, use_container_width=True) - return - - -def create_bar_chart_costs(res_costs: pd.DataFrame, current_selection: str = None): - """Create bar plot for costs by components, and dots for total costs. - - Parameters - ---------- - res_costs : pd.DataFrame - data for plotting - settings : dict - settings dictionary, like output from create_sidebar() - current_selection : str - bar to highlight with an arrow. must be an element of res_costs.index - - Output - ------ - fig : plotly.graph_objects.Figure - Figure object +def subset_and_pivot_input_data( + input_data: pd.DataFrame, + source_region_code: list = None, + parameter_code: list = None, + process_code: list = None, + index: str = "source_region_code", + columns: str = "process_code", + values: str = "value", +): """ - if res_costs.empty: # nodata to plot (FIXME: migth not be required later) - return go.Figure() - - fig = px.bar( - res_costs, - x=res_costs.index, - y=res_costs.columns[:-1], - height=500, - color_discrete_sequence=st.session_state["colors"], - ) - - # Add the dot markers for the "total" column using plotly.graph_objects - scatter_trace = go.Scatter( - x=res_costs.index, - y=res_costs["Total"], - mode="markers+text", # Display markers and text - marker={"size": 10, "color": "black"}, - name="Total", - text=res_costs["Total"].apply( - lambda x: f"{x:.2f}" - ), # Use 'total' column values as text labels - textposition="top center", # Position of the text label above the marker - ) - - fig.add_trace(scatter_trace) - - # add highlight for current selection: - if current_selection is not None and current_selection in res_costs.index: - fig.add_annotation( - x=current_selection, - y=1.2 * res_costs.at[current_selection, "Total"], - text="current selection", - showarrow=True, - arrowhead=2, - arrowsize=1, - arrowwidth=2, - ax=0, - ay=-50, - ) - fig.update_layout( - yaxis_title=st.session_state["output_unit"], - ) - return fig - - -def create_box_plot(res_costs: pd.DataFrame): - """Create a subplot with one row and one column. + Reshapes and subsets input data. Parameters ---------- - res_costs : pd.DataFrame - data for plotting - settings : dict - settings dictionary, like output from create_sidebar() + input_data : pd.DataFrame + obtained with :meth:`~ptxboa.api.PtxboaAPI.get_input_data` + source_region_code : list, optional + list for subsetting source regions, by default None + parameter_code : list, optional + list for subsetting parameter_codes, by default None + process_code : list, optional + list for subsetting process_codes, by default None + index : str, optional + index for `pivot_table()`, by default "source_region_code" + columns : str, optional + column for generating new columns in pivot_table, by default "process_code" + values : str, optional + values for `pivot_table()` , by default "value" - Output - ------ - fig : plotly.graph_objects.Figure - Figure object + Returns + ------- + : pd.DataFrame """ - fig = go.Figure() - - # Specify the row index of the data point you want to highlight - highlighted_row_index = st.session_state["region"] - # Extract the value from the specified row and column - - if highlighted_row_index: - highlighted_value = res_costs.at[highlighted_row_index, "Total"] - else: - highlighted_value = 0 - - # Add the box plot to the subplot - fig.add_trace(go.Box(y=res_costs["Total"], name="Cost distribution")) - - # Add a scatter marker for the highlighted data point - fig.add_trace( - go.Scatter( - x=["Cost distribution"], - y=[highlighted_value], - mode="markers", - marker={"size": 10, "color": "black"}, - name=highlighted_row_index, - text=f"Value: {highlighted_value}", # Add a text label - ) - ) - - # Customize the layout as needed - fig.update_layout( - title="Cost distribution for all supply countries", - xaxis={"title": ""}, - yaxis={"title": st.session_state["output_unit"]}, - height=500, - ) - - return fig - - -def create_scatter_plot(df_res, settings: dict): - df_res["Country"] = "Other countries" - df_res.at[st.session_state["region"], "Country"] = st.session_state["region"] + if source_region_code is not None: + input_data = input_data.loc[ + input_data["source_region_code"].isin(source_region_code) + ] + if parameter_code is not None: + input_data = input_data.loc[input_data["parameter_code"].isin(parameter_code)] + if process_code is not None: + input_data = input_data.loc[input_data["process_code"].isin(process_code)] - fig = px.scatter( - df_res, - y="Total", - x="tr_dst_sd", - color="Country", - text=df_res.index, - color_discrete_sequence=["blue", "red"], + reshaped = input_data.pivot_table( + index=index, columns=columns, values=values, aggfunc="sum" ) - fig.update_traces(texttemplate="%{text}", textposition="top center") - st.plotly_chart(fig) - st.write(df_res) + return reshaped def remove_subregions(api: PtxboaAPI, df: pd.DataFrame, country_name: str): @@ -428,55 +267,6 @@ def display_and_edit_data_table( return df_tab -def subset_and_pivot_input_data( - input_data: pd.DataFrame, - source_region_code: list = None, - parameter_code: list = None, - process_code: list = None, - index: str = "source_region_code", - columns: str = "process_code", - values: str = "value", -): - """ - Reshapes and subsets input data. - - Parameters - ---------- - input_data : pd.DataFrame - obtained with :meth:`~ptxboa.api.PtxboaAPI.get_input_data` - source_region_code : list, optional - list for subsetting source regions, by default None - parameter_code : list, optional - list for subsetting parameter_codes, by default None - process_code : list, optional - list for subsetting process_codes, by default None - index : str, optional - index for `pivot_table()`, by default "source_region_code" - columns : str, optional - column for generating new columns in pivot_table, by default "process_code" - values : str, optional - values for `pivot_table()` , by default "value" - - Returns - ------- - _type_ - _description_ - """ - if source_region_code is not None: - input_data = input_data.loc[ - input_data["source_region_code"].isin(source_region_code) - ] - if parameter_code is not None: - input_data = input_data.loc[input_data["parameter_code"].isin(parameter_code)] - if process_code is not None: - input_data = input_data.loc[input_data["process_code"].isin(process_code)] - - reshaped = input_data.pivot_table( - index=index, columns=columns, values=values, aggfunc="sum" - ) - return reshaped - - def register_user_changes( missing_index_name: str, missing_index_value: str, diff --git a/app/tab_compare_costs.py b/app/tab_compare_costs.py index 5c64bdfa..34689d54 100644 --- a/app/tab_compare_costs.py +++ b/app/tab_compare_costs.py @@ -3,10 +3,10 @@ import pandas as pd import streamlit as st +from app.plot_functions import create_bar_chart_costs from app.ptxboa_functions import ( calculate_results_list, config_number_columns, - create_bar_chart_costs, remove_subregions, ) from ptxboa.api import PtxboaAPI diff --git a/app/tab_dashboard.py b/app/tab_dashboard.py index e0caa550..8da08689 100644 --- a/app/tab_dashboard.py +++ b/app/tab_dashboard.py @@ -3,11 +3,7 @@ import streamlit as st from plotly.subplots import make_subplots -from app.ptxboa_functions import ( - create_bar_chart_costs, - create_box_plot, - create_world_map, -) +from app.plot_functions import create_bar_chart_costs, create_box_plot, create_world_map def _create_infobox(context_data: dict):