In causal graphs, the dyad (two nodes linked by one directed edge) is the minimal unit of structure. Larger graphs (chains, forks, colliders, networks) are assemblies of dyads, not new kinds of atom (Peters et al. 2017). This chapter fixes that unit before Chapter 3 builds patterns and Chapter 4 attaches executable mechanisms.
4.2 The Dyad: Two Nodes, One Edge
A dyad \(X \rightarrow Y\) is the smallest structure that encodes a direct dependence of \(Y\) on \(X\). In an SCM it appears as
\[
Y \coloneqq f(X, U),
\]
where \(f\) is the local mechanism and \(U\) is exogenous noise for that equation. Under \(do(X = x)\) the input is fixed and the same mechanism produces \(Y \coloneqq f(x, U)\); the edge remains as a dependence of \(Y\) on the (now intervened) value of \(X\).
project_root =let current =pwd()while !isfile(joinpath(current, "Project.toml")) && !isfile(joinpath(current, "_quarto.yml")) parent =dirname(current) parent == current &&break current = parentend currentendinclude(joinpath(project_root, "scripts", "ensure_packages.jl"))@auto_using DAGMakie CairoMakie Graphs StableRNGsg_dyad =SimpleDiGraph(2)add_edge!(g_dyad, 1, 2) # X → Y
true
The dyad \(X \rightarrow Y\): one directed edge, one local mechanism.
4.3 Why dyads are fundamental
The graph \(G = (V, E)\) is exactly a set of dyads: each \(e \in E\) joins two vertices. Roles of nodes are read from incident edges:
Source: no incoming edges (often exogenous or background)
Sink: no outgoing edges (often an outcome or terminal measurement)
Intermediate: both in- and out-edges
"""Return a node's role from its incident edges."""functionnode_role(g::AbstractGraph, node::Integer) has_in = !isempty(inneighbors(g, node)) has_out = !isempty(outneighbors(g, node))if !has_in && has_outreturn"Source"elseif has_in && !has_outreturn"Sink"elseif has_in && has_outreturn"Intermediate"elsereturn"Isolated"endendg_roles =SimpleDiGraph(4)add_edge!(g_roles, 1, 2)add_edge!(g_roles, 2, 3)add_edge!(g_roles, 3, 4)println("Node roles (from incident edges):")for i in1:nv(g_roles)println(" Node ", i, ": ", node_role(g_roles, i))end
A short chain: node 1 is a source, 2–3 are intermediate, 4 is a sink.
4.4 Building from dyads
Three classical patterns are just overlapping dyads (Chapter 3):
Chain: \(X \rightarrow Y\) and \(Y \rightarrow Z\)
Fork: \(X \leftarrow Y \rightarrow Z\)
Collider: \(X \rightarrow Y \leftarrow Z\)
With \(n\) nodes there are \(n(n-1)\) possible directed dyads; realistic models keep only a sparse subset. That sparsity is both a modelling assumption and what makes many graph algorithms practical.
rng =StableRNG(2)n =10total_possible = n * (n -1)g_sparse =SimpleDiGraph(n)for i in1:nfor t inrand(rng, 1:n, rand(rng, 1:2))if t != iadd_edge!(g_sparse, i, t)endendendactual_edges =ne(g_sparse)println("System with ", n, " nodes:")println(" Possible directed dyads: ", total_possible)println(" Edges in a sparse draw: ", actual_edges)println(" Fraction absent: ", round(100* (1- actual_edges / total_possible); digits =1), "%")
System with 10 nodes:
Possible directed dyads: 90
Edges in a sparse draw: 13
Fraction absent: 85.6%
4.5 Summary
The dyad is the atom of causal structure: two nodes, one directed edge, one local mechanism \(Y \coloneqq f(X, U)\). Node roles follow incident edges; chains, forks, and colliders are assemblies of dyads; sparsity of possible edges is the usual case. Interventions fix the source of a dyad without inventing a new kind of relation.