From aab5c683b95d9e4031278a3e145db6256d2c41dd Mon Sep 17 00:00:00 2001 From: Jochen Hanisch-Johannsen Date: Wed, 3 Sep 2025 23:53:41 +0200 Subject: [PATCH] =?UTF-8?q?Netzwerkanalyse=20erweitert:=20-=203D-Visualisi?= =?UTF-8?q?erung=20mit=20Effekt-Achse=20(z-Modi:=20Effekt,=20System,=20Sem?= =?UTF-8?q?antik)=20-=20Top-Listen=20(je=2015=20positive/negative=20Effekt?= =?UTF-8?q?st=C3=A4rken)=20erg=C3=A4nzt=20-=20Item-Projektion=20mit=20Comm?= =?UTF-8?q?unity-Labels=20-=20Config=20um=20Toggle=20f=C3=BCr=20z-Modi=20m?= =?UTF-8?q?it=20sprechenden=20Achsentiteln=20erweitert=20-=20Keys=20konsis?= =?UTF-8?q?tent=20(top=5Fn=5Fextremes,=20show=5Fitem=5Fprojection)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config_visible_learning.py | 44 ++-- export/network_systemebenen.json | 251 ++++++++++++++++++ export/network_top_extremes.json | 246 ++++++++++++++++++ visible-learning netzwerkanalyse.py | 378 +++++++++++++++++++++++++++- 4 files changed, 887 insertions(+), 32 deletions(-) create mode 100644 export/network_top_extremes.json diff --git a/config_visible_learning.py b/config_visible_learning.py index 5c6f36a..5f40367 100644 --- a/config_visible_learning.py +++ b/config_visible_learning.py @@ -1,22 +1,3 @@ -""" -Konfiguration Visible Learning - -Diese Datei steuert die Analysen der Effektstärken aus Hattie (Visible Learning). - -- csv_file: Pfad zur Eingabedatei (eine CSV, die alle Kapitel enthalten kann). -- k_clusters: Anzahl der Cluster für K-Means. - -- export_fig_visual: True = HTML-Export der Plots. -- export_fig_png: True = PNG-Export der Plots (setzt Kaleido voraus). - -- theme: Darstellungs-Theme ("dark" oder "light"). - -Kapitelsteuerung: -- selected_kapitel: Nummer eines Kapitels (z. B. 5), das isoliert betrachtet werden soll. - None = kein Filter, d. h. gesamte CSV in einem Schwung analysieren. -- analyse_all: True = alle Kapitel sequenziell einzeln durchlaufen und auswerten. - False = nur den Filter aus selected_kapitel beachten. -""" # config_visible-learning.py # Pfad zur Eingabedatei @@ -36,3 +17,28 @@ theme = "dark" selected_kapitel = None # Nummer des Kapitels (z.B. 5), None = kein Filter analyse_all = False # True = alle Kapitel durchlaufen export_werte_all = True # Wertedatei (werte_all.json) exportieren + +# 3D-Visualisierung: Toggle für die drei z-Modi mit sprechenden Achsentiteln +z_mode = "effekt" # Mögliche Werte: "effekt", "kapitel", "system" + +z_axis_labels = { + "effekt": "Effektstärke (Cohen d)", + "kapitel": "Kapitelnummer", + "system": "Systemebene (psychisch/sozial)" +} + +# ——————————————————————————————————————————————— +# Zusatz-Ausgaben & Netzwerkanalyse-Optionen +# ——————————————————————————————————————————————— +# 1) Top-Listen der Effektstärken +export_top_extremes = True +top_n_extremes = 15 + +# 2) Item-Projektion im Netzwerk + Community-Labels +show_item_projection = True # statt enable_item_projection +projection_method = "layout_spring" # "layout_spring" | "umap" +show_community_labels = True +community_algorithm = "louvain" # "louvain" | "leiden" (falls unterstützt) +min_community_size = 3 + +# 3) z-Achsen-Toggle kommt aus z_mode / z_axis_labels (oben) \ No newline at end of file diff --git a/export/network_systemebenen.json b/export/network_systemebenen.json index 0bc7db4..3229e15 100644 --- a/export/network_systemebenen.json +++ b/export/network_systemebenen.json @@ -1,4 +1,255 @@ { + "extremes": { + "top_positive": [ + { + "Thermometer_ID": 9.08, + "Stichwort": "Kollektive Wirksamkeitserwartung", + "Kapitelname": "Lehrperson", + "Subkapitel": "Einflüsse der Lehrperson", + "Effektstärke": 1.34, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 9.05, + "Stichwort": "Einschätzung des Leistungsniveaus durch die Lehrperson", + "Kapitelname": "Lehrperson", + "Subkapitel": "Einflüsse der Lehrperson", + "Effektstärke": 1.3, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 5.05, + "Stichwort": "Erkenntnisstufen", + "Kapitelname": "Lernende", + "Subkapitel": "Fähigkeiten", + "Effektstärke": 1.28, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 9.06, + "Stichwort": "Glaubwürdigkeit", + "Kapitelname": "Lehrperson", + "Subkapitel": "Einflüsse der Lehrperson", + "Effektstärke": 1.09, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 9.13, + "Stichwort": "Micro-Teaching", + "Kapitelname": "Lehrperson", + "Subkapitel": "Entwicklung von Lehrerprofessionalität", + "Effektstärke": 1.01, + "Systemebene": "sozial" + }, + { + "Thermometer_ID": 10.3, + "Stichwort": "Ergebnisorientierte Bildung", + "Kapitelname": "Curriculum", + "Subkapitel": "Andere Lehrplanbereiche", + "Effektstärke": 0.97, + "Systemebene": "sozial" + }, + { + "Thermometer_ID": 5.01, + "Stichwort": "Vorausgehende Fähigkeiten & Intelligenz", + "Kapitelname": "Lernende", + "Subkapitel": "Fähigkeiten", + "Effektstärke": 0.96, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 5.11, + "Stichwort": "Beurteilung der eigenen Leistungsfähigkeit", + "Kapitelname": "Lernende", + "Subkapitel": "Fähigkeiten", + "Effektstärke": 0.96, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 5.1, + "Stichwort": "Feldunabhängigkeit", + "Kapitelname": "Lernende", + "Subkapitel": "Fähigkeiten", + "Effektstärke": 0.94, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 9.07, + "Stichwort": "Klarheit der Lehrperson", + "Kapitelname": "Lehrperson", + "Subkapitel": "Einflüsse der Lehrperson", + "Effektstärke": 0.85, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 5.13, + "Stichwort": "Kritisches Denken", + "Kapitelname": "Lernende", + "Subkapitel": "Fähigkeiten", + "Effektstärke": 0.84, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 8.13, + "Stichwort": "Reduktion von Unterrichtsstörungen", + "Kapitelname": "Klassenzimmer", + "Subkapitel": "Einflüsse im Klassenzimmer", + "Effektstärke": 0.82, + "Systemebene": "sozial" + }, + { + "Thermometer_ID": 10.02, + "Stichwort": "Leseförderung für besondere Gruppen", + "Kapitelname": "Curriculum", + "Subkapitel": "Curriculare Programme (Lesen)", + "Effektstärke": 0.82, + "Systemebene": "sozial" + }, + { + "Thermometer_ID": 10.11, + "Stichwort": "Wiederholtes Lesen", + "Kapitelname": "Curriculum", + "Subkapitel": "Curriculare Programme (Lesen)", + "Effektstärke": 0.8, + "Systemebene": "sozial" + }, + { + "Thermometer_ID": 10.07, + "Stichwort": "Phonologische Bewusstheit", + "Kapitelname": "Curriculum", + "Subkapitel": "Curriculare Programme (Lesen)", + "Effektstärke": 0.75, + "Systemebene": "sozial" + } + ], + "top_negative": [ + { + "Thermometer_ID": 5.33, + "Stichwort": "negativ-aktivierend (Wut)", + "Kapitelname": "Lernende", + "Subkapitel": "Wille", + "Effektstärke": -0.65, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 5.43, + "Stichwort": "Misshandlung", + "Kapitelname": "Lernende", + "Subkapitel": "Thrill: Motivation", + "Effektstärke": -0.63, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 5.39, + "Stichwort": "Frühgeburt / Geburtsgewicht", + "Kapitelname": "Lernende", + "Subkapitel": "Thrill: Motivation", + "Effektstärke": -0.59, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 5.41, + "Stichwort": "Krankheit", + "Kapitelname": "Lernende", + "Subkapitel": "Thrill: Motivation", + "Effektstärke": -0.51, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 5.36, + "Stichwort": "negativ-aktivierend (Langeweile)", + "Kapitelname": "Lernende", + "Subkapitel": "Wille", + "Effektstärke": -0.46, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 5.42, + "Stichwort": "Körperliche Syndrome", + "Kapitelname": "Lernende", + "Subkapitel": "Thrill: Motivation", + "Effektstärke": -0.42, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 5.38, + "Stichwort": "kognitive Dispositionen (Prokrastination)", + "Kapitelname": "Lernende", + "Subkapitel": "Wille", + "Effektstärke": -0.41, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 5.31, + "Stichwort": "negativ-aktivierend (Angst)", + "Kapitelname": "Lernende", + "Subkapitel": "Wille", + "Effektstärke": -0.4, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 6.17, + "Stichwort": "Schulwechsel", + "Kapitelname": "Elternhaus und Familie", + "Subkapitel": "Familiäre Ressourcen", + "Effektstärke": -0.38, + "Systemebene": "sozial" + }, + { + "Thermometer_ID": 6.15, + "Stichwort": "Körperliche Züchtigung", + "Kapitelname": "Elternhaus und Familie", + "Subkapitel": "Familiäre Ressourcen", + "Effektstärke": -0.33, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 5.32, + "Stichwort": "negativ-aktivierend (Depressionen)", + "Kapitelname": "Lernende", + "Subkapitel": "Wille", + "Effektstärke": -0.3, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 5.49, + "Stichwort": "Dachloser Dialekt", + "Kapitelname": "Lernende", + "Subkapitel": "Thrill: Motivation", + "Effektstärke": -0.29, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 8.12, + "Stichwort": "(Cyber-)Bulling", + "Kapitelname": "Klassenzimmer", + "Subkapitel": "Einflüsse im Klassenzimmer", + "Effektstärke": -0.28, + "Systemebene": "sozial" + }, + { + "Thermometer_ID": 6.07, + "Stichwort": "Geschieden", + "Kapitelname": "Elternhaus und Familie", + "Subkapitel": "Familiäre Ressourcen", + "Effektstärke": -0.26, + "Systemebene": "sozial" + }, + { + "Thermometer_ID": 8.24, + "Stichwort": "Unbeliebtheit in der Klasse", + "Kapitelname": "Klassenzimmer", + "Subkapitel": "Klassenklima", + "Effektstärke": -0.26, + "Systemebene": "sozial" + } + ] + }, + "item_projection": { + "n_nodes": 169, + "n_edges": 7238, + "n_communities": 2 + }, "meta": { "theme": "dark", "min_abs_d": 0.0, diff --git a/export/network_top_extremes.json b/export/network_top_extremes.json new file mode 100644 index 0000000..01a93b5 --- /dev/null +++ b/export/network_top_extremes.json @@ -0,0 +1,246 @@ +{ + "top_positive": [ + { + "Thermometer_ID": 9.08, + "Stichwort": "Kollektive Wirksamkeitserwartung", + "Kapitelname": "Lehrperson", + "Subkapitel": "Einflüsse der Lehrperson", + "Effektstärke": 1.34, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 9.05, + "Stichwort": "Einschätzung des Leistungsniveaus durch die Lehrperson", + "Kapitelname": "Lehrperson", + "Subkapitel": "Einflüsse der Lehrperson", + "Effektstärke": 1.3, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 5.05, + "Stichwort": "Erkenntnisstufen", + "Kapitelname": "Lernende", + "Subkapitel": "Fähigkeiten", + "Effektstärke": 1.28, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 9.06, + "Stichwort": "Glaubwürdigkeit", + "Kapitelname": "Lehrperson", + "Subkapitel": "Einflüsse der Lehrperson", + "Effektstärke": 1.09, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 9.13, + "Stichwort": "Micro-Teaching", + "Kapitelname": "Lehrperson", + "Subkapitel": "Entwicklung von Lehrerprofessionalität", + "Effektstärke": 1.01, + "Systemebene": "sozial" + }, + { + "Thermometer_ID": 10.3, + "Stichwort": "Ergebnisorientierte Bildung", + "Kapitelname": "Curriculum", + "Subkapitel": "Andere Lehrplanbereiche", + "Effektstärke": 0.97, + "Systemebene": "sozial" + }, + { + "Thermometer_ID": 5.01, + "Stichwort": "Vorausgehende Fähigkeiten & Intelligenz", + "Kapitelname": "Lernende", + "Subkapitel": "Fähigkeiten", + "Effektstärke": 0.96, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 5.11, + "Stichwort": "Beurteilung der eigenen Leistungsfähigkeit", + "Kapitelname": "Lernende", + "Subkapitel": "Fähigkeiten", + "Effektstärke": 0.96, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 5.1, + "Stichwort": "Feldunabhängigkeit", + "Kapitelname": "Lernende", + "Subkapitel": "Fähigkeiten", + "Effektstärke": 0.94, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 9.07, + "Stichwort": "Klarheit der Lehrperson", + "Kapitelname": "Lehrperson", + "Subkapitel": "Einflüsse der Lehrperson", + "Effektstärke": 0.85, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 5.13, + "Stichwort": "Kritisches Denken", + "Kapitelname": "Lernende", + "Subkapitel": "Fähigkeiten", + "Effektstärke": 0.84, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 8.13, + "Stichwort": "Reduktion von Unterrichtsstörungen", + "Kapitelname": "Klassenzimmer", + "Subkapitel": "Einflüsse im Klassenzimmer", + "Effektstärke": 0.82, + "Systemebene": "sozial" + }, + { + "Thermometer_ID": 10.02, + "Stichwort": "Leseförderung für besondere Gruppen", + "Kapitelname": "Curriculum", + "Subkapitel": "Curriculare Programme (Lesen)", + "Effektstärke": 0.82, + "Systemebene": "sozial" + }, + { + "Thermometer_ID": 10.11, + "Stichwort": "Wiederholtes Lesen", + "Kapitelname": "Curriculum", + "Subkapitel": "Curriculare Programme (Lesen)", + "Effektstärke": 0.8, + "Systemebene": "sozial" + }, + { + "Thermometer_ID": 10.07, + "Stichwort": "Phonologische Bewusstheit", + "Kapitelname": "Curriculum", + "Subkapitel": "Curriculare Programme (Lesen)", + "Effektstärke": 0.75, + "Systemebene": "sozial" + } + ], + "top_negative": [ + { + "Thermometer_ID": 5.33, + "Stichwort": "negativ-aktivierend (Wut)", + "Kapitelname": "Lernende", + "Subkapitel": "Wille", + "Effektstärke": -0.65, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 5.43, + "Stichwort": "Misshandlung", + "Kapitelname": "Lernende", + "Subkapitel": "Thrill: Motivation", + "Effektstärke": -0.63, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 5.39, + "Stichwort": "Frühgeburt / Geburtsgewicht", + "Kapitelname": "Lernende", + "Subkapitel": "Thrill: Motivation", + "Effektstärke": -0.59, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 5.41, + "Stichwort": "Krankheit", + "Kapitelname": "Lernende", + "Subkapitel": "Thrill: Motivation", + "Effektstärke": -0.51, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 5.36, + "Stichwort": "negativ-aktivierend (Langeweile)", + "Kapitelname": "Lernende", + "Subkapitel": "Wille", + "Effektstärke": -0.46, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 5.42, + "Stichwort": "Körperliche Syndrome", + "Kapitelname": "Lernende", + "Subkapitel": "Thrill: Motivation", + "Effektstärke": -0.42, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 5.38, + "Stichwort": "kognitive Dispositionen (Prokrastination)", + "Kapitelname": "Lernende", + "Subkapitel": "Wille", + "Effektstärke": -0.41, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 5.31, + "Stichwort": "negativ-aktivierend (Angst)", + "Kapitelname": "Lernende", + "Subkapitel": "Wille", + "Effektstärke": -0.4, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 6.17, + "Stichwort": "Schulwechsel", + "Kapitelname": "Elternhaus und Familie", + "Subkapitel": "Familiäre Ressourcen", + "Effektstärke": -0.38, + "Systemebene": "sozial" + }, + { + "Thermometer_ID": 6.15, + "Stichwort": "Körperliche Züchtigung", + "Kapitelname": "Elternhaus und Familie", + "Subkapitel": "Familiäre Ressourcen", + "Effektstärke": -0.33, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 5.32, + "Stichwort": "negativ-aktivierend (Depressionen)", + "Kapitelname": "Lernende", + "Subkapitel": "Wille", + "Effektstärke": -0.3, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 5.49, + "Stichwort": "Dachloser Dialekt", + "Kapitelname": "Lernende", + "Subkapitel": "Thrill: Motivation", + "Effektstärke": -0.29, + "Systemebene": "psychisch" + }, + { + "Thermometer_ID": 8.12, + "Stichwort": "(Cyber-)Bulling", + "Kapitelname": "Klassenzimmer", + "Subkapitel": "Einflüsse im Klassenzimmer", + "Effektstärke": -0.28, + "Systemebene": "sozial" + }, + { + "Thermometer_ID": 6.07, + "Stichwort": "Geschieden", + "Kapitelname": "Elternhaus und Familie", + "Subkapitel": "Familiäre Ressourcen", + "Effektstärke": -0.26, + "Systemebene": "sozial" + }, + { + "Thermometer_ID": 8.24, + "Stichwort": "Unbeliebtheit in der Klasse", + "Kapitelname": "Klassenzimmer", + "Subkapitel": "Klassenklima", + "Effektstärke": -0.26, + "Systemebene": "sozial" + } + ] +} \ No newline at end of file diff --git a/visible-learning netzwerkanalyse.py b/visible-learning netzwerkanalyse.py index 2161d21..3233338 100644 --- a/visible-learning netzwerkanalyse.py +++ b/visible-learning netzwerkanalyse.py @@ -38,6 +38,11 @@ from config_visible_learning import ( export_fig_visual, export_fig_png, theme, + z_mode, + z_axis_labels, + show_item_projection, + show_community_labels, + top_n_extremes, ) # ----------------------------------------- @@ -55,6 +60,30 @@ except Exception: _styles = {} _colors = {} +# ----------------------------------------- +# Config-Fallbacks (falls Keys fehlen) +# ----------------------------------------- +try: + _Z_MODE = z_mode +except Exception: + _Z_MODE = "effekt" +try: + _Z_AXIS_LABELS = z_axis_labels +except Exception: + _Z_AXIS_LABELS = {"effekt": "Effektstärke (Cohen d)", "kapitel": "Kapitel (numerischer Index)", "system": "Systemebene (0 = Psychisch, 1 = Sozial)"} +try: + _SHOW_ITEM_PROJECTION = show_item_projection +except Exception: + _SHOW_ITEM_PROJECTION = True +try: + _SHOW_COMMUNITY_LABELS = show_community_labels +except Exception: + _SHOW_COMMUNITY_LABELS = True +try: + _TOP_N_EXTREMES = int(top_n_extremes) +except Exception: + _TOP_N_EXTREMES = 15 + # ----------------------------------------- # Export-Helfer # ----------------------------------------- @@ -106,6 +135,29 @@ def load_data(path: str) -> pd.DataFrame: df["Kapitel"] = None return df +# ----------------------------------------- +# Top-Listen (positiv/negativ) +# ----------------------------------------- + +def top_extremes(df: pd.DataFrame, n: int = 15) -> dict: + data = df.copy() + data = data[data["Systemebene"].astype(str).str.lower().isin(["psychisch", "sozial"])] + data = data.dropna(subset=["Effektstärke"]) # Sicherheit + pos = data.sort_values("Effektstärke", ascending=False).head(n) + neg = data.sort_values("Effektstärke", ascending=True).head(n) + # Konsole + print(f"\nTop +{n} (positiv):") + for _, r in pos.iterrows(): + print(f" {r['Thermometer_ID']}: {r['Stichwort']} | d={float(r['Effektstärke']):.2f}") + print(f"\nTop -{n} (negativ):") + for _, r in neg.iterrows(): + print(f" {r['Thermometer_ID']}: {r['Stichwort']} | d={float(r['Effektstärke']):.2f}") + return { + "top_positive": pos[["Thermometer_ID","Stichwort","Kapitelname","Subkapitel","Effektstärke","Systemebene"]].to_dict(orient="records"), + "top_negative": neg[["Thermometer_ID","Stichwort","Kapitelname","Subkapitel","Effektstärke","Systemebene"]].to_dict(orient="records"), + } + + # ----------------------------------------- # Netzwerk bauen # ----------------------------------------- @@ -161,6 +213,103 @@ def build_bipartite_graph( ) return G +# ----------------------------------------- +# Item-Projektion (bipartit -> Item-Item) + Communities +# ----------------------------------------- +from networkx.algorithms import community as nx_comm + +def build_item_projection(G: nx.Graph) -> tuple[nx.Graph, dict[str,int], list[set]]: + """Projiziert das bipartite Netz auf die Item-Seite. Zwei Items werden verbunden, + wenn sie dasselbe System teilen. Kanten-Gewicht = min(|w_i|, |w_j|). + Liefert das Item-Graph, ein Mapping node->community_id und die Community-Mengen. + """ + # Item- und System-Knoten bestimmen + items = [n for n, d in G.nodes(data=True) if d.get("bipartite") == "item"] + systems = [n for n, d in G.nodes(data=True) if d.get("bipartite") == "system"] + # Zuordnung: System -> Liste (item, |weight|) + sys_to_items: dict[str, list[tuple[str,float]]] = {} + for s in systems: + sys_to_items[s] = [] + for u, v, d in G.edges(data=True): + if u in systems and v in items: + sys_to_items[u].append((v, abs(float(d.get("weight",0.0))))) + elif v in systems and u in items: + sys_to_items[v].append((u, abs(float(d.get("weight",0.0))))) + # Item-Graph aufbauen + Gi = nx.Graph() + for it in items: + nd = G.nodes[it] + Gi.add_node(it, **nd) + for s, lst in sys_to_items.items(): + # Alle Paare innerhalb desselben Systems verbinden + for i in range(len(lst)): + for j in range(i+1, len(lst)): + a, wa = lst[i] + b, wb = lst[j] + w = min(wa, wb) + if Gi.has_edge(a,b): + Gi[a][b]["weight"] += w + else: + Gi.add_edge(a, b, weight=w) + if Gi.number_of_edges() == 0: + return Gi, {}, [] + # Communities (gewichtete Modularity, Greedy) + coms = nx_comm.greedy_modularity_communities(Gi, weight="weight") + node2com: dict[str,int] = {} + for cid, members in enumerate(coms): + for n in members: + node2com[n] = cid + return Gi, node2com, [set(c) for c in coms] + + +def plot_item_projection(Gi: nx.Graph, node2com: dict[str,int], title: str = "Item-Projektion (Communities)"): + if Gi.number_of_nodes() == 0: + print("Hinweis: Item-Projektion leer (zu wenig Überlappung).") + return + pos = nx.spring_layout(Gi, seed=42, weight="weight") + # Communities zu Traces gruppieren + com_to_nodes: dict[int, list[str]] = {} + for n in Gi.nodes(): + cid = node2com.get(n, -1) + com_to_nodes.setdefault(cid, []).append(n) + traces = [] + # Farb-/Markerstile aus CI (zyklisch) + style_keys = [ + "marker_accent", "marker_brightArea", "marker_depthArea", + "marker_positiveHighlight", "marker_negativeHighlight", + "marker_primaryLine", "marker_secondaryLine" + ] + keys_cycle = style_keys * 10 + for idx, (cid, nodes) in enumerate(sorted(com_to_nodes.items(), key=lambda t: t[0])): + xs = [pos[n][0] for n in nodes] + ys = [pos[n][1] for n in nodes] + htxt = [] + for n in nodes: + nd = Gi.nodes[n] + htxt.append( + "Thermometer: " + str(nd.get("id","")) + + "
Stichwort: " + str(nd.get("label","")) + + "
Kapitel: " + str(nd.get("kapitelname","")) + + "
Subkapitel: " + str(nd.get("subkapitel","")) + + "
d: " + f"{nd.get('d',np.nan):.2f}" + ) + mk = _styles.get(keys_cycle[idx], dict(size=8)) + traces.append(go.Scatter( + x=xs, y=ys, mode="markers+text" if _SHOW_COMMUNITY_LABELS else "markers", + marker={**mk, "size": 9}, + text=[str(node2com.get(n, -1)) if _SHOW_COMMUNITY_LABELS else None for n in nodes], + textposition="top center", + hovertext=htxt, + hovertemplate="%{hovertext}", + name=f"Community {cid}" + )) + fig = go.Figure(data=traces) + fig.update_layout(_ci_layout(title)) + fig.update_xaxes(title_text="Semantische Position X (Projektion)", showticklabels=False, showgrid=False, zeroline=False) + fig.update_yaxes(title_text="Semantische Position Y (Projektion)", showticklabels=False, showgrid=False, zeroline=False) + fig.show() + export_figure(fig, "vl-network-item-projection") + # ----------------------------------------- # Layout & Visualisierung (Plotly) # ----------------------------------------- @@ -188,13 +337,13 @@ def plot_network(G: nx.Graph, title: str = "Netzwerk: Systemebenen × Thermomete x_pos, y_pos = _edge_segments(G, pos, sign="pos") x_neg, y_neg = _edge_segments(G, pos, sign="neg") - line_primary = _styles.get("linie_primaryLine", dict(width=1)) - line_secondary = _styles.get("linie_secondaryLine", dict(width=1)) + line_positive = _styles.get("linie_positiveHighlight", dict(width=1)) + line_negative = _styles.get("linie_negativeHighlight", dict(width=1)) edge_pos = go.Scatter( x=x_pos, y=y_pos, mode="lines", - line=line_primary, + line=line_positive, hoverinfo="skip", showlegend=True, name="Kanten (d ≥ 0)" @@ -202,14 +351,14 @@ def plot_network(G: nx.Graph, title: str = "Netzwerk: Systemebenen × Thermomete edge_neg = go.Scatter( x=x_neg, y=y_neg, mode="lines", - line=line_secondary, + line=line_negative, hoverinfo="skip", showlegend=True, name="Kanten (d < 0)" ) # System-Knoten: Marker aus CI (z. B. accent) - sys_marker = _styles.get("marker_accent", dict(size=18)) + sys_marker = _styles.get("marker_primaryLine", dict(size=18)) sys_x = [pos[n][0] for n in system_nodes] sys_y = [pos[n][1] for n in system_nodes] sys_text = [G.nodes[n].get("label", n) for n in system_nodes] @@ -225,7 +374,7 @@ def plot_network(G: nx.Graph, title: str = "Netzwerk: Systemebenen × Thermomete ) # Item-Knoten: Marker aus CI (z. B. brightArea); Größe ~ |degree_weight| - item_marker = _styles.get("marker_brightArea", dict(size=10)) + item_marker = _styles.get("marker_secondaryLine", dict(size=10)) it_x = [pos[n][0] for n in item_nodes] it_y = [pos[n][1] for n in item_nodes] @@ -266,13 +415,195 @@ def plot_network(G: nx.Graph, title: str = "Netzwerk: Systemebenen × Thermomete ) fig = go.Figure(data=[edge_pos, edge_neg, systems_trace, items_trace]) + # CI-Layout und inhaltliche Achsentitel (2D: Semantische Position aus Layout) fig.update_layout(_ci_layout(title)) - # Achsen & Grid neutral halten, keine Beschriftungen im Plot (alles im Hover) - fig.update_xaxes(showticklabels=False, showgrid=False, zeroline=False) - fig.update_yaxes(showticklabels=False, showgrid=False, zeroline=False) + fig.update_xaxes( + title_text="Semantische Position X (Layout)", + showticklabels=False, showgrid=False, zeroline=False + ) + fig.update_yaxes( + title_text="Semantische Position Y (Layout)", + showticklabels=False, showgrid=False, zeroline=False + ) fig.show() export_figure(fig, "vl-network") +def _edge_segments_3d(G: nx.Graph, pos_xy: dict[str, tuple[float, float]], z_map: dict[str, float], sign: str | None = None): + """Erzeugt x,y,z-Koordinaten-Listen für 3D-Liniensegmente (mit None-Trennern). Optional nach Vorzeichen filtern.""" + xs, ys, zs = [], [], [] + for u, v, d in G.edges(data=True): + if sign and d.get("sign") != sign: + continue + x0, y0 = pos_xy[u] + x1, y1 = pos_xy[v] + z0 = float(z_map.get(u, 0.0)) + z1 = float(z_map.get(v, 0.0)) + xs += [x0, x1, None] + ys += [y0, y1, None] + zs += [z0, z1, None] + return xs, ys, zs + + +def plot_network_3d(G: nx.Graph, z_mode: str = "effekt", title: str = "3D: Systemebenen × Thermometer", seed: int = 42): + """ + Semantische 3D-Ansicht: + - z_mode = "effekt": z = Effektstärke (Items), Systeme z=0 + - z_mode = "kapitel": z = Kapitelnummer (Items), Systeme unterhalb der Items (min_z - 0.5) + - z_mode = "system": z = 0 (psychisch), 1 (sozial), Items = Mittelwert ihrer Systemnachbarn + x/y stammen aus einem 2D-Spring-Layout (stabile, gut lesbare Projektion), z ist semantisch belegt. + """ + styles = _styles + colors = _colors + + # 2D-Layout für X/Y (stabile Projektion) + pos_xy = nx.spring_layout(G, seed=seed, k=None, weight="weight", dim=2) + + # Z-Koordinaten je Knoten ermitteln + z_map: dict[str, float] = {} + if z_mode == "effekt": + for n, d in G.nodes(data=True): + if d.get("bipartite") == "item": + z_map[n] = float(d.get("d", 0.0)) + else: + z_map[n] = 0.0 + elif z_mode == "kapitel": + item_z_vals = [] + for n, d in G.nodes(data=True): + if d.get("bipartite") == "item": + try: + # Kapitelnummer aus Kapitelname kann alphanumerisch sein; wir nutzen, wenn vorhanden, numerische "Kapitel" + # Falls keine numerische Kapitelspalte existiert, wird 0 gesetzt. + kap = d.get("kapitelname", "") + # Fallback: im Nodes-Attribut existiert keine numerische Kapitelnummer; daher 0 + z_map[n] = float(d.get("kapitel", 0.0)) if "kapitel" in d else 0.0 + except Exception: + z_map[n] = 0.0 + item_z_vals.append(z_map[n]) + min_z = min(item_z_vals) if item_z_vals else 0.0 + for n, d in G.nodes(data=True): + if d.get("bipartite") == "system": + z_map[n] = float(min_z) - 0.5 + elif z_mode == "system": + # Systeme klar trennen + for n, d in G.nodes(data=True): + if d.get("bipartite") == "system": + lbl = str(d.get("label", "")).strip().lower() + z_map[n] = 0.0 if "psych" in lbl else 1.0 + # Items: Mittelwert der z-Werte ihrer System-Nachbarn (im bipartiten Graphen genau einer) + for n, d in G.nodes(data=True): + if d.get("bipartite") == "item": + zs = [] + for nbr in G[n]: + zs.append(z_map.get(nbr, 0.0)) + z_map[n] = float(np.mean(zs)) if zs else 0.0 + else: + # Unbekannter Modus -> alle 0 + z_map = {n: 0.0 for n in G.nodes()} + + # Knotenlisten + system_nodes = [n for n, d in G.nodes(data=True) if d.get("bipartite") == "system"] + item_nodes = [n for n, d in G.nodes(data=True) if d.get("bipartite") == "item"] + + # Kanten (pos/neg) vorbereiten + x_pos, y_pos, z_pos = _edge_segments_3d(G, pos_xy, z_map, sign="pos") + x_neg, y_neg, z_neg = _edge_segments_3d(G, pos_xy, z_map, sign="neg") + + line_positive = styles.get("linie_positiveHighlight", dict(width=1)) + line_negative = styles.get("linie_negativeHighlight", dict(width=1)) + + edge_pos = go.Scatter3d( + x=x_pos, y=y_pos, z=z_pos, + mode="lines", + line=line_positive, + hoverinfo="skip", + showlegend=True, + name="Kanten (d ≥ 0)" + ) + edge_neg = go.Scatter3d( + x=x_neg, y=y_neg, z=z_neg, + mode="lines", + line=line_negative, + hoverinfo="skip", + showlegend=True, + name="Kanten (d < 0)" + ) + + # System-Knoten + sys_marker = styles.get("marker_primaryLine", dict(size=18)) + sys_x = [pos_xy[n][0] for n in system_nodes] + sys_y = [pos_xy[n][1] for n in system_nodes] + sys_z = [z_map[n] for n in system_nodes] + sys_text = [G.nodes[n].get("label", n) for n in system_nodes] + sys_hover = [f"Systemebene: {G.nodes[n].get('label','')}" for n in system_nodes] + + systems_trace = go.Scatter3d( + x=sys_x, y=sys_y, z=sys_z, mode="markers", + marker={**sys_marker, "size": 10}, + text=sys_text, + hovertext=sys_hover, + hovertemplate="%{hovertext}", + name="System" + ) + + # Item-Knoten: Thermometer im Sekundärstil (gleiches Marker-Design für +/-); Kanten behalten Vorzeichenfarben + pos_marker = styles.get("marker_secondaryLine", dict(size=6)) + neg_marker = styles.get("marker_secondaryLine", dict(size=6)) + + pos_x, pos_y, pos_z, pos_hover = [], [], [], [] + neg_x, neg_y, neg_z, neg_hover = [], [], [], [] + for n in item_nodes: + x, y = pos_xy[n] + z = z_map[n] + nd = G.nodes[n] + hover = ( + "Thermometer: " + str(nd.get("id","")) + + "
Stichwort: " + str(nd.get("label","")) + + "
Kapitel: " + str(nd.get("kapitelname","")) + + "
Subkapitel: " + str(nd.get("subkapitel","")) + + "
d: " + f"{nd.get('d',np.nan):.2f}" + ) + if float(nd.get("d", 0.0)) >= 0: + pos_x.append(x); pos_y.append(y); pos_z.append(z); pos_hover.append(hover) + else: + neg_x.append(x); neg_y.append(y); neg_z.append(z); neg_hover.append(hover) + + items_pos_trace = go.Scatter3d( + x=pos_x, y=pos_y, z=pos_z, mode="markers", + marker=pos_marker, + hovertext=pos_hover, + hovertemplate="%{hovertext}", + name="Thermometer (d ≥ 0)" + ) + items_neg_trace = go.Scatter3d( + x=neg_x, y=neg_y, z=neg_z, mode="markers", + marker=neg_marker, + hovertext=neg_hover, + hovertemplate="%{hovertext}", + name="Thermometer (d < 0)" + ) + + fig = go.Figure(data=[edge_pos, edge_neg, systems_trace, items_pos_trace, items_neg_trace]) + fig.update_layout(_ci_layout(f"{title} – z: {z_mode}")) + # Achsentitel mit inhaltlicher Bedeutung setzen + z_title = _Z_AXIS_LABELS.get(z_mode, "Z") + + fig.update_scenes( + xaxis=dict( + title="Semantische Position X (Layout)", + showticklabels=False, showgrid=False, zeroline=False + ), + yaxis=dict( + title="Semantische Position Y (Layout)", + showticklabels=False, showgrid=False, zeroline=False + ), + zaxis=dict( + title=z_title, + showticklabels=False, showgrid=False, zeroline=False + ), + ) + fig.show() + export_figure(fig, f"vl-network-3d-{z_mode}") + # ----------------------------------------- # Einfache Metriken & Export # ----------------------------------------- @@ -311,7 +642,8 @@ def run_network_analysis( min_abs_d: float = 0.00, kapitel_filter: list[int] | None = None, subkapitel_filter: list[str] | None = None, - seed: int = 42 + seed: int = 42, + z_mode: str = "effekt" ): df = load_data(csv_path) # Datenqualität knapp loggen @@ -333,14 +665,34 @@ def run_network_analysis( plot_network(G, title="Netzwerk: Systemebenen × Thermometer (Kanten: Effektstärke)", seed=seed) + # 3D-Ansicht mit semantischer z-Achse + plot_network_3d(G, z_mode=z_mode, title="Netzwerk (3D): semantische z-Achse", seed=seed) + summary = summarize_network(G) print("\nSystemgewicht-Summen:", summary["system_weight_sums"]) print("\nTop-Items (weighted degree):") for r in summary["top_items_by_weighted_degree"]: print(f" {r['Thermometer_ID']}: {r['Stichwort']} | d={r['Effektstärke']:.2f} | wd={r['weighted_degree_abs']:.2f}") + # Top-Listen exportieren + extremes = top_extremes(df, n=_TOP_N_EXTREMES) + export_json(extremes, "network_top_extremes.json") + + # Item-Projektion + Communities (optional) + item_proj_summary = {} + if _SHOW_ITEM_PROJECTION: + Gi, node2com, coms = build_item_projection(G) + plot_item_projection(Gi, node2com, title="Item-Projektion (Communities)") + item_proj_summary = { + "n_nodes": Gi.number_of_nodes(), + "n_edges": Gi.number_of_edges(), + "n_communities": len(coms), + } + # Export JSON payload = { + "extremes": extremes, + "item_projection": item_proj_summary, "meta": { "theme": theme, "min_abs_d": float(min_abs_d), @@ -384,6 +736,6 @@ if __name__ == "__main__": min_abs_d=0.00, kapitel_filter=None, subkapitel_filter=None, - seed=42 - ) - \ No newline at end of file + seed=42, + z_mode=_Z_MODE + ) \ No newline at end of file