Graph types#
The graph type says what a discovery result identifies: a directed model, a class of equivalent
models, or relations across time. Andrey records this in GraphStructure.kind or in a
TemporalStructure.
Every matrix below comes from the pictured graph’s to_numpy() method. A cell M[i, j] is the
mark at node i on its edge with j: 0 absent, 1 tail, 2 arrowhead, 3 circle. See
Graph representation to compare this convention with other packages.
DAG: a directed model#
A directed acyclic graph has a direction on every edge and no directed cycles. In a causal
model, X -> Y represents a direct effect relative to the variables in that model. Interpreting
a learned DAG causally depends on the method’s assumptions and the data.
| X | Y | Z | |
|---|---|---|---|
| X | 0 | 1 | 0 |
| Y | 2 | 0 | 1 |
| Z | 0 | 2 | 0 |
The chain X -> Y -> Z has marks (TAIL, ARROW) on each edge. Andrey stores it with kind="dag":
import numpy as np
from andrey import GraphStructure
dag = GraphStructure.from_numpy(
np.array([[0, 1, 0], [2, 0, 1], [0, 2, 0]], dtype=np.int8),
kind="dag",
labels=("X", "Y", "Z"),
)
assert dag.endpoints(0, 1) == (1, 2)
CPDAG: an equivalence class#
A completed partially directed acyclic graph represents DAGs with the same conditional independence relationships. Directed edges agree across that class; an undirected edge can point either way in different members. Chickering (2002) describes this representation.
| X | Y | Z | |
|---|---|---|---|
| X | 0 | 1 | 0 |
| Y | 1 | 0 | 1 |
| Z | 0 | 1 | 0 |
The chain X -- Y -- Z represents three DAGs: X -> Y -> Z, X <- Y <- Z, and
X <- Y -> Z. It excludes X -> Y <- Z, which has a collider at Y and different independence
relationships. Undirected edges cannot always be oriented independently.
Andrey uses kind="cpdag". The pair (TAIL, TAIL) means an unresolved direction, not two
opposite causal effects. PC and GES, among other methods, return this kind. GIES returns it too
for data with interventions, where the class holds only the DAGs that also agree on the data under
each set of intervened variables, so it can direct more edges
(Hauser and Buhlmann (2012)).
cpdag = GraphStructure.from_numpy(
np.array([[0, 1, 0], [1, 0, 1], [0, 1, 0]], dtype=np.int8),
kind="cpdag",
labels=("X", "Y", "Z"),
)
assert cpdag.endpoints(0, 1) == (1, 1)
Directed cycles: the kind matters#
Andrey’s kind="digraph" allows directed cycles and self-loops. The same endpoint pair (2, 2)
has different meanings in pag and digraph, and the renderer draws them differently:
| X | Y | |
|---|---|---|
| X | 0 | 2 |
| Y | 2 | 0 |
| X | Y | |
|---|---|---|
| X | 0 | 2 |
| Y | 2 | 0 |
In the PAG fragment, one edge has two arrowheads. In the directed graph, X -> Y and Y -> X
are separate directed edges. The endpoint matrix alone cannot distinguish them; retain kind
when saving or converting a graph.
Temporal graphs#
A TemporalStructure stores a graph for each lag. An edge X -> Y in lag(k) means
X(t-k) -> Y(t). Lag zero describes within-time relations. A self-loop at a positive lag means
a variable affects its own future value.
| X | Y | |
|---|---|---|
| X | 0 | 1 |
| Y | 2 | 0 |
| X | Y | |
|---|---|---|
| X | 0 | 2 |
| Y | 1 | 0 |
| X | Y | |
|---|---|---|
| X | 0 | 2 |
| Y | 2 | 0 |
Here, X(t-1) -> Y(t) occurs at lag 1 and Y(t-2) -> X(t) occurs at lag 2. Collapsing time
produces a directed cycle in summary_graph(), even though both original arrows point forward
in time. The summary retains each direction’s lags:
from andrey import TemporalStructure
lag1 = GraphStructure.from_numpy(np.array([[0, 1], [2, 0]]), kind="dag")
lag2 = GraphStructure.from_numpy(np.array([[0, 2], [1, 0]]), kind="dag")
temporal = TemporalStructure.from_lag_graphs([lag1, lag2], lags=[1, 2], labels=("X", "Y"))
summary = temporal.summary_graph()
assert summary.kind == "digraph"
assert summary.lags_of(0, 1) == (1,)
assert summary.lags_of(1, 0) == (2,)
lags lists lag values, so lag(2) selects lag 2, not the third array position. Granger
causality finds no effects within a time step, so its lags start at 1. lag_weights holds
autoregressive coefficients; VARMA can also provide lag_weights_ma for moving-average
coefficients.
Longitudinal results add an occasion axis. Use at(time, lag) and time_weights to inspect the
full panel. lag(k), lag_weights, and summary_graph() refer to the final occasion, not a
union over all occasions. NaN in time_weights marks a block that could not be computed.
Which methods return each type?#
Structure |
Methods |
|---|---|
|
|
|
|
|
|
|
|
cdnod returns kind cpdag, but its graph is only partially directed: the domain index orients
edges that the CPDAG of observational data leaves undirected, so score it against the graph CD-NOD
can identify rather than the true DAG’s CPDAG.
multi_group_direct_lingam returns a list of results, one per group. GIN includes explicit latent
nodes, marked by node_types (OBSERVED=0, LATENT=1). ANM and PNL are direction tests on two
variables: each returns a two-node dag with the direction its tests decide, or no edge, and
both p-values and test statistics in metadata.
Draw any GraphStructure, including a temporal lag or summary, with andrey.viz.draw:
from pathlib import Path
from andrey.viz import draw
Path("graph.svg").write_text(draw(pag), encoding="utf-8")