import streamlit as st import pandas as pd import altair as alt from data import default from find_heatingsystem import finde_passende_heizsysteme from calculations_annuity import ( calculate_energiebedarf, calculate_annuity_nk, calculate_annuity_nv, calculate_annuity_nb, calculate_annuity_ns ) import io # Passwortschutz-Block if "auth_ok" not in st.session_state: st.session_state["auth_ok"] = False if not st.session_state["auth_ok"]: st.title("🔒 Zugang geschützt") pw = st.text_input("Bitte Passwort eingeben", type="password") if pw == "KliWinBa25!": st.session_state["auth_ok"] = True st.success("Passwort korrekt! Klicken Sie auf 'Weiter'") if st.button("Weiter"): pass # Seite baut sich automatisch neu auf, App wird ab jetzt dargestellt elif pw != "": st.error("Falsches Passwort.") st.stop() # Funktion zum Laden der Szenario-Excel def load_szenario_data(filepath): df = pd.read_csv(filepath, sep=";", encoding="latin1", skip_blank_lines=True) df = df.dropna(subset=["Szenario"]) df = df.set_index("Szenario") return df PRIMARY = "#004c93" SECONDARY = "#8b3003" BG_LIGHT = "#dfe4f2" HILITE1 = "#c13f1a" ALT1 = "#00386c" ALT2 = "#0069c8" ALT3 = "#0087ff" st.set_page_config(page_title="KliWinBa – Wirtschaftlichkeitsrechner für Heizsysteme", layout="centered") st.markdown(f"""
KliWinBa
Wirtschaftlichkeitsrechner für Heizsysteme
""", unsafe_allow_html=True) alle_heizsysteme = [ "Luft-Wasser Wärmepumpe", "Wasser-Wasser Wärmepumpe", "Sole-Wasser Wärmepumpe", "Pelletheizung", "Holzhackschnitzelheizung", "Wasserstoffheizung", "Gasheizung", "Ölheizung", ] if 'eingabedaten' not in st.session_state: st.session_state.eingabedaten = None if "df_heizsysteme" not in st.session_state: st.session_state.df_heizsysteme = None if "user_values" not in st.session_state: st.session_state.user_values = {} if "heizlast_user" not in st.session_state: st.session_state.heizlast_user = None if "selected_objekt_id" not in st.session_state: st.session_state["selected_objekt_id"] = None if "selected_objekt_id_batch_detail" not in st.session_state: st.session_state["selected_objekt_id_batch_detail"] = None submitted = False if "szenario_bestaetigt" not in st.session_state: st.session_state.szenario_bestaetigt = False if "szenario" not in st.session_state: st.session_state.szenario = None annuitaeten_gesamt = [] if "annuitaeten_gesamt_batch" not in st.session_state: st.session_state["annuitaeten_gesamt_batch"] = [] szenario_df = load_szenario_data('Data/Szenario-Input.csv') szenario_liste = list(szenario_df.index) # Auswahl Manuell/Batch modus = st.radio( "Berechnungsmodus", ("Manuelle Eingabe", "Upload csv-Datei"), horizontal=True, help="Wählen Sie 'Manuelle Eingabe' für die Berechnung eines einzelnen Gebäudes. Wählen Sie 'Upload csv-Datei', um mehrere Gebäude gleichzeitig zu berechnen." ) with st.form("szenario_formular"): st.markdown("### Szenarienauswahl") st.info( """**Hinweis zu den Szenarien:** Im 'Ist-Zustand' entsprechen alle Kostenannahmen und Preise den heutigen. Im 'Mittleren Szenario' wird von einer Erhöhung des CO₂-Preises auf 180 €/t bis zum Jahr 2040 ausgegangen. Im 'Klimaschutz-Szenario' wird von einer Erhöhung des CO₂-Preises auf 300 €/t bis zum Jahr 2040 ausgegangen. Im Szenario 'Niedrige Klimaschutzambitionen' wird der CO₂-Preis auf 45 €/t gedeckelt. """ ) szenario = st.radio( "Szenarienauswahl", szenario_liste, index=None if st.session_state.szenario is None else ( szenario_liste.index(st.session_state.szenario) ), key="radio_szenario" ) szenario_bestaetigen = st.form_submit_button("Szenario bestätigen") if szenario_bestaetigen and not szenario: st.warning("Bitte wählen Sie zuerst ein Szenario aus!", icon="⚠️") if szenario_bestaetigen: st.session_state.szenario_bestaetigt = True st.session_state.szenario = st.session_state.radio_szenario szenario = st.session_state.szenario else: szenario = st.session_state.szenario szenario_map = szenario_df['Kuerzel'].to_dict() szen_kurz = szenario_map.get(szenario, "A") if szenario in szenario_df.index: szen_values = szenario_df.loc[szenario].to_dict() else: st.warning(f"Szenario '{szenario}' nicht gefunden. Standardwerte werden verwendet.") szen_values = {} if modus == "Manuelle Eingabe": if st.session_state.szenario_bestaetigt: with st.container(): st.markdown('''

Grunddaten

''', unsafe_allow_html=True) st.write("") with st.expander("Heizlast-Eingabe (optional)", expanded=False): kenne_heizlast = st.checkbox( "Ich kenne meine Heizlast", value=st.session_state.get("kenne_heizlast", False), key="kenne_heizlast") heizlast_user = st.number_input( "Heizlast (kW)", min_value=1.0, max_value=110.0, value=st.session_state.get("heizlast_user", 10.0), step=0.5, help="Bitte geben Sie hier Ihre bekannte Heizlast in kW ein.", disabled=not st.session_state.kenne_heizlast, key="heizlast_user" ) with st.form("input_form", clear_on_submit=False): col_links, col_rechts = st.columns(2) with col_links: nutzflaeche = st.number_input( "Wohnfläche [m²]", min_value=20.0, max_value=10000.0, value=200.0, step=10.0 ) baujahr = st.number_input( "Baujahr des Gebäudes", min_value=1900, max_value=2025, value=1980 ) gesamtbedarf_input = st.text_input( "Gesamtwärmebedarf [kWh/a]", value="", placeholder="optional", help="Falls bekannt, hier eintragen (dann wird der spezifische Bedarf ignoriert!)" ) gesamtbedarf = None if gesamtbedarf_input.strip() != "": try: gesamtbedarf = float(gesamtbedarf_input.replace(",", ".")) except ValueError: gesamtbedarf = None st.warning("Bitte einen gültigen Zahlenwert für den Gesamtwärmebedarf eintragen.") if gesamtbedarf is not None and gesamtbedarf > 0: spezifisch_disabled = True spezifisch_hint = "Wird ignoriert, weil Gesamtwärmebedarf eingetragen ist." else: spezifisch_disabled = False spezifisch_hint = "" energiebedarf = st.number_input( "Spezifischer Wärmebedarf [kWh/m²a]", min_value=0.0, max_value=500.0, value=150.0, step=5.0, disabled=spezifisch_disabled, help="Hinweis: Wird ignoriert, wenn Gesamtwärmebedarf eingetragen ist!" ) if spezifisch_hint: st.info(spezifisch_hint) st.markdown("
", unsafe_allow_html=True) with col_rechts: with st.expander("⚙️ Erweiterte Einstellungen", expanded=False): preisaenderungsfaktor_emission = float(szen_values['preisaenderungsfaktor_emission']) emission_cost_per_t = float(szen_values['emission_cost']) zinssatz = float(szen_values['zinssatz']) beobachtungszeitraum = int(szen_values['beobachtungszeitraum']) zinssatz_prozent = (zinssatz - 1) * 100 zinssatz_prozent = st.number_input( "Zinssatz [%]", value=zinssatz_prozent, min_value=0.0, max_value=100.0, step=0.1, format="%.1f") zinssatz = 1 + zinssatz_prozent / 100 beobachtungszeitraum = st.number_input( "Beobachtungszeitraum (Jahre)", min_value=5, max_value=40, value=beobachtungszeitraum, help="Nach VDI 2067 wird für Heizsysteme eine Beobachtungsdauer von 20 Jahren angenommen") wachstumsrate_emission = (preisaenderungsfaktor_emission - 1) * 100 wachstumsrate_emission = st.number_input( "Wachstumsrate CO₂-Kosten [%]", value=wachstumsrate_emission, min_value=0.0, max_value=100.0, step=0.1, format="%.1f") preisaenderungsfaktor_emission = 1 + wachstumsrate_emission / 100 emission_cost_per_t = st.number_input( "CO₂-Kosten EUR/t (2025)", value=emission_cost_per_t, min_value=0.0, max_value=1000.0, step=1.0, format="%.1f") emission_cost = emission_cost_per_t / 1000 submitted = st.form_submit_button("Weiter zur Heizsystem-Auswahl") gesamtbedarf = None if gesamtbedarf_input.strip() != "": try: gesamtbedarf = float(gesamtbedarf_input.replace(",", ".")) except ValueError: gesamtbedarf = None st.warning("Bitte einen gültigen Zahlenwert für den Gesamtwärmebedarf eintragen.") if gesamtbedarf is not None and gesamtbedarf > 0 and nutzflaeche > 0: energiebedarf_spezifisch = gesamtbedarf / nutzflaeche else: energiebedarf_spezifisch = energiebedarf st.write(f"In Berechnungen wird verwendet: {energiebedarf_spezifisch:.2f}".replace(".", ",") + " kWh/m²a") if submitted or st.session_state.df_heizsysteme is not None: heizlast_user = st.session_state.heizlast_user if st.session_state.kenne_heizlast else None if submitted or st.session_state.df_heizsysteme is None: try: df = finde_passende_heizsysteme(energiebedarf_spezifisch, baujahr, nutzflaeche, szen_kurz, heizlast_user=heizlast_user) if df.empty: st.error("Keine passenden Heizsysteme gefunden.") st.stop() df.columns = [ "Name", "Leistung", "Investitionskosten", "Betriebsdauer", "Effizienz", "Emissionen", "Preisänderungsfaktor_Inv", "Betriebskosten", "Preisänderungsfaktor_Bedarf", "Fixkosten_O+M", "Preisänderungsfaktor_O+M", "Emissionsänderungsfaktor", "Förderung" ] for col in ["Investitionskosten", "Betriebsdauer", "Effizienz", "Emissionen", "Preisänderungsfaktor_Inv", "Betriebskosten", "Preisänderungsfaktor_Bedarf", "Fixkosten_O+M", "Preisänderungsfaktor_O+M", "Emissionsänderungsfaktor", "Förderung"]: df[col] = df[col].astype(str).str.replace(",", ".").str.replace("|", ".") df[col] = pd.to_numeric(df[col], errors='coerce') st.session_state.eingabedaten = { "nutzflaeche": nutzflaeche, "baujahr": baujahr, "energiebedarf": energiebedarf_spezifisch, "zinssatz": zinssatz, "beobachtungszeitraum": beobachtungszeitraum, "preisaenderungsfaktor_emission": preisaenderungsfaktor_emission, "emission_cost": emission_cost } st.session_state.df_heizsysteme = df.copy() st.session_state.user_values = {} except Exception as e: st.error(f"Fehler bei der Heizungsauswahl: {e}") st.stop() else: df = st.session_state.df_heizsysteme.copy() nutzflaeche = st.session_state.eingabedaten["nutzflaeche"] baujahr = st.session_state.eingabedaten["baujahr"] energiebedarf = st.session_state.eingabedaten["energiebedarf"] zinssatz = st.session_state.eingabedaten["zinssatz"] beobachtungszeitraum = st.session_state.eingabedaten["beobachtungszeitraum"] preisaenderungsfaktor_emission = st.session_state.eingabedaten["preisaenderungsfaktor_emission"] emission_cost = st.session_state.eingabedaten["emission_cost"] st.markdown("## 🔧 Einzelne Werte vor Berechnung anpassen (optional)") benutzerdef_werte = st.checkbox("Benutzerdefinierte Werte je Heizsystem aktivieren", value=False) with st.expander("ℹ️ Erläuterung zu den Betriebskosten-Angaben (Tarife)", expanded=False): st.markdown(""" **Hinweis zu den angesetzten Betriebskosten je Heizsystem:** - **Wärmepumpen:** Für Luft-, Wasser- und Sole-Wärmepumpen wurde ein mittlerer Wärmepumpen-Tarif von **25 ct/kWh** angesetzt (Vergleichsportale wie Verivox, Stand 2025). - **Gasheizung:** Für Erdgas wurden durchschnittliche Privatkundenpreise von **12,28 ct/kWh** herangezogen (destatis, zweites Halbjahr 2024). - **Ölheizung:** Für Heizöl wurde ein Mittelwert von **10,12 ct/kWh** verwendet (Tecson, Jahresmittel 2024/2025). - **Pelletheizung:** Für Holzpellets wurden als Bezug der DEPV und die gängigen regionalen Durchschnittspreise 2024 betrachtet – **5,91 ct/kWh**. - **Holzhackschnitzelheizung:** Für Holzhackschnitzel wurde ein durchschnittlicher Preis von **3,07 ct/kWh** angenommen (Jahresmittelwert 2024 Carmen-ev). - **Wasserstoffheizung:** Hier wurde ein Mittelwert der Prognose der Thüga-Gruppe verwendet mit einem Aufschlag von 10 ct/kWh zu einem Preis von **33,65 ct/kWh**. **Bitte beachten Sie: Durch individuelle Verträge, regionale Unterschiede und Förderungen können tatsächliche Betriebskosten stark abweichen.** """) with st.form("edit_heizsysteme"): user_values = {} for idx, row in df.iterrows(): with st.expander(f"{row['Name']} (Leistung: {row['Leistung']} kW)", expanded=False): c1, c2 = st.columns([1,1]) field_state = not benutzerdef_werte with c1: invest = st.number_input("Investitionskosten (€)", min_value=0.0, value=float(row['Investitionskosten']), step=100.0, key=f"inv_{idx}", disabled=field_state, format="%f") förderung = st.number_input("Förderung (%)", min_value=0.0, value=float(row['Förderung']), step=1.0, key=f"foe_{idx}", disabled=field_state, format="%f", help = "Prozentuale Förderung nach aktuellem GEG. 30 % Grundförderung für EE-Heizungen. 20 % Geschwindigkeitsbonus bei Eigennutzung. 30 % Einkommensbonus (Versteuerndes Jahreseinkommen bis 40.000 €). Zusammen kombinierbar bis 70 %.") betrieb = st.number_input("Betriebsjahre", min_value=1, value=int(row['Betriebsdauer']), key=f"betr_{idx}", disabled=field_state) eff = st.number_input("Effizienz", min_value=0.01, value=float(row['Effizienz']), step=0.05, key=f"eff_{idx}", disabled=field_state) bkosten = st.number_input("Betriebskosten (€/kWh)", value=float(row['Betriebskosten']), step=0.001, key=f"bk_{idx}", disabled=field_state, format="%.3f") with c2: preis_inv = st.number_input("Preisänderungsfaktor Investitionskosten", value=float(row['Preisänderungsfaktor_Inv']), step=0.01, key=f"prinv_{idx}", disabled=field_state) preis_bedarf = st.number_input("Jährlicher Preisänderungsfaktor Betriebskosten", value=float(row['Preisänderungsfaktor_Bedarf']), step=0.01, key=f"prbed_{idx}", disabled=field_state) fix_om = st.number_input("Fixkosten Wartung (€/kW)", value=float(row['Fixkosten_O+M']), step=0.01, key=f"fom_{idx}", disabled=field_state) em_faktor = st.number_input("Emissionsänderungsfaktor", value=float(row['Emissionsänderungsfaktor']), step=0.01, key=f"emfac_{idx}", disabled=field_state) emission = st.number_input("Emissionen (kg CO2/kWh)", value=float(row['Emissionen']), step=0.001, key=f"em_{idx}", disabled=field_state, format="%.3f") user_values[idx] = { "Investitionskosten": invest, "Förderung": förderung, "Betriebsdauer": betrieb, "Effizienz": eff, "Preisänderungsfaktor_Inv": preis_inv, "Betriebskosten": bkosten, "Preisänderungsfaktor_Bedarf": preis_bedarf, "Fixkosten_O+M": fix_om, "Emissionsänderungsfaktor": em_faktor, "Emissionen": emission } st.form_submit_button("Speichern") if benutzerdef_werte: st.session_state.user_values = user_values st.markdown("---") if st.button("Berechnung mit diesen Einstellungen durchführen"): df_berechnung = df.copy() if benutzerdef_werte: values = st.session_state.user_values for idx in df_berechnung.index: for key in values[idx]: df_berechnung.loc[idx, key] = values[idx][key] try: df_berechnung["Energiebedarf"] = df_berechnung.apply(lambda row: calculate_energiebedarf( energiebedarf, nutzflaeche, row["Effizienz"]), axis=1) df_berechnung["Annuität_NK"] = df_berechnung.apply( lambda row: calculate_annuity_nk(row["Förderung"], row["Investitionskosten"], zinssatz, row["Betriebsdauer"], beobachtungszeitraum, row["Preisänderungsfaktor_Inv"]), axis=1) df_berechnung["Annuität_NV"] = df_berechnung.apply( lambda row: calculate_annuity_nv( float(row["Betriebskosten"]) - float(row["Emissionen"]) * emission_cost, row["Energiebedarf"], zinssatz, row["Preisänderungsfaktor_Bedarf"], beobachtungszeitraum, emission_cost, row["Emissionen"], preisaenderungsfaktor_emission, row["Emissionsänderungsfaktor"] ), axis=1) df_berechnung["Annuität_NB"] = df_berechnung.apply( lambda row: calculate_annuity_nb(row["Leistung"], row["Fixkosten_O+M"], row["Preisänderungsfaktor_O+M"], zinssatz, beobachtungszeitraum), axis=1) df_berechnung["Annuität_NS"] = 0 df_berechnung["Annuität"] = df_berechnung["Annuität_NK"] + df_berechnung["Annuität_NV"] + df_berechnung["Annuität_NB"] + df_berechnung["Annuität_NS"] resultat = df_berechnung[["Name", "Annuität"]].sort_values("Annuität") st.subheader("Annualisierte Gesamtkosten nach Heizsystem") st.dataframe(resultat.style.format({"Annuität": lambda x: f"{int(x):,}".replace(",", " ") + " €"}), hide_index=True) df_stacked = df_berechnung[["Name", "Annuität_NK", "Annuität_NV", "Annuität_NB"]].melt( id_vars="Name", value_vars=["Annuität_NK", "Annuität_NV", "Annuität_NB"], var_name="Kostenart", value_name="Wert" ) df_stacked["Kostenart"] = df_stacked["Kostenart"].replace({ "Annuität_NK": "Kapitalgebundene Kosten", "Annuität_NV": "Bedarfsgebundene Kosten", "Annuität_NB": "Betriebsgebundene Kosten" }) kostenart_order = ["Kapitalgebundene Kosten", "Bedarfsgebundene Kosten", "Betriebsgebundene Kosten"] df_stacked["Kostenart"] = pd.Categorical(df_stacked["Kostenart"], categories=kostenart_order, ordered=True) kostenart_sort_map = {k: i for i, k in enumerate(kostenart_order)} df_stacked["Kostenart_Sort"] = df_stacked["Kostenart"].map(kostenart_sort_map) gesamt_sortierung = df_berechnung[["Name", "Annuität"]].sort_values("Annuität", ascending=True) sortierte_names = list(gesamt_sortierung["Name"]) color_order = ["Kapitalgebundene Kosten", "Bedarfsgebundene Kosten", "Betriebsgebundene Kosten"] color_scale = alt.Scale(domain=kostenart_order, range=[ALT1, PRIMARY, ALT2]) stacked_chart = ( alt.Chart(df_stacked) .mark_bar() .encode( x=alt.X("Wert:Q", title="Annualisierte Kosten (€)", stack="zero"), y=alt.Y("Name:N", title="Heizsystem", sort=sortierte_names, axis=alt.Axis(labelLimit=150)), color=alt.Color( "Kostenart:N", scale=color_scale, title="Kostenart", legend=alt.Legend( orient="bottom", direction="horizontal", titleOrient="top", titleAnchor="middle", columns=3, symbolSize=150, labelFontSize=12, titleFontSize=13 ) ), order=alt.Order("Kostenart_Sort:Q", sort="ascending"), tooltip=["Name", "Kostenart", "Wert"] ) .properties( width="container", height=500, title=alt.TitleParams( text="Zusammensetzung der Annualisierten Kosten pro Heizsystem", fontSize=16, anchor="start" ) ) ) st.altair_chart(stacked_chart, use_container_width=True) except Exception as e: st.error(f"Fehler bei der Berechnung: {e}") else: st.info("Bitte Eingaben machen und auf den Button klicken.") elif modus == "Upload csv-Datei": st.info("Bitte Eingaben machen und auf den Button klicken.") tabelle_vorlage = pd.read_csv("Input-Vorlage.csv", sep=";") tabelle_vorlage = tabelle_vorlage[["Objekt-ID", "Wohnflaeche", "Baujahr", "Gesamtwaermebedarf", "spezifischer Waermebedarf"]] outbuf = io.StringIO() tabelle_vorlage.to_csv(outbuf, sep=";", index=False) vorlage_bytes = outbuf.getvalue().encode("utf-8") st.markdown("#### Beispiel-CSV als Vorlage") st.download_button( label="CSV-Vorlage herunterladen", data=vorlage_bytes, file_name="Input-Vorlage.csv", mime="text/csv", help="Diese Vorlage können Sie befüllen und anschließend hochladen. Bitte ändern Sie nicht die Spaltennamen." ) if st.session_state.szenario_bestaetigt: st.caption( "Bitte eine CSV-Datei mit den Spalten 'Objekt-ID', 'Wohnflaeche', 'Baujahr', 'Gesamtwaermebedarf', 'spezifischer Waermebedarf' hochladen." ) uploaded_file = st.file_uploader( "**⚠️ Hinweis:**\n Die Datei wird auf den Hugging-Face-Servern gespeichert. Laden Sie daher keine sensiblen Daten hoch.", type=['csv'] ) with st.expander("⚙️ Erweiterte Einstellungen", expanded=True): preisaenderungsfaktor_emission = float(szen_values.get('preisaenderungsfaktor_emission', 1.0)) emission_cost_per_t = float(szen_values.get('emission_cost', 0.0)) zinssatz = float(szen_values.get('zinssatz', 1.03)) beobachtungszeitraum = int(szen_values.get('beobachtungszeitraum', 20)) zinssatz_prozent = (zinssatz - 1) * 100 zinssatz_prozent = st.number_input( "Zinssatz [%]", value=zinssatz_prozent, min_value=0.0, max_value=100.0, step=0.1, format="%.1f", key="zinssatz_csv") zinssatz = 1 + zinssatz_prozent / 100 beobachtungszeitraum = st.number_input( "Beobachtungszeitraum (Jahre)", min_value=5, max_value=40, value=beobachtungszeitraum, key="beobachtungszeitraum_csv", help="Nach VDI 2067 wird für Heizsysteme eine Beobachtungsdauer von 20 Jahren angenommen") wachstumsrate_emission = (preisaenderungsfaktor_emission - 1) * 100 wachstumsrate_emission = st.number_input( "Wachstumsrate CO₂-Kosten [%]", value=wachstumsrate_emission, min_value=0.0, max_value=100.0, step=0.1, format="%.1f", key="wachstumsrate_emission_csv") preisaenderungsfaktor_emission = 1 + wachstumsrate_emission / 100 emission_cost_per_t = st.number_input( "CO₂-Kosten EUR/t (2025)", value=emission_cost_per_t, min_value=0.0, max_value=1000.0, step=1.0, format="%.1f", key="emission_cost_csv") emission_cost = emission_cost_per_t / 1000 if uploaded_file: df_input = pd.read_csv(uploaded_file, sep=";", dtype={"Objekt-ID": str}) st.write("Vorschau (erste Zeilen):", df_input.head()) #Hilfsfunktion, damit auch Kommazahlen eingelesen werden können def parsefloat(cell): """Robuste Umwandlung beliebiger Zellen aus CSV in float, auch für , als Dezimaltrennzeichen.""" try: if pd.isnull(cell) or (isinstance(cell, str) and not cell.strip()): return None return float(str(cell).replace(",", ".")) except Exception: return None # Für die globale Anpassung Vorschau der Heizsysteme mit ersten Objekt row0 = df_input.iloc[0] try: nutzflaeche0 = parsefloat(row0["Wohnflaeche"]) baujahr0 = int(row0["Baujahr"]) gesamtbedarf0 = parsefloat(row0.get("Gesamtwaermebedarf", "")) spezbedarf0 = parsefloat(row0.get("spezifischer Waermebedarf", "")) if gesamtbedarf0 is not None and gesamtbedarf0 > 0 and nutzflaeche0 and nutzflaeche0 > 0: energiebedarf_spezifisch0 = gesamtbedarf0 / nutzflaeche0 elif spezbedarf0 is not None and spezbedarf0 > 0: energiebedarf_spezifisch0 = spezbedarf0 else: energiebedarf_spezifisch0 = None preview_df = finde_passende_heizsysteme(energiebedarf_spezifisch0, baujahr0, nutzflaeche0, szen_kurz) preview_df.columns = [ "Name", "Leistung", "Investitionskosten", "Betriebsdauer", "Effizienz", "Emissionen", "Preisänderungsfaktor_Inv", "Betriebskosten", "Preisänderungsfaktor_Bedarf", "Fixkosten_O+M", "Preisänderungsfaktor_O+M", "Emissionsänderungsfaktor", "Förderung" ] for col in ["Investitionskosten", "Betriebsdauer", "Effizienz", "Emissionen", "Preisänderungsfaktor_Inv", "Betriebskosten", "Preisänderungsfaktor_Bedarf", "Fixkosten_O+M", "Preisänderungsfaktor_O+M", "Emissionsänderungsfaktor", "Förderung"]: preview_df[col] = preview_df[col].astype(str).str.replace(",", ".").str.replace("|", ".") preview_df[col] = pd.to_numeric(preview_df[col], errors='coerce') except Exception as e: st.error(f"Fehler bei Heizsystem-Auswahl-Vorschau: {e}") preview_df = pd.DataFrame() # --------------------------- Globale Anpassung nur für system-spezifische Werte benutzerdef_werte_batch = st.checkbox("Globale Heizsystem-Werte (system-spezifisch) anpassen", value=False) with st.expander("ℹ️ Erläuterung zu den Betriebskosten-Angaben (Tarife)", expanded=False): st.markdown(""" **Hinweis zu den angesetzten Betriebskosten je Heizsystem:** - **Wärmepumpen:** Für Luft-, Wasser- und Sole-Wärmepumpen wurde ein mittlerer Wärmepumpen-Tarif von **25 ct/kWh** angesetzt (Vergleichsportale wie Verivox, Stand 2025). - **Gasheizung:** Für Erdgas wurden durchschnittliche Privatkundenpreise von **12,28 ct/kWh** herangezogen (destatis, zweites Halbjahr 2024). - **Ölheizung:** Für Heizöl wurde ein Mittelwert von **10,12 ct/kWh** verwendet (Tecson, Jahresmittel 2024/2025). - **Pelletheizung:** Für Holzpellets wurden als Bezug der DEPV und die gängigen regionalen Durchschnittspreise 2024 betrachtet – **5,91 ct/kWh**. - **Holzhackschnitzelheizung:** Für Holzhackschnitzel wurde ein durchschnittlicher Preis von **3,07 ct/kWh** angenommen (Jahresmittelwert 2024 Carmen-ev). - **Wasserstoffheizung:** Hier wurde ein Mittelwert der Prognose der Thüga-Gruppe verwendet mit einem Aufschlag von 10 ct/kWh zu einem Preis von **33,65 ct/kWh**. **Bitte beachten Sie: Durch individuelle Verträge, regionale Unterschiede und Förderungen können tatsächliche Betriebskosten stark abweichen.** """) batch_user_values = {} # Nur die system-spezifischen Felder! if benutzerdef_werte_batch and not preview_df.empty: with st.form("batch_edit_heizsysteme"): for idx, row in preview_df.iterrows(): with st.expander(f"{row['Name']}", expanded=False): förderung = st.number_input("Förderung (%)", min_value=0.0, value=float(row['Förderung']), step=1.0, key=f"bfoe_{idx}", format="%f", help = "Prozentuale Förderung nach aktuellem GEG. 30 % Grundförderung für EE-Heizungen. 20 % Geschwindigkeitsbonus bei Eigennutzung. 30 % Einkommensbonus (Versteuerndes Jahreseinkommen bis 40.000 €). Zusammen kombinierbar bis 70 %.") bkosten = st.number_input("Betriebskosten (€/kWh)", value=float(row['Betriebskosten']), step=0.001, key=f"bbk_{idx}", format="%.3f") preis_bedarf = st.number_input("Jährlicher Preisänderungsfaktor Betriebskosten", value=float(row['Preisänderungsfaktor_Bedarf']), step=0.01, key=f"bprbed_{idx}") emission = st.number_input("Emissionen (kg CO2/kWh)", value=float(row['Emissionen']), step=0.001, key=f"bem_{idx}", format="%.3f") batch_user_values[idx] = { "Förderung": förderung, "Betriebskosten": bkosten, "Preisänderungsfaktor_Bedarf": preis_bedarf, "Emissionen": emission } st.form_submit_button("Globale Heizsystem-Werte speichern") if benutzerdef_werte_batch: st.session_state.batch_user_values = batch_user_values else: st.session_state.batch_user_values = None do_calc = st.button("Batch-Berechnung starten") if do_calc: try: df_out = df_input.copy() df_out["Guenstigste Alternative"] = None for sys in alle_heizsysteme: df_out[sys] = None annuitaeten_gesamt = [] for idx, row in df_input.iterrows(): try: nutzflaeche = parsefloat(row["Wohnflaeche"]) baujahr = int(row["Baujahr"]) gesamtbedarf = parsefloat(row.get("Gesamtwaermebedarf", "")) spezbedarf = parsefloat(row.get("spezifischer Waermebedarf", "")) if gesamtbedarf is not None and gesamtbedarf > 0 and nutzflaeche and nutzflaeche > 0: energiebedarf_spezifisch = gesamtbedarf / nutzflaeche elif spezbedarf is not None and spezbedarf > 0: energiebedarf_spezifisch = spezbedarf else: energiebedarf_spezifisch = None df = finde_passende_heizsysteme(energiebedarf_spezifisch, baujahr, nutzflaeche, szen_kurz) df.columns = [ "Name", "Leistung", "Investitionskosten", "Betriebsdauer", "Effizienz", "Emissionen", "Preisänderungsfaktor_Inv", "Betriebskosten", "Preisänderungsfaktor_Bedarf", "Fixkosten_O+M", "Preisänderungsfaktor_O+M", "Emissionsänderungsfaktor", "Förderung" ] for col in ["Investitionskosten", "Betriebsdauer", "Effizienz", "Emissionen", "Preisänderungsfaktor_Inv", "Betriebskosten", "Preisänderungsfaktor_Bedarf", "Fixkosten_O+M", "Preisänderungsfaktor_O+M", "Emissionsänderungsfaktor", "Förderung"]: df[col] = df[col].astype(str).str.replace(",", ".").str.replace("|", ".") df[col] = pd.to_numeric(df[col], errors='coerce') # Wenn Batch-global-Werte gesetzt sind, anwenden: batch_user_vals = st.session_state.get("batch_user_values") if batch_user_vals is not None: # Mapping via Name zur Sicherheit preview_heizsys_names = {preview_df.loc[x, "Name"]: x for x in preview_df.index} for pidx in batch_user_vals: sysname = preview_df.loc[pidx, "Name"] # Suche nach dem Namen im aktuellen DF df_idx = df[df["Name"] == sysname].index if not df_idx.empty: for key in batch_user_vals[pidx]: df.at[df_idx[0], key] = batch_user_vals[pidx][key] energiebedarf = energiebedarf_spezifisch df["Energiebedarf"] = df.apply(lambda rowh: calculate_energiebedarf( energiebedarf, nutzflaeche, rowh["Effizienz"]), axis=1) df["Annuität_NK"] = df.apply( lambda rowh: calculate_annuity_nk(rowh["Förderung"], rowh["Investitionskosten"], zinssatz, rowh["Betriebsdauer"], beobachtungszeitraum, rowh["Preisänderungsfaktor_Inv"]), axis=1) df["Annuität_NV"] = df.apply( lambda rowh: calculate_annuity_nv( float(rowh["Betriebskosten"]) - float(rowh["Emissionen"]) * emission_cost, rowh["Energiebedarf"], zinssatz, rowh["Preisänderungsfaktor_Bedarf"], beobachtungszeitraum, emission_cost, rowh["Emissionen"], preisaenderungsfaktor_emission, rowh["Emissionsänderungsfaktor"] ), axis=1) df["Annuität_NB"] = df.apply( lambda rowh: calculate_annuity_nb(rowh["Leistung"], rowh["Fixkosten_O+M"], rowh["Preisänderungsfaktor_O+M"], zinssatz, beobachtungszeitraum), axis=1) df["Annuität_NS"] = 0 df["Annuität"] = df["Annuität_NK"] + df["Annuität_NV"] + df["Annuität_NB"] + df["Annuität_NS"] ann_dict = dict(zip(df["Name"], df["Annuität"])) guenstigstes_sys = min(ann_dict.items(), key=lambda x: x[1])[0] if ann_dict else "" df_out.at[idx, "Guenstigste Alternative"] = guenstigstes_sys for sys in alle_heizsysteme: wert = ann_dict.get(sys, None) df_out.at[idx, sys] = wert # df = df.sort_values("Annuität") df_anni = df[[ "Name", "Annuität_NK", "Annuität_NV", "Annuität_NB", "Annuität" ]].copy() df_anni["Objekt-ID"] = row.get("Objekt-ID", idx) annuitaeten_gesamt.append(df_anni) # for j in range(min(len(df), max_count)): # df_out.at[idx, annuität_colnames[j]] = int(df.iloc[j]["Annuität"]) # df_out.at[idx, hsystem_colnames[j]] = df.iloc[j]["Name"] except Exception as e: st.warning(f"Objekt-ID {row.get('Objekt-ID', idx)}: {e}") heizsystem_namensmap = { "Ölheizung": "Oelheizung", "Luft-Wasser Wärmepumpe": "Luft-Wasser Waermepumpe", "Sole-Wasser Wärmepumpe": "Sole-Wasser Waermepumpe", "Wasser-Wasser Wärmepumpe": "Wasser-Wasser Waermepumpe", } # Klarnamen-Liste aus Mapping generieren heizsysteme_klar = [heizsystem_namensmap.get(hs, hs) for hs in alle_heizsysteme] spalten_anni = [f"Annuitaet {name}" for name in heizsysteme_klar] csv_map = {name: spalte for name, spalte in zip(heizsysteme_klar, spalten_anni)} # Schritt 1: Spalten korrekt umbenennen df_out = df_out.rename(columns=heizsystem_namensmap) df_out = df_out.rename(columns=csv_map) df_out["Guenstigste Alternative"] = df_out["Guenstigste Alternative"].replace(heizsystem_namensmap) # Schritt 2: Format-Wandlung nur auf Annuität-Spalten def format_eur(x): try: if pd.isnull(x): return "" return "{:.2f}".format(float(x)).replace(".", ",") except Exception: return str(x) for spalte in spalten_anni: if spalte in df_out.columns: df_out[spalte] = df_out[spalte].apply(format_eur) st.success("Berechnung abgeschlossen!") st.session_state["annuitaeten_gesamt_batch"] = annuitaeten_gesamt if annuitaeten_gesamt: # Verbinde alle Einzel-DFs zu einem großen Table df_alle = pd.concat(annuitaeten_gesamt, ignore_index=True) # Mittelwerte pro System df_mittel = df_alle.groupby("Name")[["Annuität_NK", "Annuität_NV", "Annuität_NB", "Annuität"]].mean(numeric_only=True).reset_index() st.session_state["df_mittel"] = df_mittel st.session_state["annuitaeten_gesamt_batch"] = annuitaeten_gesamt st.session_state["df_out"] = df_out except Exception as e: st.error(f"Fehler beim Einlesen/Berechnen: {e}") if "df_out" in st.session_state and st.session_state["df_out"] is not None: csv_buffer = io.StringIO() st.session_state["df_out"].to_csv(csv_buffer, sep=";", index=False) csv_bytes = csv_buffer.getvalue().encode("utf-8") st.download_button( "Download Ergebnis-CSV", csv_bytes, file_name="heizsysteme_batch_ergebnis.csv", mime="text/csv" ) # --- Häufigkeit der günstigsten Technologie als Säulendiagramm ---------- if "df_out" in st.session_state and st.session_state["df_out"] is not None: # Count cheapest alternatives freq = ( st.session_state["df_out"]["Guenstigste Alternative"] .value_counts() .rename_axis("Heizsystem") .reset_index(name="Anzahl") ) # Sort bars descending freq = freq.sort_values("Anzahl", ascending=False) # Bar chart with Altair bar_chart = ( alt.Chart(freq) .mark_bar() .encode( x=alt.X("Heizsystem:N", sort=freq["Heizsystem"].tolist(), title="günstigste Alternative"), y=alt.Y("Anzahl:Q", title="Häufigkeit"), tooltip=["Heizsystem", "Anzahl"] ) .properties( width="container", height=400, title=alt.TitleParams( text="Häufigkeit der günstigsten Heizsystem-Alternative", fontSize=16, anchor="start" ) ) ) st.altair_chart(bar_chart, use_container_width=True) if "df_mittel" in st.session_state and st.session_state["df_mittel"] is not None: df_mittel = st.session_state["df_mittel"] df_stacked = df_mittel.melt( id_vars="Name", value_vars=["Annuität_NK", "Annuität_NV", "Annuität_NB"], var_name="Kostenart", value_name="Wert" ) df_stacked["Kostenart"] = df_stacked["Kostenart"].replace({ "Annuität_NK": "Kapitalgebundene Kosten", "Annuität_NV": "Bedarfsgebundene Kosten", "Annuität_NB": "Betriebsgebundene Kosten" }) kostenart_order = ["Kapitalgebundene Kosten", "Bedarfsgebundene Kosten", "Betriebsgebundene Kosten"] df_stacked["Kostenart"] = pd.Categorical(df_stacked["Kostenart"], categories=kostenart_order, ordered=True) color_scale = alt.Scale(domain=kostenart_order, range=["#00386c", "#004c93", "#0069c8"]) sortierte_names = df_mittel.sort_values("Annuität")["Name"] st.markdown("### Mittlere annualisierte Kosten pro Heizsystem (Batch-Durchschnitt)") st.altair_chart( (alt.Chart(df_stacked) .mark_bar() .encode( x=alt.X("Wert:Q", title="mittlere annualisierte Kosten (€)", stack="zero"), y=alt.Y("Name:N", title="Heizsystem", sort=list(sortierte_names)), color=alt.Color("Kostenart:N", scale=color_scale, title="Kostenart", legend=alt.Legend( orient="bottom", direction="horizontal", titleOrient="top", titleAnchor="middle", columns=3, symbolSize=150, labelFontSize=12, titleFontSize=13 ) ), order=alt.Order("Kostenart_Sort:Q", sort="ascending"), tooltip=["Name", "Kostenart", "Wert"]) .properties( width="container", height=500, title=alt.TitleParams( text="Zusammensetzung der Annualisierten Kosten pro Heizsystem", fontSize=16, anchor="start" ) )), use_container_width=True ) if ( "annuitaeten_gesamt_batch" in st.session_state and st.session_state["annuitaeten_gesamt_batch"] ): st.markdown("### Einzelobjekte interaktiv anzeigen") df_alle = pd.concat(st.session_state["annuitaeten_gesamt_batch"], ignore_index=True) objekt_ids = df_alle["Objekt-ID"].unique() selected_objekt_id = st.selectbox( "Objekt-ID wählen für Einzelanzeige:", objekt_ids, key="objektid_einzelauswahl" ) df_objekt = df_alle[df_alle["Objekt-ID"] == selected_objekt_id].copy() if not df_objekt.empty: df_stacked_obj = df_objekt[["Name", "Annuität_NK", "Annuität_NV", "Annuität_NB"]].melt( id_vars="Name", value_vars=["Annuität_NK", "Annuität_NV", "Annuität_NB"], var_name="Kostenart", value_name="Wert" ) df_stacked_obj["Kostenart"] = df_stacked_obj["Kostenart"].replace({ "Annuität_NK": "Kapitalgebundene Kosten", "Annuität_NV": "Bedarfsgebundene Kosten", "Annuität_NB": "Betriebsgebundene Kosten" }) kostenart_order = ["Kapitalgebundene Kosten", "Bedarfsgebundene Kosten", "Betriebsgebundene Kosten"] df_stacked_obj["Kostenart"] = pd.Categorical(df_stacked_obj["Kostenart"], categories=kostenart_order, ordered=True) kostenart_sort_map = {k: i for i, k in enumerate(kostenart_order)} df_stacked_obj["Kostenart_Sort"] = df_stacked_obj["Kostenart"].map(kostenart_sort_map) gesamt_sortierung = df_objekt[["Name", "Annuität"]].sort_values("Annuität", ascending=True) sortierte_names = list(gesamt_sortierung["Name"]) color_scale = alt.Scale(domain=kostenart_order, range=[ALT1, PRIMARY, ALT2]) stacked_chart_obj = ( alt.Chart(df_stacked_obj) .mark_bar() .encode( x=alt.X("Wert:Q", title="Annualisierte Kosten (€)", stack="zero"), y=alt.Y("Name:N", title="Heizsystem", sort=sortierte_names, axis=alt.Axis(labelLimit=150)), color=alt.Color( "Kostenart:N", scale=color_scale, title="Kostenart", legend=alt.Legend( orient="bottom", direction="horizontal", titleOrient="top", titleAnchor="middle", columns=3, symbolSize=150, labelFontSize=12, titleFontSize=13 ) ), order=alt.Order("Kostenart_Sort:Q", sort="ascending"), tooltip=["Name", "Kostenart", "Wert"] ) .properties( width="container", height=500, title=alt.TitleParams( text=f"Kostenaufteilung für Objekt-ID {selected_objekt_id}", fontSize=16, anchor="start" ) ) ) st.altair_chart(stacked_chart_obj, use_container_width=True) else: st.warning("Keine Daten für diese Objekt-ID.") st.markdown("---") st.caption("Berechnung nach VDI 2067, Heizlastberechnung gemäß DIN EN 15378.") else: st.warning("Bitte laden Sie eine CSV-Datei hoch", icon="⚠️")