Spaces:
Sleeping
Sleeping
| 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""" | |
| <style> | |
| html, body, [class*="css"] {{ | |
| font-family: Optima, 'Optima', 'Segoe UI', 'Arial', 'sans-serif' !important; | |
| }} | |
| .kliwinba-header {{ | |
| font-family: Optima, 'Optima', 'Segoe UI', 'Arial', 'sans-serif'; | |
| font-size: 36px; | |
| font-weight: 700; | |
| padding: 1.5rem 1rem 1rem 1rem; | |
| color: white; | |
| background: {PRIMARY}; | |
| border-radius: 10px; | |
| letter-spacing: 1px; | |
| margin-bottom: 1.0rem; | |
| text-align: center; | |
| }} | |
| .kliwinba-header .subline {{ | |
| display: block; | |
| font-size: 28px; | |
| font-weight: 400; | |
| margin-top: 0.4rem; | |
| }} | |
| .block-container {{ | |
| background: {BG_LIGHT}; | |
| }} | |
| h2, .stMarkdown h2 {{ | |
| color: {SECONDARY}; | |
| font-family: Optima, 'Optima', 'Segoe UI', 'Arial', 'sans-serif'; | |
| }} | |
| </style> | |
| <div class="kliwinba-header"> | |
| KliWinBa<br> | |
| <span class="subline">Wirtschaftlichkeitsrechner für Heizsysteme</span> | |
| </div> | |
| """, 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(''' | |
| <div style="background-color: white; padding: 1.5em; border-radius: 10px; box-shadow: 0 0 6px rgba(0,0,0,0.1);border: 1px solid #004c93;"> | |
| <h4 style="color:#004c93; margin-top:0; margin-bottom:1rem;">Grunddaten</h4> | |
| ''', 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("</div>", 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="⚠️") |