This repository has been archived on 2025-10-27. You can view files and clone it, but cannot push or open issues or pull requests.
Files
learning-management-system/lms_statistische-analyse.py
2025-05-31 11:10:57 +02:00

370 lines
17 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
)
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)
# 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)