andrey.GraphStructure#
- class andrey.GraphStructure[source]#
A learned graph: its nodes, and a mark at each end of every edge.
A method returns one as
StructureOutput.structure;from_numpy,from_edges,from_networkx, andfrom_pcalgbuild one from an existing graph.kindsays how to read the edges:"dag": every edge is directed, with no directed cycles."cpdag": an equivalence class of DAGs; an undirected edge--can point either way."pag": an equivalence class that allows hidden variables; a circleois an unresolved endpoint, and<->is a relation through hidden causes."digraph": directed edges that may form cycles and self-loops, such as the graph for a positive lag of aTemporalStructure. Here the pair(2, 2)is two directed edges,i -> jandj -> i.
A cell
M[i, j]ofto_numpy()is the mark at nodeion its edge withj:0absent,1tail,2arrowhead,3circle (andrey.core.NULL,TAIL,ARROW,CIRCLE).endpoints(i, j)reads one edge, andoriented_edges()lists every edge as(source, target, type).oriented_edges(),to_edges(), andto_scipy_sparse()scale to large graphs;to_numpy()andadjacencybuild a dense(n, n)array on each call. The graph is read-only.- classmethod from_numpy(M, *, kind=None, labels=None, allow_self_loops=False, node_types=None)[source]#
Build a graph from an
(n, n)endpoint-mark matrix, the inverse ofto_numpy.- Parameters:
M (
ndarray) – Endpoint marks:M[i, j]is the mark at nodeion its edge withj,0absent,1tail,2arrowhead,3circle.i -> jisM[i, j] = 1andM[j, i] = 2. Every edge needs a mark in both cells.kind (
Literal['dag','cpdag','pag','digraph'] |None) – Graph type.Noneinfers"pag"from any circle or(2, 2)pair, else"cpdag"from any(1, 1)pair, else"dag"; it never infers"digraph", so pass it for a graph with 2-cycles. The marks are not checked againstkind; callvalidate().labels (
tuple[str,...] |None) – Node names, one per node, each taken as text,str(label);Nonenumbers the nodes from 0.allow_self_loops (
bool) – Accept self-loops on the diagonal. A self-loopM[i, i] = 2is a variable’s effect on its own future, in the graph for a positive lag of aTemporalStructure;2is the only self-loop mark.node_types (
ndarray|None) –0for an observed node and1for a latent one (andrey.core.OBSERVED,LATENT);Nonewhen every node is observed.
- Returns:
The graph.
- Return type:
- Raises:
ValueError – If
kindis not one of the four kinds,Mis not square, has a nonzero diagonal withoutallow_self_loopsor a diagonal value other than0or2with it, holds a value outside0..3, has a mark inM[i, j]but not inM[j, i],labelsdoes not havenentries or has two that are equal as text, such as1and"1", ornode_typesis not shape(n,).
- classmethod from_edges(edges, *, n_nodes, kind=None, labels=None)[source]#
Build a graph from an edge list, the inverse of
to_edges.- Parameters:
edges (
ndarray) – Structured array with fieldsi,j,mark_i,mark_j(dtypeandrey.core.EDGE_DTYPE), one row per edge;mark_iis the mark atiandmark_jthe mark atj. A row withi == jis a self-loop.n_nodes (
int) – Number of nodes, including nodes without edges.kind (
Literal['dag','cpdag','pag','digraph'] |None) – Graph type;Noneinfers it asfrom_numpydoes.labels (
tuple[str,...] |None) – Node names, one per node, each taken as text,str(label);Nonenumbers the nodes from 0.
- Returns:
The graph.
- Return type:
- Raises:
ValueError – If a mark is outside
0..3, a row has a mark at one end only, orlabelsdoes not haven_nodesentries or has two that are equal as text.IndexError – If
edgesis not a structured array with those fields, or names a node outsiderange(n_nodes).
- classmethod from_networkx(g, *, kind=None)[source]#
Build a graph from a NetworkX graph, the inverse of
to_networkx.Each edge
(u, v)is read from itsendpoints=(mark at u, mark at v)attribute. An edge without the attribute isu -> v. Two such edgesu -> vandv -> ugive the mark pair(2, 2), so passkind="digraph"to read them as a 2-cycle rather than apagbidirected edge. Self-loops are kept. Nodes are indexed in insertion order; unless they are exactly0, 1, ..., n - 1in that order, each node becomes a label, converted tostr.- Parameters:
- Returns:
The graph.
- Return type:
- Raises:
ValueError – If an
endpointsmark is outside0..3, an edge has a mark at one end only, or two nodes are equal as text, such as1and"1".
- classmethod from_pcalg(amat, kind, *, labels=None)[source]#
Build a graph from the adjacency matrix
amatthat R’s pcalg package uses.pcalg records a PAG, and a MAG, as
amat.pag:amat[a, b]is the mark at the column nodebon the edge betweenaandb,0no edge,1circle,2arrowhead,3tail. It records a DAG or CPDAG asamat.cpdag:a -> bisamat[b, a] = 1withamat[a, b] = 0, at the child’s row, anda -- bis1in both cells. This reads either, the inverse ofto_pcalg.- Parameters:
amat (
ArrayLike) – The pcalg matrix, such asfit@amatfromfcioras(fit, "amat")frompc.kind (
Literal['dag','cpdag','pag']) – How pcalg coded the graph:"pag"readsamat.pag(a MAG too), and"cpdag"and"dag"readamat.cpdag.labels (
tuple[str,...] |None) – Node names, one per node, each taken as text,str(label);Nonenumbers the nodes from 0.
- Returns:
The graph, of
kind.- Return type:
- Raises:
ValueError – If
kindis not"dag","cpdag", or"pag",amatis not square, has a nonzero diagonal, holds a value other than0,1,2,3for a"pag"or0and1otherwise, or, for a"pag", has a mark inamat[a, b]but not inamat[b, a]. Also if the edges are not allowed forkind, such as an undirected edge in a"dag", orlabelsdoes not havenentries or has two that are equal as text.
Examples
>>> import numpy as np >>> from andrey import GraphStructure >>> amat = np.array([[0, 0, 0], [0, 0, 0], [1, 1, 0]]) # 0 -> 2 <- 1, child's row 2 >>> GraphStructure.from_pcalg(amat, "cpdag").oriented_edges() [(0, 2, 'directed'), (1, 2, 'directed')]
- property kind: Literal['dag', 'cpdag', 'pag', 'digraph']#
"dag","cpdag","pag", or"digraph"(cycles allowed).- Type:
Graph type
- property node_types: ndarray | None#
Per-node codes,
0observed and1latent, orNonewhen every node is observed.ginsets it for the latent nodes it adds (andrey.core.OBSERVED,LATENT).
- property adjacency: ndarray#
The endpoint-mark matrix, the same as
to_numpy(); seeto_numpyfor the codes.Each access builds a new dense
(n, n)array; for a large graph useoriented_edges,to_edges, orto_scipy_sparse.
- to_numpy(*, dtype=<class 'numpy.int8'>)[source]#
Return the endpoint marks as a dense
(n, n)array.A cell
M[i, j]is the mark at nodeion its edge withj:0absent,1tail,2arrowhead,3circle.i -> jhasM[i, j] = 1andM[j, i] = 2. Each call builds a new array.
- to_scipy_sparse()[source]#
Return the endpoint marks as a
scipy.sparse.csr_array, without a dense array.- Returns:
The
to_numpymatrix in sparse form; memory grows with the number of edges.- Return type:
- to_networkx(*, multigraph=False)[source]#
Return the graph as a NetworkX graph whose edges carry their marks.
Each arc
(u, v)has the attributeendpoints=(mark at u, mark at v). For adag,cpdag, orpag, each edge is one arc from the lower to the higher node index whatever its direction, so readendpointsor useoriented_edges(). Adigraphruns each arc from tail to arrowhead and gives a 2-cycle both arcs, so NetworkX cycle functions see its cycles. AMultiDiGraphalso adds the reverse arc for<->ando-o. Nodes are named bylabelswhen set, else numbered from 0.from_networkxreads the result back.- Parameters:
multigraph (
bool) – Return aMultiDiGraphfor anykind; apagalways gets one.- Returns:
A
MultiDiGraphfor apagor withmultigraph=True, else aDiGraph.- Return type:
- to_pcalg()[source]#
Return the adjacency matrix
amatthat R’s pcalg package uses for this kind of graph.A
pagis written as pcalg’samat.pag:amat[a, b]is the mark at the column nodebon the edge betweenaandb,0no edge,1circle,2arrowhead,3tail. Adagorcpdagis written asamat.cpdag:a -> bisamat[b, a] = 1withamat[a, b] = 0, at the child’s row, anda -- bis1in both cells.from_pcalgreads the result back. Each call builds a new array.- Returns:
The pcalg matrix,
int8.- Return type:
- Raises:
ValueError – If the graph is a
digraph, which pcalg has no matrix for, has a self-loop, or has an edge itskinddoes not allow (validate()lists them).
Examples
>>> from andrey import GraphStructure >>> import numpy as np >>> M = np.array([[0, 1], [2, 0]]) # 0 -> 1 >>> GraphStructure.from_numpy(M, kind="dag").to_pcalg().tolist() [[0, 0], [1, 0]]
- to_edges()[source]#
Return the derived typed edge list, one row per edge
i <= j.- Returns:
Structured array (dtype
andrey.core.EDGE_DTYPE) with fieldsi,j,mark_i,mark_j, one row per edge withi <= j;mark_iis the mark ati. A row withi == jis a self-loop.- Return type:
- oriented_edges(*, index=False)[source]#
Return one
(source, target, type)row per edge, oriented by its marks.A directed edge runs from its tail to its arrowhead whatever the node order, and nodes are named by
labelswhen the graph has them.andrey runprints the same rows.- Parameters:
index (
bool) – Return node indices even when the graph has labels.- Returns:
Rows follow
to_edgesorder. Adigraph2-cycle yields twodirectedrows, lower-to-higher node index first, then the reverse.typeis one of:directed:source -> target; a self-loop hassource == target.partially_directed:source o-> target.partially_undirected:source -o target, a tail atsourceand a circle attarget.undirected:source -- target.bidirected:source <-> targetin apag.circle:source o-o target.
The three symmetric types list the lower node index first.
- Return type:
Examples
>>> import numpy as np >>> from andrey import GraphStructure >>> M = np.array([[0, 2], [1, 0]]) # the arrowhead is at node 0 >>> GraphStructure.from_numpy(M, labels=("wet", "rain")).oriented_edges() [('rain', 'wet', 'directed')]
- neighbors(i)[source]#
Return the nodes that share an edge with node
i, whatever the marks.For a
dagorcpdageach call scans every edge; to visit every node, useto_scipy_sparse().
- validate()[source]#
Check that every edge’s marks are allowed for
kind.It does not check a
dagfor directed cycles, or that acpdagorpagis a completed equivalence class.- Raises:
ValueError – If a circle appears outside a
pag, adagorcpdaghas an edge with two arrowheads, adagordigraphhas an undirected edge, or an edge has a mark at one end only.- Return type: