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:
@ -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)
|
||||
|
||||
Reference in New Issue
Block a user