MAG#

class pgmpy.base.MAG(*args, backend=None, **kwargs)[source]#

Bases: AncestralBase

Class for representing Maximal Ancestral Graphs (MAGs).

A MAG is a type of graph used in causal inference to represent conditional independence relations when some variables are latent (unobserved). Unlike simple directed acyclic graphs (DAGs), MAGs allow for special edge types (directed and bidirected) that capture the presence of latent confounding and selection bias. Every pair of nodes in a MAG is connected in such a way that the graph is “maximal,” meaning no additional edges can be added without changing the set of implied conditional independence relations.

Parameters:
ebunchiterable of tuples, optional

A list or iterable of edges to add at initialization.

latentsset, default=set()

Set of latent (unobserved) variables.

exposuresset, default=set()

Set of exposure variables in the graph. These are the variables that represent the treatment or intervention being studied in a causal analysis. Default is an empty set.

outcomesset, default=set()

Set of outcome variables in the graph. These are the variables that represent the response or dependent variables being studied in a causal analysis. Default is an empty set.

rolesdict, optional (default: None)

A dictionary mapping roles to node names. The keys are roles, and the values are role names (strings or iterables of str). If provided, this will automatically assign roles to the nodes in the graph. Passing a key-value pair via roles is equivalent to calling with_role(role, variables) for each key-value pair in the dictionary.

References

[1]

Zhang, J. (2008). Causal Reasoning with Ancestral Graphs. Journal of Machine Learning Research, 9(7).

Examples

>>> from pgmpy.base import MAG
>>> mag = MAG(ebunch=[("L", "A", "-", ">"), ("B", "C", "-", ">")], latents={"L"})
>>> sorted(mag.nodes())
['A', 'B', 'C', 'L']

Roles can be assigned to nodes in the graph at construction or using methods.

At construction:

>>> mag = MAG(
...     ebunch=[("L", "A", "-", ">"), ("B", "C", "-", ">")],
...     latents={"L"},
...     exposures={"A"},
...     outcomes={"B"},
... )

Roles can also be assigned after creation using with_role method.

>>> mag = mag.with_role("adjustment", {"L", "C"})

Vertices of a specific role can be retrieved using get_role method.

>>> mag.get_role("exposures")
['A']
>>> mag.get_role("adjustment")
['L', 'C']
has_inducing_path(u, v, W)[source]#

Check if there exists an inducing path between two nodes relative to W.

An inducing path between u and v is a path such that: - The path has length > 2 (at least one intermediate node), - Every intermediate node is a collider on the path, - Every intermediate node is either:

  • in W, or

  • an ancestor of u or v.

Parameters:
uHashable

Source node.

vHashable

Target node.

Wset

Subset of nodes to check inducing paths through (often latents).

Returns:
bool

True if there exists an inducing path, False otherwise.

Examples

>>> from pgmpy.base import MAG
>>> mag = MAG()
>>> mag.add_edge("X", "L", "-", ">")
>>> mag.add_edge("Y", "L", "-", ">")
>>> mag.latents = {"L"}
>>> mag.has_inducing_path("X", "Y", mag.latents)
True
is_visible_edge(u, v) bool[source]#

Check if a directed edge u -> v is visible in the MAG.

A directed edge A → B in a MAG is considered visible if there exists a vertex C not adjacent to B such that either:

  1. C → A exists, or

2. There is a collider path from C to A that is into A, and every vertex on that path is a parent of B.

Parameters:
uHashable

Source node (tail of the edge).

vHashable

Target node (head of the edge).

Returns:
bool

True if the edge u -> v is visible, False otherwise.

Examples

>>> edges = [
...     ("A", "D", "-", ">"),
...     ("B", "C", "-", ">"),
...     ("X", "A", "-", ">"),
... ]
>>> mag = MAG(ebunch=edges)
>>> mag.is_visible_edge("A", "D")
True
>>> mag.is_visible_edge("B", "C")
False
lower_manipulation(X, inplace=False)[source]#

Performs lower manipulation.

Removes all edges that are visible and originate from nodes in X. For edges from X that are invisible, adds bidirected edges from the other endpoint to its neighbors outside X to preserve conditional independencies. All other edges remain unchanged.

Parameters:
Xset

Set of nodes to perform manipulation on.

inplacebool, optional

If True, modifies the current graph in place. Defaults to False.

Returns:
MAG

A new MAG with outgoing edges from X removed.

Examples

>>> from pgmpy.base import MAG
>>> mag = MAG()
>>> mag.add_edge("A", "B", "-", ">")
>>> mag.add_edge("A", "C", "-", ">")
>>> mag.add_edge("C", "B", "-", ">")
>>> mag.add_edge("B", "C", ">", ">")
>>> new_mag = mag.lower_manipulation({"A"})
>>> edges = list(new_mag.edges(data=True))
>>> len(edges)
1
>>> edges[0][0], edges[0][1]
('B', 'C')
>>> edges[0][2]['marks']['B'], edges[0][2]['marks']['C']
('>', '>')
upper_manipulation(X, inplace=False)[source]#

Performs upper manipulation.

Deletes all edges (directed or bidirected) that have an arrowhead pointing to any variable in X. The rest of the graph remains unchanged.

Parameters:
Xset

Set of nodes to perform manipulation on.

inplacebool, optional

If True, modifies the current graph in place. Defaults to False.

Returns:
MAG

A new MAG with incoming edges to X removed.

Examples

>>> from pgmpy.base import MAG
>>> mag = MAG()
>>> mag.add_edge("X", "Y", ">", "-")
>>> mag.add_edge("Z", "X", ">", "-")
>>> mag.add_edge("A", "X", "-", ">")
>>> new_mag = mag.upper_manipulation({"X"})
>>> new_mag.has_edge("Z", "X")
True
>>> new_mag.has_edge("A", "X")
False
>>> new_mag.has_edge("X", "Y")
False