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