Netzwerkanalyse erweitert:

- 3D-Visualisierung mit Effekt-Achse (z-Modi: Effekt, System, Semantik)
- Top-Listen (je 15 positive/negative Effektstärken) ergänzt
- Item-Projektion mit Community-Labels
- Config um Toggle für z-Modi mit sprechenden Achsentiteln erweitert
- Keys konsistent (top_n_extremes, show_item_projection)
This commit is contained in:
2025-09-03 23:53:41 +02:00
parent eef369d025
commit aab5c683b9
4 changed files with 887 additions and 32 deletions

View File

@ -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)

View File

@ -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,

View File

@ -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"
}
]
}

View File

@ -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","")) +
"<br>Stichwort: " + str(nd.get("label","")) +
"<br>Kapitel: " + str(nd.get("kapitelname","")) +
"<br>Subkapitel: " + str(nd.get("subkapitel","")) +
"<br>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}<extra></extra>",
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 &lt; 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}<extra></extra>",
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","")) +
"<br>Stichwort: " + str(nd.get("label","")) +
"<br>Kapitel: " + str(nd.get("kapitelname","")) +
"<br>Subkapitel: " + str(nd.get("subkapitel","")) +
"<br>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}<extra></extra>",
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}<extra></extra>",
name="Thermometer (d &lt; 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
seed=42,
z_mode=_Z_MODE
)