import os, re, sys, unicodedata, subprocess from pathlib import Path import pandas as pd import plotly.graph_objs as go from collections import defaultdict from config_lms import plotly_theme, export_fig_visual, export_fig_png from ci_template.plotly_template import ( get_standard_layout, get_colors, set_theme, plot_table_from_dict, plot_table_from_dataframe ) set_theme(plotly_theme) # --- Hilfsfunktionen für Export und Slugify --- def slugify(text): text = unicodedata.normalize('NFKD', text) text = text.encode('ascii', 'ignore').decode('ascii') text = re.sub(r'[^\w\s-]', '', text) text = re.sub(r'[\s]+', '-', text) return text.strip().lower() def export_figure(fig, name, export_flag_html, export_flag_png): filename_part = "notsan-aprv-vergleich" safe_filename = slugify(f"{name}_{filename_part}") remote_path = "jochen-hanisch@sternenflottenakademie.local:/mnt/deep-space-nine/public/plot/promotion/" if export_flag_html: export_path_html = os.path.join("..", "Allgemein beruflich", "Research", "Forschungsprojekte", "Systemische Kompetenzentwicklung für High Responsibility Teams", f"{safe_filename}.html") fig.write_html(export_path_html, full_html=True, include_plotlyjs="cdn") try: subprocess.run(["scp", export_path_html, remote_path], check=True) print(f"✅ HTML-Datei '{export_path_html}' erfolgreich übertragen.") os.remove(export_path_html) print(f"🗑️ Lokale HTML-Datei '{export_path_html}' wurde gelöscht.") except subprocess.CalledProcessError as e: print("❌ Fehler beim HTML-Übertragen:") print(e.stderr) if export_flag_png: export_path_png = os.path.join("..", "Allgemein beruflich", "Research", "Forschungsprojekte", "Systemische Kompetenzentwicklung für High Responsibility Teams", f"{safe_filename}.png") try: fig.write_image(export_path_png, width=1200, height=800, scale=2) print(f"✅ PNG-Datei lokal gespeichert: '{export_path_png}'") except Exception as e: print("❌ Fehler beim PNG-Export:", str(e)) # Ursprüngliche Datentabelle (bitte bei Bedarf anpassen) df = pd.DataFrame({ "Kursbezeichnung": [ "NFS-H-01", "NFS-H-02", "NFS-H-03", "NFS-H-04", "NFS-H-05", "NFS-H-06", "NFS-H-07", "NFS-H-08", "NFS-H-09", "NFS-H-10", "NFS-H-11", "NFS-H-12", "NFS-H-13", "NFS-H-14", "NFS-H-15", "NFS-H-16", "NFS-H-17", "NFS-H-18", "NFS-H-19", "NFS-H-20", "NFS-H-21", "NFS-H-22", "NFS-H-23", "NFS-H-24", "NFS-H-25", "NFS-H-26", "NFS-H-27", "NFS-H-28", "NFS-H-29", "NFS-H-30", "NFS-H-31", "NFS-H-32" ], "Titel": [ "Einführung in die berufliche Ausbildung", "Das eigene Berufsfeld erkunden und berufliches Selbstverständnis entwickeln", "Die eigene Lehrrettungswache erleben", "Das rettungsdienstliche Umfeld kennen lernen", "Mit sich selbst und Anderen umgehen", "Einen Patienten im Krankentransport beurteilen", "Lebensrettende Maßnahmen durchführen", "Eine Einsatzfahrt durchführen", "Einen Krankentransport durchführen", "Mit BOS-Peers kommunizieren", "Mit unterschiedlichen Patienten-Peers kommunizieren", "Beratungsgespräche führen", "Mit Sterben im Rettungsdienst umgehen", "Einen Einsatz in der primären Notfallmedizin durchführen", "Einen Notfallpatienten beurteilen", "Patienten mit A-Problem behandeln", "Patienten mit B-Problem behandeln", "Patienten mit C-Problem behandeln", "Patienten mit D-Problem behandeln", "Patienten mit E-Problem behandeln", "Einen pädiatrischen Patienten behandeln", "Einen Einsatz in der Sekundärrettung durchführen", "Urologische und nephrologische Notfälle versorgen", "Hals-Nasen-Ohren Erkrankungen und Verletzungen versorgen", "Mit psychischen Erkrankungen und Notfällen umgehen", "Eine gynäkologische Patientin versorgen", "Notfälle der Wasserrettung versorgen", "Einsätze mit besonderen Verhaltensmaßnahmen durchführen", "Einsätze mit besonderer Logistik durchführen", "Mit BOS zusammenarbeiten", "Das eigene Berufsfeld reflektieren", "Vorbereitung auf die Notfallsanitäterprüfung" ], "∑ Aufgaben": [ 6, 16, 28, 37, 31, 14, 47, 36, 11, 25, 12, 18, 15, 18, 24, 26, 24, 36, 20, 57, 15, 11, 17, 19, 11, 22, 9, 61, 55, 13, 54, 60 ], "Dauer [d]": [ 7, 32, 17, 31, 53, 15, 67, 77, 13, 42, 2, 7, 4, 20, 31, 48, 7, 12, 29, 90, 2, 53, 5, 2, 42, 18, 2, 93, 100, 5, 5, 40 ] }) # Zentrale Definition der Themenbereiche THEMENBEREICHE = ["medizinisch", "rettungsdienstlich", "bezugswissenschaftlich", "Einführung/Prüfung"] # --- Automatische Berechnung Themenbereichsanteile pro Kurs --- # Ausschließlich systematische APrV-Zuordnung aus lms-verteilung.xlsx und zuordnung_praezise verwenden zuordnung_praezise = { # Medizinisch "1a": "medizinisch", "1b": "medizinisch", "1c": "medizinisch", "1d": "rettungsdienstlich", "1e": "medizinisch", "1f": "medizinisch", "7a": "medizinisch", "7b": "medizinisch", "7c": "medizinisch", "7d": "medizinisch", "7e": "medizinisch", "7f": "medizinisch", "7g": "medizinisch", "7h": "medizinisch", "7i": "medizinisch", # Rettungsdienstlich "2a": "rettungsdienstlich", "2b": "rettungsdienstlich", "2c": "rettungsdienstlich", "2d": "rettungsdienstlich", "2e": "rettungsdienstlich", "2f": "rettungsdienstlich", "2g": "rettungsdienstlich", "2h": "rettungsdienstlich", "4a": "rettungsdienstlich", "4b": "rettungsdienstlich", "4c": "rettungsdienstlich", "5a": "rettungsdienstlich", "5b": "rettungsdienstlich", "5c": "rettungsdienstlich", "5d": "rettungsdienstlich", "5e": "rettungsdienstlich", # Bezugswissenschaftlich "3a": "bezugswissenschaftlich", "3b": "bezugswissenschaftlich", "3c": "bezugswissenschaftlich", "3d": "bezugswissenschaftlich", "3e": "bezugswissenschaftlich", "6a": "bezugswissenschaftlich", "6b": "bezugswissenschaftlich", "6c": "bezugswissenschaftlich", "6d": "bezugswissenschaftlich", "8a": "bezugswissenschaftlich", "8b": "bezugswissenschaftlich", "8c": "bezugswissenschaftlich", "8d": "bezugswissenschaftlich", "9a": "bezugswissenschaftlich", "9b": "bezugswissenschaftlich", "9c": "bezugswissenschaftlich", "9d": "bezugswissenschaftlich", "9e": "bezugswissenschaftlich", "10a": "bezugswissenschaftlich", "10b": "bezugswissenschaftlich", "10c": "bezugswissenschaftlich", "10d": "bezugswissenschaftlich" } # Systematische Zuordnung aus externer Excel df_para = pd.read_excel(Path(__file__).parent / "lms-verteilung.xlsx") df_para = df_para.set_index("Nummer") anteile_liste = [] for idx, row in df.iterrows(): kursnummer = int(row["Kursbezeichnung"].split("-")[-1]) if kursnummer not in df_para.index: # Keine manuelle Kurs-Zuordnung mehr, ausschließlich systematische APrV-Zuordnung d = "unbekannt" print(f"❗ Kurs {kursnummer} ({row['Titel']}) hat unbekannten Themenbereich.") print(f" → Verwendete APrV-Kürzel: {[ ]}") anteile_liste.append((0, 0, 0, "unbekannt")) continue else: para_raw = df_para.loc[kursnummer, "NotSan-APrV"] if pd.isna(para_raw): d = "unbekannt" print(f"❗ Kurs {kursnummer} ({row['Titel']}) hat unbekannten Themenbereich.") print(f" → Verwendete APrV-Kürzel: {[ ]}") anteile_liste.append((0, 0, 0, "unbekannt")) continue else: para_liste = [p.strip() for p in str(para_raw).split(",")] zähler = defaultdict(int) gesamt = 0 for para in para_liste: bereich = zuordnung_praezise.get(para) if bereich: zähler[bereich] += 1 gesamt += 1 m = round(zähler["medizinisch"] / gesamt * 100, 2) if gesamt else 0 r = round(zähler["rettungsdienstlich"] / gesamt * 100, 2) if gesamt else 0 b = round(zähler["bezugswissenschaftlich"] / gesamt * 100, 2) if gesamt else 0 d = max(("medizinisch", m), ("rettungsdienstlich", r), ("bezugswissenschaftlich", b), key=lambda x: x[1])[0] if gesamt else "unbekannt" # Debug-Ausgabe bei unbekanntem Themenbereich if d == "unbekannt": print(f"❗ Kurs {kursnummer} ({row['Titel']}) hat unbekannten Themenbereich.") print(f" → Verwendete APrV-Kürzel: {para_liste}") anteile_liste.append((m, r, b, d)) df["Anteil medizinisch"], df["Anteil rettungsdienstlich"], df["Anteil bezugswissenschaftlich"], df["Themenbereich"] = zip(*anteile_liste) # # --- APrV-Grundlagen: Themenschwerpunkte und Kompetenzbereiche --- # # Grundlage: APrV-Zuordnung durch Schüler*innen (prozentuale Verteilung nach Stunden) # Thematische Gewichtung nach APrV apr_theme_df = pd.DataFrame({ "Thema": ["medizinisch", "rettungsdienstlich", "bezugswissenschaftlich"], "Stunden": [496, 909, 515], "Anteil (%)": [27, 47, 26] }) # Kompetenzgewichtung nach APrV apr_kompetenz_df = pd.DataFrame({ "Kompetenz": ["fachlich", "sozial", "personal", "methodisch"], "Stunden": [464, 294, 206, 956], "Anteil (%)": [24, 15, 11, 50] }) # Umbenennung "unbekannt" → "Einführung/Prüfung" für valide Kategorisierung df["Themenbereich"] = df["Themenbereich"].replace("unbekannt", "Einführung/Prüfung") # Deskriptive Gesamtstatistik gesamt_stats = { "Anzahl Kurse": len(df), "Gesamtaufgaben": df["∑ Aufgaben"].sum(), "Gesamtdauer [d]": df["Dauer [d]"].sum(), "Ø Aufgaben pro Kurs": df["∑ Aufgaben"].mean(), "Ø Dauer pro Kurs [d]": df["Dauer [d]"].mean(), "Median Aufgaben": df["∑ Aufgaben"].median(), "Median Dauer [d]": df["Dauer [d]"].median(), "Standardabweichung Aufgaben": df["∑ Aufgaben"].std(), "Standardabweichung Dauer [d]": df["Dauer [d]"].std(), "Min Aufgaben": df["∑ Aufgaben"].min(), "Max Aufgaben": df["∑ Aufgaben"].max(), "Min Dauer [d]": df["Dauer [d]"].min(), "Max Dauer [d]": df["Dauer [d]"].max(), "Korrelation Aufgaben vs. Dauer": df["∑ Aufgaben"].corr(df["Dauer [d]"]) } print("Gesamtstatistik:\n", pd.Series(gesamt_stats).round(2)) # Gruppierte Statistik gruppe = df.groupby("Themenbereich").agg({ "∑ Aufgaben": ["mean", "std", "count"], "Dauer [d]": ["mean", "std"] }).round(2) gruppe.columns = ['Ø Aufgaben', 'SD Aufgaben', 'Anzahl Kurse', 'Ø Dauer', 'SD Dauer'] print("\nStatistik nach Themenbereich:\n", gruppe) def plot_boxplots(df): colors = get_colors() # Spezifische und dokumentierte Reihenfolge der Boxplots kategorie_order = [] for kategorie in ["medizinisch", "rettungsdienstlich", "bezugswissenschaftlich", "Einführung/Prüfung"]: if kategorie in df["Themenbereich"].unique(): kategorie_order.append(kategorie) # Aufgaben-Boxplot fig1 = go.Figure() for k in kategorie_order: d = df[df["Themenbereich"] == k] fig1.add_trace(go.Box( y=d["∑ Aufgaben"], name=k, boxmean='sd', marker=dict(color=colors["secondaryLine"]), line=dict(color=colors["primaryLine"]) )) fig1.update_layout(get_standard_layout( title="Verteilung der Aufgaben pro Themenbereich", x_title="Themenbereich", y_title="∑ Aufgaben" )) export_figure(fig1, fig1.layout.title.text, export_fig_visual, export_fig_png) fig1.show() # Dauer-Boxplot fig2 = go.Figure() for k in kategorie_order: d = df[df["Themenbereich"] == k] fig2.add_trace(go.Box( y=d["Dauer [d]"], name=k, boxmean='sd', marker=dict(color=colors["secondaryLine"]), line=dict(color=colors["primaryLine"]) )) fig2.update_layout(get_standard_layout( title="Verteilung der Kursdauer pro Themenbereich", x_title="Themenbereich", y_title="Dauer in Tagen" )) export_figure(fig2, fig2.layout.title.text, export_fig_visual, export_fig_png) fig2.show() def plot_aprv_pies(): colors = get_colors() # Thema-Tortendiagramm fig1 = go.Figure(data=[go.Pie( labels=apr_theme_df["Thema"], values=apr_theme_df["Anteil (%)"], marker=dict(colors=[colors["primaryLine"], colors["secondaryLine"], colors["depthArea"]]), textinfo='label+percent', insidetextorientation='radial' )]) fig1.update_layout(get_standard_layout( title="Anteil der Themenbereiche nach APrV", x_title="", y_title="" )) fig1.update_layout(showlegend=True) export_figure(fig1, fig1.layout.title.text, export_fig_visual, export_fig_png) fig1.show() # Kompetenz-Tortendiagramm fig2 = go.Figure(data=[go.Pie( labels=apr_kompetenz_df["Kompetenz"], values=apr_kompetenz_df["Anteil (%)"], marker=dict(colors=[ colors["primaryLine"], colors["secondaryLine"], colors["depthArea"], colors["brightArea"] ]), textinfo='label+percent', insidetextorientation='radial' )]) fig2.update_layout(get_standard_layout( title="Anteil der Kompetenzbereiche nach APrV", x_title="", y_title="" )) fig2.update_layout(showlegend=True) export_figure(fig2, fig2.layout.title.text, export_fig_visual, export_fig_png) fig2.show() def plot_vergleich_lehrplan_aprv(): # Gruppierung der Kursdauer nach Themenbereich nfsh_theme = df.groupby("Themenbereich")["Dauer [d]"].sum().reset_index() nfsh_theme["Anteil (%)"] = 100 * nfsh_theme["Dauer [d]"] / nfsh_theme["Dauer [d]"].sum() # Zusammenführung mit Schüler*innen-Schätzung aus APrV vergleich_df = pd.merge( apr_theme_df[["Thema", "Anteil (%)"]].rename(columns={"Anteil (%)": "Schätzung APrV"}), nfsh_theme.rename(columns={"Themenbereich": "Thema", "Anteil (%)": "NFS-H-Anteil"}), on="Thema" ) # Visualisierung: Gegenüberstellung als Balkendiagramm colors = get_colors() fig = go.Figure(data=[ go.Bar(name='Schätzung APrV', x=vergleich_df["Thema"], y=vergleich_df["Schätzung APrV"], marker_color=colors["primaryLine"]), go.Bar(name='NFS-H-Anteil', x=vergleich_df["Thema"], y=vergleich_df["NFS-H-Anteil"], marker_color=colors["secondaryLine"]) ]) fig.update_layout( get_standard_layout("Vergleich Themengewichtung: APrV-Schätzung vs. NFS-H-Lehrplan", "Thema", "Anteil [%]"), barmode='group' ) export_figure(fig, fig.layout.title.text, export_fig_visual, export_fig_png) fig.show() def plot_vergleich_kompetenz_aprv(): # Automatische Zuordnung der Kompetenzbereiche basierend auf CSV-Datei kuerzel_df = pd.read_csv(Path(__file__).parent / "APrV-Kuerzel_zu_Kompetenzbereichen.csv") kuerzel_map = dict(zip(kuerzel_df["Kürzel"], kuerzel_df["Kompetenzbereich"])) kompetenz_liste = [] for idx, row in df.iterrows(): kursnummer = int(row["Kursbezeichnung"].split("-")[-1]) if kursnummer not in df_para.index: kompetenz_liste.append("unbekannt") continue para_raw = df_para.loc[kursnummer, "NotSan-APrV"] if pd.isna(para_raw): kompetenz_liste.append("unbekannt") continue para_liste = [p.strip() for p in str(para_raw).split(",")] zähler = defaultdict(int) for para in para_liste: komp = kuerzel_map.get(para) if komp: zähler[komp] += 1 if zähler: hauptkategorie = max(zähler.items(), key=lambda x: x[1])[0] else: hauptkategorie = "unbekannt" kompetenz_liste.append(hauptkategorie) df["Kompetenzbereich"] = kompetenz_liste # Gruppierung nach Kompetenzbereich nfsh_k = df.groupby("Kompetenzbereich")["Dauer [d]"].sum().reset_index() nfsh_k["Anteil (%)"] = 100 * nfsh_k["Dauer [d]"] / nfsh_k["Dauer [d]"].sum() # Merge mit APrV-Kompetenzverteilung vergleich_k = pd.merge( apr_kompetenz_df[["Kompetenz", "Anteil (%)"]].rename(columns={"Anteil (%)": "Schätzung APrV"}), nfsh_k.rename(columns={"Kompetenzbereich": "Kompetenz", "Anteil (%)": "NFS-H-Anteil"}), on="Kompetenz" ) # Visualisierung colors = get_colors() fig = go.Figure(data=[ go.Bar(name='Schätzung APrV', x=vergleich_k["Kompetenz"], y=vergleich_k["Schätzung APrV"], marker_color=colors["primaryLine"]), go.Bar(name='NFS-H-Anteil', x=vergleich_k["Kompetenz"], y=vergleich_k["NFS-H-Anteil"], marker_color=colors["secondaryLine"]) ]) fig.update_layout( get_standard_layout("Vergleich Kompetenzgewichtung: APrV-Schätzung vs. NFS-H-Lehrplan", "Kompetenzbereich", "Anteil [%]"), barmode='group' ) export_figure(fig, fig.layout.title.text, export_fig_visual, export_fig_png) fig.show() # --- Visualisierungen aufrufen --- # Alle Visualisierungen in sinnvoller Reihenfolge aufrufen plot_aprv_pies() plot_vergleich_lehrplan_aprv() plot_vergleich_kompetenz_aprv() plot_boxplots(df) # Tabellen-Visualisierungen nach bisherigen Plots aus ci_template aufrufen plot_table_from_dict(gesamt_stats, "Gesamtstatistik der curricularen Struktur", export_figure, export_fig_visual, export_fig_png) plot_table_from_dataframe(gruppe, "Themenbereich", "Gruppierte Statistik nach Themenbereichen", export_figure, export_fig_visual, export_fig_png)