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

382 lines
17 KiB
Python

import pandas as pd
import plotly.graph_objs as go
import os
import re
import unicodedata
import subprocess
from config_lms import plotly_theme, export_fig_visual, export_fig_png
import sys
sys.path.append("/Users/jochen_hanisch-johannsen/Documents/scripte/Jochen-Hanisch/CI/ci_template")
from plotly_template import get_standard_layout, get_colors, set_theme
from pathlib import Path
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))
# PNG-Exportpfad definieren
png_export_path = Path(__file__).parent.parent.parent / "Allgemein beruflich" / "Research" / "Charité - Universitätsmedizin Berlin" / "Dissertation" / "Abbildungen"
# 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
]
})
# --- Automatische Berechnung Themenbereichsanteile pro Kurs ---
# Ausschließlich systematische APrV-Zuordnung aus lms-verteilung.xlsx und zuordnung_praezise verwenden
from collections import defaultdict
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 Reihenfolge: medizinisch, rettungsdienstlich, bezugswissenschaftlich, Einführung/Prüfung
kategorie_order = []
for k in ["medizinisch", "rettungsdienstlich", "bezugswissenschaftlich", "Einführung/Prüfung"]:
if k in df["Themenbereich"].unique():
kategorie_order.append(k)
# Ergänze ggf. andere Kategorien (falls weitere vorkommen)
for k in sorted(df["Themenbereich"].unique()):
if k not in kategorie_order:
kategorie_order.append(k)
# 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)