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 ) from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score from sklearn.preprocessing import StandardScaler 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) # --- K-Means-Clusteranalyse auf Basis der Themenbereichsanteile --- anteilsmerkmale = ['Anteil medizinisch', 'Anteil rettungsdienstlich', 'Anteil bezugswissenschaftlich'] if df[anteilsmerkmale].nunique().eq(1).all(): print("⚠️ Alle Anteilsmerkmale sind konstant. K-Means-Clustering wird übersprungen.") df['Cluster_Anteile'] = 'Nicht gültig' silhouette_avg_anteile = None else: try: scaler = StandardScaler() scaled_anteile = scaler.fit_transform(df[anteilsmerkmale]) if len(df) < 2: raise ValueError("Zu wenige Datenpunkte für die Clusteranalyse der Anteile.") kmeans_anteile = KMeans(n_clusters=4, random_state=42) df['Cluster_Anteile'] = kmeans_anteile.fit_predict(scaled_anteile).astype(str) silhouette_avg_anteile = silhouette_score(scaled_anteile, df['Cluster_Anteile'].astype(int)) print(f"Silhouette-Score (Anteile): {silhouette_avg_anteile:.4f}") # Beschreibung der Cluster cluster_means_anteile = df[anteilsmerkmale + ['Cluster_Anteile']].groupby('Cluster_Anteile').mean() cluster_labels_anteile = { str(c): "
".join(cluster_means_anteile.loc[str(c)].sort_values(ascending=False).head(3).index) for c in cluster_means_anteile.index } df['Cluster_Label_Anteile'] = df['Cluster_Anteile'].map(cluster_labels_anteile) for cluster, label in cluster_labels_anteile.items(): print(f"Cluster {cluster}: {label.replace('
', ', ')}") # Visualisierung als 3D-Scatterplot import plotly.express as px colors = get_colors() fig_anteile = px.scatter_3d( df, x='Anteil medizinisch', y='Anteil rettungsdienstlich', z='Anteil bezugswissenschaftlich', color='Cluster_Label_Anteile', size_max=50, color_discrete_sequence=list(colors.values()), hover_data=anteilsmerkmale + ['Cluster_Label_Anteile'], title=f"Clusteranalyse (Themenanteile) | Silhouette: {silhouette_avg_anteile:.4f}", labels={ 'Anteil medizinisch': 'Medizinisch', 'Anteil rettungsdienstlich': 'Rettungsdienstlich', 'Anteil bezugswissenschaftlich': 'Bezugswissenschaftlich', 'Cluster_Label_Anteile': 'Cluster-Beschreibung' } ) fig_anteile.update_layout(get_standard_layout( title="K-Means-Clusteranalyse nach Themenanteilen", x_title='Medizinisch', y_title='Rettungsdienstlich', z_title='Bezugswissenschaftlich' )) export_figure(fig_anteile, "kmeans_cluster_anteile", export_fig_visual, export_fig_png) except Exception as e: print(f"Fehler bei der Clusteranalyse auf Anteilsdaten: {e}") df['Cluster_Anteile'] = 'Fehler' silhouette_avg_anteile = None # --- Hierarchisches Clustering der Themenanteile mit Dendrogramm --- # --- Interaktives Dendrogramm mit Plotly --- from scipy.cluster.hierarchy import dendrogram, linkage import plotly.graph_objects as go # Berechnung der Linkage-Matrix für hierarchisches Clustering try: linkage_matrix = linkage(scaled_anteile, method='ward') except Exception as e: print(f"Fehler beim Erzeugen der Linkage-Matrix für das Dendrogramm: {e}") linkage_matrix = None def create_plotly_dendrogram(linkage_matrix, labels, orientation='bottom'): colors = get_colors() dendro = dendrogram(linkage_matrix, labels=labels, orientation=orientation, no_plot=True) icoord = dendro['icoord'] dcoord = dendro['dcoord'] color_list = dendro['color_list'] label = dendro['ivl'] data = [] for i in range(len(icoord)): xs = icoord[i] ys = dcoord[i] data.append(go.Scatter( x=xs, y=ys, mode='lines', line=dict(color=colors["primaryLine"], width=2), hoverinfo='none' )) layout = get_standard_layout( title="Interaktives Dendrogramm der Kursanteile (Ward-Linkage)", x_title="Kursbezeichnung", y_title="Distanz" ) layout.update(dict( xaxis=dict(tickvals=list(range(5, len(labels)*10+5, 10)), ticktext=label, tickangle=90), showlegend=False, margin=dict(l=40, r=0, b=120, t=50), width=1200, height=600 )) fig_dendrogramm_anteile = go.Figure(data=data, layout=layout) export_figure(fig_dendrogramm_anteile, "dendrogramm_cluster_anteile", export_fig_visual, export_fig_png) fig_dendrogramm_anteile.show() # linkage_matrix und scaled_anteile werden weiter oben erzeugt create_plotly_dendrogram(linkage_matrix, df["Kursbezeichnung"].tolist()) # 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) # --- Matplotlib-Dendrogramm-Plot und Export (CI-konform) --- import matplotlib.pyplot as plt from scipy.cluster.hierarchy import dendrogram, linkage def plot_dendrogram_matplotlib(df, scaled_anteile): try: linkage_matrix = linkage(scaled_anteile, method='ward') fig, ax = plt.subplots(figsize=(16, 8)) dendrogram( linkage_matrix, labels=df["Kursbezeichnung"].tolist(), leaf_rotation=90, leaf_font_size=10, color_threshold=None ) ax.set_title("Dendrogramm der Kursanteile (Ward-Linkage)", fontsize=14) ax.set_xlabel("Kursbezeichnung", fontsize=12) ax.set_ylabel("Distanz", fontsize=12) fig.tight_layout() # Speichern export_path_png = os.path.join( "..", "Allgemein beruflich", "Research", "Forschungsprojekte", "Systemische Kompetenzentwicklung für High Responsibility Teams", "dendrogramm_cluster_anteile_matplotlib.png" ) fig.savefig(export_path_png, dpi=300) print(f"✅ Matplotlib-Dendrogramm gespeichert: {export_path_png}") plt.close(fig) except Exception as e: print(f"❌ Fehler beim Erzeugen des Matplotlib-Dendrogramms: {e}") # Am Ende aufrufen plot_dendrogram_matplotlib(df, scaled_anteile)