- 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
535 lines
23 KiB
Python
535 lines
23 KiB
Python
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): "<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)
|