From d3f0d865a44d45ffd34931a3599f86a0a1000e7b Mon Sep 17 00:00:00 2001 From: Jochen Hanisch-Johannsen Date: Thu, 19 Jun 2025 23:05:18 +0200 Subject: [PATCH] feat: CI-konforme Dendrogramm-Visualisierung mit Matplotlib als Abschluss der Analyse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Neue Funktion `plot_dendrogram_matplotlib(df, scaled_anteile)` ergänzt: - erzeugt Dendrogramm der Kursanteile mit scipy + matplotlib - speichert das Diagramm als PNG im Forschungsprojektverzeichnis - automatische Farbgebung durch scipy.dendrogram() - sauberes CI-nahes Layout (Schriftgrößen, Achsentitel, Formatierung) - Funktionsaufruf ans Ende des Skripts verschoben: - wird als letzter Schritt der statistischen Analyse ausgeführt - damit visuelle Gesamtstruktur abgeschlossen - Bestehende Plotly-basierte Dendrogramm-Funktion bleibt erhalten (interaktiv, CI) - Fokus der matplotlib-Version liegt auf Publikationsreife und Clusterklarheit --- lms_statistische-analyse.py | 165 ++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) diff --git a/lms_statistische-analyse.py b/lms_statistische-analyse.py index c5eac8e..5d39471 100644 --- a/lms_statistische-analyse.py +++ b/lms_statistische-analyse.py @@ -8,6 +8,9 @@ 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 --- @@ -364,6 +367,168 @@ 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)