feat: CI-konforme Dendrogramm-Visualisierung mit Matplotlib als Abschluss der Analyse

- 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
This commit is contained in:
2025-06-19 23:05:18 +02:00
parent ef140a5df1
commit d3f0d865a4

View File

@ -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): "<br>".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('<br>', ', ')}")
# 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)