Systemtheoretische Modelle und Algorithmen zur Analyse angedacht.
This commit is contained in:
410
visible-learning systemtheoretisch.py
Normal file
410
visible-learning systemtheoretisch.py
Normal file
@ -0,0 +1,410 @@
|
||||
from __future__ import annotations
|
||||
"""
|
||||
Visible Learning – Systemtheoretische Sicht (nach Luhmann/Erziehungssystem)
|
||||
---------------------------------------------------------------------------
|
||||
Ziel
|
||||
- Daten (Thermometer) laden
|
||||
- Psychische und soziale Adressierungen identifizieren (Mapping/Heuristik)
|
||||
- Kopplungsindex (Kommunikation ↔ Gedanke) als Indikator für Lernfähigkeit berechnen
|
||||
- CI-konforme Visualisierungen (2D & 3D) erzeugen
|
||||
|
||||
CI
|
||||
- Verwendet die gleiche CI wie die statistische Datei (ci_template.plotly_template)
|
||||
- Farben/Styles ausschließlich über Template (keine Hardcodierung)
|
||||
"""
|
||||
|
||||
# -----------------------------------------
|
||||
# Imports
|
||||
# -----------------------------------------
|
||||
import os
|
||||
import json
|
||||
import math
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
import plotly.graph_objs as go
|
||||
import plotly.io as pio
|
||||
|
||||
# -----------------------------------------
|
||||
# Konfiguration laden (identischer Mechanismus)
|
||||
# -----------------------------------------
|
||||
# Erwartete Felder: csv_file, theme, export_fig_visual, export_fig_png
|
||||
from config_visible_learning import (
|
||||
csv_file,
|
||||
theme,
|
||||
export_fig_visual,
|
||||
export_fig_png,
|
||||
)
|
||||
|
||||
# -----------------------------------------
|
||||
# CI-Template (identisch zu statistischer Datei)
|
||||
# -----------------------------------------
|
||||
try:
|
||||
from ci_template import plotly_template
|
||||
plotly_template.set_theme(theme)
|
||||
_layout = lambda title, x, y, z='Z': plotly_template.get_standard_layout(
|
||||
title=title, x_title=x, y_title=y, z_title=z
|
||||
)
|
||||
_styles = plotly_template.get_plot_styles()
|
||||
_colors = plotly_template.get_colors()
|
||||
except Exception as _e:
|
||||
# Fallback (neutral)
|
||||
def _layout(title, x, y, z='Z'):
|
||||
return dict(title=title, xaxis_title=x, yaxis_title=y)
|
||||
_styles = {
|
||||
"marker_accent": dict(color="#1f77b4", size=8, symbol="circle"),
|
||||
"marker_positiveHighlight": dict(color="#2ca02c", size=8, symbol="circle"),
|
||||
"marker_negativeHighlight": dict(color="#d62728", size=8, symbol="circle"),
|
||||
"linie_primaryLine": dict(color="#1f77b4", width=2),
|
||||
"linie_secondaryLine": dict(color="#ff7f0e", width=2),
|
||||
"balken_accent": dict(color="#1f77b4"),
|
||||
}
|
||||
_colors = {
|
||||
"accent": "#1f77b4",
|
||||
"brightArea": "#66CCCC",
|
||||
"depthArea": "#006666",
|
||||
"positiveHighlight": "#2ca02c",
|
||||
"negativeHighlight": "#d62728",
|
||||
"text": "#333333",
|
||||
"background": "#ffffff",
|
||||
"white": "#ffffff",
|
||||
}
|
||||
|
||||
# -----------------------------------------
|
||||
# Export-Helfer
|
||||
# -----------------------------------------
|
||||
EXPORT_DIR = os.path.join(os.path.dirname(__file__), "export")
|
||||
os.makedirs(EXPORT_DIR, exist_ok=True)
|
||||
|
||||
def export_figure(fig, name: str, do_html: bool, do_png: bool):
|
||||
base = os.path.join(EXPORT_DIR, name)
|
||||
if do_html:
|
||||
pio.write_html(fig, file=f"{base}.html", auto_open=False, include_plotlyjs="cdn")
|
||||
if do_png:
|
||||
try:
|
||||
pio.write_image(fig, f"{base}.png", scale=2)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def export_json(obj: dict, name: str):
|
||||
p = os.path.join(EXPORT_DIR, name)
|
||||
try:
|
||||
with open(p, "w", encoding="utf-8") as f:
|
||||
json.dump(obj, f, ensure_ascii=False, indent=2)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# -----------------------------------------
|
||||
# Daten laden und vorbereiten
|
||||
# -----------------------------------------
|
||||
REQUIRED = ["Thermometer_ID", "Stichwort", "Effektstärke"]
|
||||
|
||||
def load_data(path: str) -> pd.DataFrame:
|
||||
df = pd.read_csv(path)
|
||||
missing = [c for c in REQUIRED if c not in df.columns]
|
||||
if missing:
|
||||
raise ValueError(f"Fehlende Spalten in CSV: {missing}")
|
||||
|
||||
df["Thermometer_ID"] = df["Thermometer_ID"].astype(str)
|
||||
df["Effektstärke"] = (
|
||||
df["Effektstärke"].astype(str).str.replace(",", ".", regex=False).str.strip()
|
||||
)
|
||||
df["Effektstärke"] = pd.to_numeric(df["Effektstärke"], errors="coerce")
|
||||
df = df.dropna(subset=["Effektstärke"]).copy()
|
||||
|
||||
# Kapitelnummer & -name herstellen, falls nicht vorhanden
|
||||
if "Kapitel" not in df.columns:
|
||||
df["Kapitel"] = df["Thermometer_ID"].str.split(".").str[0].astype(int)
|
||||
if "Kapitelname" not in df.columns:
|
||||
kapitel_map = {
|
||||
5: "Lernende",
|
||||
6: "Elternhaus und Familie",
|
||||
7: "Schule und Gesellschaft",
|
||||
8: "Klassenzimmer",
|
||||
9: "Lehrperson",
|
||||
10: "Curriculum",
|
||||
11: "Zielorientiertes Unterrichten",
|
||||
12: "Lernstrategien",
|
||||
13: "Lehrstrategien",
|
||||
14: "Nutzung von Technologien",
|
||||
15: "Schulische und außerschulische Einflüsse",
|
||||
}
|
||||
df["Kapitelname"] = df["Kapitel"].map(kapitel_map).fillna(df["Kapitel"].map(lambda k: f"Kapitel {k}"))
|
||||
return df
|
||||
|
||||
# -----------------------------------------
|
||||
# System-Mapping: Psychisch / Sozial
|
||||
# -----------------------------------------
|
||||
# Erwartete optionale Datei: system_mapping.csv
|
||||
# Spalten: "Term","Psych","Sozial" (0/1), wobei "Term" gegen Stichwort gematcht wird (Teilstring, case-insensitive)
|
||||
|
||||
def load_system_mapping(map_csv: str = "system_mapping.csv") -> pd.DataFrame | None:
|
||||
path = os.path.join(os.path.dirname(__file__), map_csv)
|
||||
if os.path.exists(path):
|
||||
m = pd.read_csv(path)
|
||||
for col in ["Term", "Psych", "Sozial"]:
|
||||
if col not in m.columns:
|
||||
raise ValueError("system_mapping.csv muss die Spalten 'Term','Psych','Sozial' enthalten.")
|
||||
m["Term"] = m["Term"].astype(str).str.strip()
|
||||
m["Psych"] = m["Psych"].astype(int).clip(0, 1)
|
||||
m["Sozial"] = m["Sozial"].astype(int).clip(0, 1)
|
||||
return m
|
||||
return None
|
||||
|
||||
def classify_systems(df: pd.DataFrame, mapping: pd.DataFrame | None = None) -> pd.DataFrame:
|
||||
out = df.copy()
|
||||
out["Psych"] = 0
|
||||
out["Sozial"] = 0
|
||||
|
||||
# 1) Mapping-Datei (präzise)
|
||||
if mapping is not None and not mapping.empty:
|
||||
sw = out["Stichwort"].astype(str).str.lower()
|
||||
for _, row in mapping.iterrows():
|
||||
term = str(row["Term"]).lower().strip()
|
||||
if not term:
|
||||
continue
|
||||
mask = sw.str.contains(term, na=False)
|
||||
out.loc[mask, "Psych"] = np.maximum(out.loc[mask, "Psych"], int(row["Psych"]))
|
||||
out.loc[mask, "Sozial"] = np.maximum(out.loc[mask, "Sozial"], int(row["Sozial"]))
|
||||
|
||||
# 2) Heuristik (falls nach Mapping noch 0/0), bewusst konservativ
|
||||
# - Psychische Marker
|
||||
psych_tokens = [
|
||||
"intelligenz","kognition","exekutiv","gedächtnis","selbstwirksam",
|
||||
"selbstbild","emotion","angst","depress","wut","frustration","konzentration",
|
||||
"ausdauer","beharrlichkeit","zuversicht","mindset","kreativ","neugier",
|
||||
"arbeitsgedächtnis","einstellung","motivation","willen"
|
||||
]
|
||||
# - Soziale Marker
|
||||
sozial_tokens = [
|
||||
"klasse","klassen","beziehung","lehrer","schüler","unterricht",
|
||||
"klima","team","gruppe","beratung","schulleitung","schule","familie",
|
||||
"eltern","konflikt","zusammenhalt","zugehörigkeit","tracking","sommerschule",
|
||||
"curriculum","kalender","stundenplan","pause","bulling","ausschluss"
|
||||
]
|
||||
|
||||
# nur dort heuristisch, wo noch keine Setzung vorhanden ist:
|
||||
unset_mask = (out["Psych"] == 0) & (out["Sozial"] == 0)
|
||||
sw2 = out.loc[unset_mask, "Stichwort"].astype(str).str.lower()
|
||||
|
||||
out.loc[unset_mask & sw2.str.contains("|".join(psych_tokens), na=False), "Psych"] = 1
|
||||
out.loc[unset_mask & sw2.str.contains("|".join(sozial_tokens), na=False), "Sozial"] = 1
|
||||
|
||||
return out
|
||||
|
||||
# -----------------------------------------
|
||||
# Kopplungsindex (Erziehungssystem: Lernfähig/nicht lernfähig)
|
||||
# -----------------------------------------
|
||||
def minmax_norm(a: np.ndarray) -> np.ndarray:
|
||||
a = np.asarray(a, dtype=float)
|
||||
if a.size == 0:
|
||||
return a
|
||||
lo, hi = np.nanmin(a), np.nanmax(a)
|
||||
if not np.isfinite(lo) or not np.isfinite(hi) or hi - lo <= 1e-12:
|
||||
return np.zeros_like(a)
|
||||
return (a - lo) / (hi - lo)
|
||||
|
||||
def compute_coupling_index(df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""
|
||||
Kopplungsindex = norm(|d|) * w
|
||||
w = 1.0 bei Kopplung (Psych=1 & Sozial=1)
|
||||
0.6 bei nur Psych=1 oder nur Sozial=1
|
||||
0.2 sonst (unspezifisch)
|
||||
Vorzeichen des Index = Vorzeichen(d)
|
||||
"""
|
||||
out = df.copy()
|
||||
abs_d = out["Effektstärke"].abs().values
|
||||
abs_d_norm = minmax_norm(abs_d)
|
||||
|
||||
both = (out["Psych"] == 1) & (out["Sozial"] == 1)
|
||||
single = ((out["Psych"] == 1) ^ (out["Sozial"] == 1))
|
||||
none = (out["Psych"] == 0) & (out["Sozial"] == 0)
|
||||
|
||||
w = np.where(both, 1.0, np.where(single, 0.6, 0.2))
|
||||
signed = np.sign(out["Effektstärke"].values) * abs_d_norm * w
|
||||
out["Kopplungsindex"] = signed
|
||||
|
||||
# Label für schnelle Lesbarkeit
|
||||
def addr_label(p, s):
|
||||
if p == 1 and s == 1:
|
||||
return "Kopplung (Psych+Sozial)"
|
||||
if p == 1 and s == 0:
|
||||
return "Psychisch adressiert"
|
||||
if p == 0 and s == 1:
|
||||
return "Sozial adressiert"
|
||||
return "Unspezifisch"
|
||||
out["Adressierung"] = [addr_label(p, s) for p, s in zip(out["Psych"], out["Sozial"])]
|
||||
|
||||
# Ränge
|
||||
out["Rank_abs_d"] = (-out["Effektstärke"].abs()).rank(method="min").astype(int)
|
||||
out["Rank_kopplung"] = (-out["Kopplungsindex"].abs()).rank(method="min").astype(int)
|
||||
return out
|
||||
|
||||
# -----------------------------------------
|
||||
# Visualisierungen (CI-konform, keine Hardcodierung)
|
||||
# -----------------------------------------
|
||||
def plot_sign_system_2d(df: pd.DataFrame):
|
||||
"""
|
||||
2D-Sicht: X=Psych (0/1), Y=Sozial (0/1), Markergröße|Farbe ~ Kopplungsindex
|
||||
"""
|
||||
x = df["Psych"].astype(int)
|
||||
y = df["Sozial"].astype(int)
|
||||
size = (df["Kopplungsindex"].abs() * 22.0 + 6.0).astype(float)
|
||||
|
||||
color_pos = _colors.get("positiveHighlight", "#2ca02c")
|
||||
color_neg = _colors.get("negativeHighlight", "#d62728")
|
||||
point_colors = np.where(df["Kopplungsindex"] >= 0, color_pos, color_neg)
|
||||
|
||||
hover = (
|
||||
"Thermometer: %{customdata[0]}<br>"
|
||||
"Stichwort: %{text}<br>"
|
||||
"d: %{customdata[1]:.2f}<br>"
|
||||
"Adressierung: %{customdata[2]}<br>"
|
||||
"Kopplungsindex: %{customdata[3]:.3f}<br>"
|
||||
"Kapitel: %{customdata[4]}<extra></extra>"
|
||||
)
|
||||
|
||||
fig = go.Figure()
|
||||
fig.add_trace(go.Scatter(
|
||||
x=x, y=y,
|
||||
mode="markers",
|
||||
marker=dict(size=size, color=point_colors),
|
||||
text=df["Stichwort"],
|
||||
customdata=np.stack([
|
||||
df["Thermometer_ID"],
|
||||
df["Effektstärke"],
|
||||
df["Adressierung"],
|
||||
df["Kopplungsindex"],
|
||||
df["Kapitelname"]
|
||||
], axis=-1),
|
||||
hovertemplate=hover,
|
||||
name="Thermometer"
|
||||
))
|
||||
|
||||
# Diskrete Achsen (0/1) mit CI-Layout
|
||||
fig.update_layout(_layout(
|
||||
"Erziehungssystem – Adressierung & Kopplung (2D)",
|
||||
"Psychisch (0/1)", "Sozial (0/1)"
|
||||
))
|
||||
fig.update_xaxes(tickmode="array", tickvals=[0, 1], ticktext=["0", "1"])
|
||||
fig.update_yaxes(tickmode="array", tickvals=[0, 1], ticktext=["0", "1"])
|
||||
fig.show()
|
||||
export_figure(fig, "sys_erziehung_2d", export_fig_visual, export_fig_png)
|
||||
|
||||
def plot_sign_system_3d(df: pd.DataFrame):
|
||||
"""
|
||||
3D-Sicht: X=Psych (0/1), Y=Sozial (0/1), Z=Effektstärke; Farbe/Größe ~ Kopplungsindex
|
||||
"""
|
||||
size = (df["Kopplungsindex"].abs() * 8.0 + 4.0).astype(float)
|
||||
color_pos = _colors.get("positiveHighlight", "#2ca02c")
|
||||
color_neg = _colors.get("negativeHighlight", "#d62728")
|
||||
point_colors = np.where(df["Kopplungsindex"] >= 0, color_pos, color_neg)
|
||||
|
||||
hover = (
|
||||
"Thermometer: %{text}<br>"
|
||||
"Kapitel: %{customdata[0]}<br>"
|
||||
"Psych: %{x} | Sozial: %{y}<br>"
|
||||
"d: %{z:.2f}<br>"
|
||||
"Kopplungsindex: %{customdata[1]:.3f}<extra></extra>"
|
||||
)
|
||||
|
||||
fig = go.Figure()
|
||||
fig.add_trace(go.Scatter3d(
|
||||
x=df["Psych"].astype(int),
|
||||
y=df["Sozial"].astype(int),
|
||||
z=df["Effektstärke"],
|
||||
mode="markers",
|
||||
marker={**_styles.get("marker_accent", {}), "size": size, "color": point_colors},
|
||||
text=df["Stichwort"],
|
||||
customdata=np.stack([df["Kapitelname"], df["Kopplungsindex"]], axis=-1),
|
||||
hovertemplate=hover,
|
||||
name="Thermometer"
|
||||
))
|
||||
|
||||
fig.update_layout(_layout(
|
||||
"Erziehungssystem – 3D-Sicht (Psych × Sozial × d)",
|
||||
"Psychisch (0/1)", "Sozial (0/1)", "Cohen d"
|
||||
))
|
||||
fig.update_scenes(
|
||||
xaxis=dict(tickmode="array", tickvals=[0,1], ticktext=["0","1"]),
|
||||
yaxis=dict(tickmode="array", tickvals=[0,1], ticktext=["0","1"])
|
||||
)
|
||||
fig.show()
|
||||
export_figure(fig, "sys_erziehung_3d", export_fig_visual, export_fig_png)
|
||||
|
||||
def plot_rank_tables(df: pd.DataFrame, top_n: int = 15):
|
||||
"""
|
||||
Zwei tabellarische Sichten:
|
||||
- Top |d| (stärkste Magnitude)
|
||||
- Top |Kopplungsindex| (stärkste systemische Kopplung)
|
||||
"""
|
||||
from plotly.graph_objs import Table, Figure
|
||||
|
||||
def table(data: pd.DataFrame, title: str, fname: str):
|
||||
cols = ["Thermometer_ID", "Stichwort", "Kapitelname", "Effektstärke", "Psych", "Sozial", "Kopplungsindex", "Adressierung"]
|
||||
data = data[cols].copy()
|
||||
data["Effektstärke"] = data["Effektstärke"].round(2)
|
||||
data["Kopplungsindex"] = data["Kopplungsindex"].round(3)
|
||||
|
||||
headers = list(data.columns)
|
||||
values = [data[c].astype(str).tolist() for c in headers]
|
||||
fig = Figure(data=[Table(
|
||||
header=dict(values=headers, fill_color=_colors["brightArea"], font=dict(color=_colors["white"])),
|
||||
cells=dict(values=values, fill_color=_colors["depthArea"], font=dict(color=_colors["white"]))
|
||||
)])
|
||||
fig.update_layout(_layout(title, "", ""))
|
||||
fig.show()
|
||||
export_figure(fig, fname, export_fig_visual, export_fig_png)
|
||||
|
||||
top_abs = df.sort_values(df["Effektstärke"].abs(), ascending=False).head(top_n)
|
||||
table(top_abs, f"Top {top_n} nach |d|", "sys_top_absd")
|
||||
|
||||
top_coup = df.sort_values(df["Kopplungsindex"].abs(), ascending=False).head(top_n)
|
||||
table(top_coup, f"Top {top_n} nach |Kopplungsindex|", "sys_top_kopplung")
|
||||
|
||||
# -----------------------------------------
|
||||
# Pipeline
|
||||
# -----------------------------------------
|
||||
def analyse_system(path_csv: str, map_csv: str = "system_mapping.csv"):
|
||||
# Laden
|
||||
df = load_data(path_csv)
|
||||
|
||||
# Systemklassifikation
|
||||
mapping = load_system_mapping(map_csv)
|
||||
df = classify_systems(df, mapping=mapping)
|
||||
|
||||
# Kopplungsindex
|
||||
df = compute_coupling_index(df)
|
||||
|
||||
# Export Kern-Output
|
||||
try:
|
||||
out_cols = ["Thermometer_ID","Stichwort","Kapitel","Kapitelname","Effektstärke","Psych","Sozial","Adressierung","Kopplungsindex"]
|
||||
df[out_cols].to_csv(os.path.join(EXPORT_DIR, "system_view.csv"), index=False)
|
||||
export_json(df[out_cols].to_dict(orient="records"), "system_view.json")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Visualisierungen
|
||||
plot_sign_system_2d(df)
|
||||
plot_sign_system_3d(df)
|
||||
plot_rank_tables(df, top_n=15)
|
||||
|
||||
# Konsolen-Report
|
||||
print("—" * 60)
|
||||
print("SYSTEMTHEORETISCHE SICHT – Zusammenfassung")
|
||||
print(df.groupby("Adressierung")["Effektstärke"].agg(n="count", mean="mean").round(3))
|
||||
print("\nTop 10 Kopplung (|Index|):")
|
||||
print(
|
||||
df.loc[:, ["Thermometer_ID", "Stichwort", "Kapitelname", "Effektstärke", "Kopplungsindex"]]
|
||||
.assign(abs_idx=lambda t: t["Kopplungsindex"].abs())
|
||||
.sort_values("abs_idx", ascending=False)
|
||||
.head(10)
|
||||
.drop(columns=["abs_idx"])
|
||||
.to_string(index=False)
|
||||
)
|
||||
|
||||
# -----------------------------------------
|
||||
# Main
|
||||
# -----------------------------------------
|
||||
if __name__ == "__main__":
|
||||
analyse_system(os.path.join(os.path.dirname(__file__), csv_file))
|
||||
Reference in New Issue
Block a user