name: cai-causal-graph description: | Define, manipulate, and serialize causal graph structures (DAG, CPDAG, MAG, PAG) using cai-causal-graph. Provides CausalGraph, Node, Edge, Skeleton classes with variable type support (CONTINUOUS, BINARY, MULTICLASS, ORDINAL). Use when user imports from cai_causal_graph, works with causal graphs, needs graph conversion (NetworkX, adjacency matrix, dict), or mentions DAG/CPDAG/PAG/MAG representations. Do NOT trigger for graph neural networks or knowledge graphs unrelated to causality. license: Apache-2.0 metadata: author: causalens version: "0.5.14" sdk_schema_version: "0.1.0" sdk_dependencies: "cai-causal-graph>=0.5.0"
CAI Causal Graph
Python library for defining, manipulating, and serializing causal graph structures (DAG, CPDAG, MAG, PAG).
Quick Reference
| Class | Module | Purpose |
|---|---|---|
CausalGraph |
cai_causal_graph.causal_graph |
Core graph: nodes + typed edges, DAG validation |
Skeleton |
cai_causal_graph.causal_graph |
Undirected view of a CausalGraph (no edge direction) |
TimeSeriesCausalGraph |
cai_causal_graph.time_series_causal_graph |
Subclass of CausalGraph for lagged time-series nodes |
Node |
cai_causal_graph.graph_components |
Single node with identifier and variable_type |
Edge |
cai_causal_graph.graph_components |
Single edge with source, destination, edge_type |
EdgeType |
cai_causal_graph.type_definitions |
Enum of all valid edge types |
NodeVariableType |
cai_causal_graph.type_definitions |
Enum of variable types for nodes |
EdgeConstraint |
cai_causal_graph.type_definitions |
Enum for domain-knowledge constraints |
All public classes are re-exported from cai_causal_graph directly:
from cai_causal_graph import CausalGraph, TimeSeriesCausalGraph, EdgeType, NodeVariableType, Skeleton
Graph Types
Choose the graph representation based on what the edges mean:
| Need | Graph Type | Edge constraint |
|---|---|---|
| Fully directed, no cycles | DAG | All edges -> |
| Partially directed (discovery output) | CPDAG | Mix of -> and -- |
| Latent confounders allowed | MAG | Has <> bidirected edges |
| Maximal uncertainty from FCI | PAG | Has o>, o-, oo edges |
CausalGraph enforces acyclicity only over directed edges. It does not enforce which combination of edge types is
"valid" for a given graph type — that is the caller's responsibility.
Edge Semantics
EdgeType values and their string representations:
EdgeType member |
String | Description |
|---|---|---|
DIRECTED_EDGE |
-> |
Causal direction from source to destination |
UNDIRECTED_EDGE |
-- |
Undirected; semantics depend on graph type (see gotchas) |
BIDIRECTED_EDGE |
<> |
Latent common cause (MAG/PAG) |
UNKNOWN_DIRECTED_EDGE |
o> |
PAG: circle mark on tail side |
UNKNOWN_UNDIRECTED_EDGE |
o- |
PAG: circle mark, undirected |
UNKNOWN_EDGE |
oo |
PAG: both endpoints ambiguous |
Critical edge semantics differences by graph type:
| Edge | In CPDAG | In MAG | In PAG |
|---|---|---|---|
-- (undirected) |
Orientation unknown — can resolve to -> or <- |
Implies selection bias | Not used |
<> (bidirected) |
Not used | Latent common confounder | Latent common confounder |
o>, o-, oo |
Not used | Not used | Ambiguous PAG-specific marks |
Node Variable Types
NodeVariableType values:
| Member | String value | Meaning |
|---|---|---|
UNSPECIFIED |
'unspecified' |
Default — type not declared |
CONTINUOUS |
'continuous' |
Real-valued variable |
BINARY |
'binary' |
Two-valued variable |
MULTICLASS |
'multiclass' |
Categorical with 3+ classes |
ORDINAL |
'ordinal' |
Ordered categorical |
variable_type defaults to UNSPECIFIED and is mutable post-creation:
from cai_causal_graph import CausalGraph, NodeVariableType
cg = CausalGraph()
cg.add_node('x', variable_type=NodeVariableType.CONTINUOUS)
node = cg.get_node('x')
node.variable_type = NodeVariableType.BINARY # mutable
UNSPECIFIED is dangerous — downstream tools may silently treat it as continuous.
Core Usage
Building a graph
from cai_causal_graph import CausalGraph, EdgeType, NodeVariableType
cg = CausalGraph()
# Nodes are auto-created when adding edges
cg.add_edge('x', 'y') # default: DIRECTED_EDGE
cg.add_edge('z', 'y', edge_type=EdgeType.DIRECTED_EDGE)
cg.add_edge('a', 'b', edge_type=EdgeType.UNDIRECTED_EDGE) # CPDAG undirected
# Or add nodes explicitly first
cg.add_node('w', variable_type=NodeVariableType.BINARY)
cg.add_edges_from([('w', 'x'), ('w', 'z')])
print(cg.is_dag()) # True if all edges are directed and acyclic
print(cg.get_node_names())
print(cg.get_edge_pairs())
Inspecting nodes and edges
node = cg.get_node('x')
print(node.identifier, node.variable_type)
print(node.is_source_node(), node.is_sink_node())
print(node.get_inbound_edges(), node.get_outbound_edges())
edge = cg.get_edge('x', 'y')
print(edge.source.identifier, edge.destination.identifier)
print(edge.get_edge_type()) # EdgeType.DIRECTED_EDGE
print(edge.descriptor) # '(x -> y)'
Serialization
# dict round-trip (`from_dict` deepcopies metadata; `to_dict` does not deep-copy nested meta)
d = cg.to_dict()
cg2 = CausalGraph.from_dict(d)
# NetworkX (directed edges only -> DiGraph; undirected only -> Graph; mixed raises GraphConversionError)
nx_graph = cg.to_networkx()
cg3 = CausalGraph.from_networkx(nx_graph)
# Numpy adjacency matrix (directed/undirected only; bidirected/unknown raises TypeError)
adj, node_names = cg.to_numpy()
cg4 = CausalGraph.from_adjacency_matrix(adj, node_names=node_names)
Skeleton
skeleton = cg.skeleton # property; returns Skeleton instance
print(skeleton.nodes) # nodes without edge direction info
print(skeleton.edges) # all edges as UNDIRECTED_EDGE
adj = skeleton.adjacency_matrix # symmetric binary matrix
Time series graph
from cai_causal_graph import TimeSeriesCausalGraph, EdgeType
ts = TimeSeriesCausalGraph()
ts.add_edge('X1 lag(n=1)', 'X1', edge_type=EdgeType.DIRECTED_EDGE)
ts.add_edge('X2 lag(n=1)', 'X2', edge_type=EdgeType.DIRECTED_EDGE)
node = ts.get_node('X1 lag(n=1)')
print(node.variable_name) # 'X1'
print(node.time_lag) # -1
summary = ts.get_summary_graph() # CausalGraph collapsing lags
minimal = ts.get_minimal_graph() # TimeSeriesCausalGraph with minimum lag structure
lag_0_nodes = ts.get_nodes_at_lag(0)
Domain knowledge constraints
from cai_causal_graph.type_definitions import EdgeConstraint
# EdgeConstraint is used by discovery algorithms, not enforced by CausalGraph itself
EdgeConstraint.HARD_DIRECTED_EDGE # force edge direction
EdgeConstraint.HARD_UNDIRECTED_EDGE # force undirected
EdgeConstraint.SOFT_DIRECTED_EDGE # prefer direction
EdgeConstraint.FORBIDDEN_EDGE # forbid the edge
Assumptions and Limitations
- Self-loops are not allowed — adding
source == destinationraisesCyclicConnectionError. - Nodes are auto-created when adding edges if they do not already exist.
add_edgeraisesCyclicConnectionErrorif directed edges would form a cycle; the graph is rolled back.to_numpy()/from_adjacency_matrix()only supportDIRECTED_EDGEandUNDIRECTED_EDGE— bidirected or unknown edge types raiseTypeError.from_adjacency_matrix()expects anumpy.ndarrayinput. Passing plain Python lists can raiseAttributeErrorbefore adjacency validation.to_networkx()only producesDiGraph(all directed) orGraph(all undirected) — mixed edge types raiseGraphConversionError.- Node and edge metadata is shallow-copied when passed to constructors;
from_dictdeepcopies. replace_node()copies edgemetashallowly from originals but does not preserve edge object identity.networkxversion is bounded to>=3.0.0, <3.3.0due to a bug inget_all_simple_pathsin 3.3.
Gotchas
--(undirected) in a CPDAG means orientation is unknown and can be resolved either way. The same symbol in a MAG implies selection bias — a completely different semantic.- Deleting a node invalidates graph-backed node queries — methods like
get_inbound_edges()andget_outbound_edges()raiseNodeDoesNotExistErrorafter deletion. node.variable_typeis mutable; changing it after creation is intentional and supported.Edge.__eq__treatsUNDIRECTED_EDGE,BIDIRECTED_EDGE, andUNKNOWN_EDGEas directionless —(a -- b)and(b -- a)are equal.CausalGraph.is_dag()returnsFalseimmediately if any non-directed edge is present (does not check only directed subgraph).Skeleton.edgesreturns all edges asUNDIRECTED_EDGEregardless of original type.
Error Types
All errors are nested inside CausalGraphErrors:
| Exception | When raised |
|---|---|
CyclicConnectionError |
Self-loop or directed cycle |
NodeDoesNotExistError |
Accessing deleted or missing node |
NodeDuplicatedError |
Adding a node that already exists |
EdgeDoesNotExistError |
Accessing missing or deleted edge |
EdgeExistsError |
Adding an edge that already exists |
ReverseEdgeExistsError |
Adding edge when reverse already exists |
GraphConversionError |
to_networkx() / to_gml_string() with mixed edges |
InvalidAdjacencyMatrixError |
Non-square or non-binary numpy.ndarray in from_adjacency_matrix() |
from cai_causal_graph.exceptions import CausalGraphErrors
try:
cg.get_node('missing')
except CausalGraphErrors.NodeDoesNotExistError:
...