Tobi-ewl's picture
Update app.py
75645cb verified
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="⚠️")