--- title: "Comparisons with Other Packages" output: rmarkdown::html_vignette bibliography: references.bib vignette: > %\VignetteIndexEntry{Comparisons with Other Packages} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- In this vignette, we compare `caugi` to some of the most widely used graph packages in R, Python, and Java. ## Overview The following table summarizes the packages included in this comparison, their general focus, and the languages they are packaged for. | Package | Type | Language | URL | | :---------- | ------------------------------- | ------------------------------- | :------------------------------------------ | | igraph | General-purpose | R, Python, C | | | graph | General-purpose | R | | | gRbase | Graphical models | R | | | pcalg | Causal graphs | R | | | dagitty | Causal graphs | R, Web | | | bnlearn | Bayesian networks | R | | | ggm | Graphical Markov models | R | | | MixedGraphs | Causal graphs | R | | | NetworkX | General-purpose | Python | | | pgmpy | Probabilitistc graphical models | Python | | | Tetrad | Causal graphs | Java, CLI, R[^tetrad-r], Python | | : An overview of the packages included in this comparison [^tetrad-r]: Bindings for Tetrad exist in , but not in the form of a formal R package. The authors instead recommend using the Python bindings from R instead. ## Scope The comparison focuses on **graph representation and analysis**: which graph classes each package can represent, which structural and causal-graph algorithms it implements, and how it interoperates with other tooling. Two adjacent areas are intentionally out of scope: - **Causal discovery** (PC, FCI, GES, GFCI, LiNGAM, etc.). `caugi` does not implement discovery algorithms. - **Statistical inference and parameter learning** (CPT estimation, likelihood, parameter fitting): also out of scope for `caugi`. ## Graph Types Supported | Package | DAG | CPDAG | MPDAG | MAG | PAG | ADMG | SWIG | UG | Mixed/general | | :---------- | :------------------ | :---------------- | :--------------- | :------------- | :------------- | :------------------- | :--- | :-------------- | :----------------- | | **caugi** | ● | ◐[^caugi-cpdag] | ● | ◐[^caugi-mag] | ○ | ● | ○ | ● | ● | | igraph | ◐[^igraph-dag] | ○[^igraph-cpdag] | ○ | ○ | ○ | ○ | ○ | ◐[^igraph-ug] | ◐[^igraph-mixed] | | graph | ◐[^graph-dag] | ○ | ○ | ○ | ○ | ○ | ○ | ◐[^graph-ug] | ◐[^graph-mixed] | | gRbase | ◐[^grbase-dag] | ○ | ○ | ○ | ○ | ○ | ○ | ◐[^grbase-ug] | ○ | | pcalg | ◐[^pcalg-dag] | ◐[^pcalg-cpdag] | ◐[^pcalg-mpdag] | ◐[^pcalg-mag] | ◐[^pcalg-pag] | ○ | ○ | ○ | ○ | | dagitty | ● | ◐[^dagitty-cpdag] | ○ | ● | ● | ○ | ○ | ○ | ○ | | bnlearn | ● | ◐[^bnlearn-cpdag] | ○ | ○ | ○ | ○ | ○ | ◐[^bnlearn-ug] | ◐[^bnlearn-mixed] | | ggm | ◐[^ggm-dag] | ◐[^ggm-cpdag] | ○ | ◐[^ggm-mag] | ○ | ◐[^ggm-admg] | ○ | ◐[^ggm-ug] | ◐[^ggm-mixed] | | MixedGraphs | ◐[^mixedgraphs-dag] | ○ | ○ | ○ | ○ | ◐[^mixedgraphs-admg] | ○ | ● | ● | | NetworkX | ◐[^networkx-dag] | ○ | ○ | ○ | ○ | ○ | ○ | ◐[^networkx-ug] | ◐[^networkx-mixed] | | pgmpy | ● | ◐[^pgmpy-cpdag] | ○[^pgmpy-mpdag] | ● | ○ | ● | ○ | ● | ○[^pgmpy-mixed] | | Tetrad | ● | ◐[^tetrad-cpdag] | ◐[^tetrad-mpdag] | ◐[^tetrad-mag] | ●[^tetrad-pag] | ◐[^tetrad-admg] | ○ | ◐[^tetrad-ug] | ◐[^tetrad-mixed] | : Supported graph types for the packages in this comparison. `●` indicates a dedicated class with type-level invariants; `◐` indicates representability without a dedicated class or invariant enforcement; `○` indicates unsupported graph types. [^caugi-cpdag]: `caugi` exposes a general `PDAG` class that can represent any CPDAG, and `is_cpdag()` validates the structure post-hoc, but there is no dedicated CPDAG class and CPDAG invariants are not enforced at construction (e.g. that the graph is the completion of a Markov equivalence class). [^caugi-mag]: `caugi` accepts `class = "AG"` (ancestral graph) in the constructor and exports `is_mag()` to validate maximality post-hoc, but there is no dedicated MAG class---MAGs are representable as AGs and checked rather than enforced. [^igraph-dag]: igraph has no dedicated DAG class. `is_dag()` tests whether a directed graph is acyclic, but the package's single `igraph` object class does not enforce DAG invariants. A directed graph that happens to be acyclic is representable, but the type is not distinguished at the object level. [^igraph-cpdag]: igraph has no CPDAG, MPDAG, MAG, PAG, ADMG, or SWIG class and no associated type-level operations (confirmed absent from R source and NAMESPACE). [^igraph-ug]: Undirected graphs are representable via `is_directed = FALSE` (e.g. `make_empty_graph(directed = FALSE)`), but there is no dedicated UG class enforcing undirectedness as a type invariant distinct from the general `igraph` object. [^igraph-mixed]: igraph supports per-edge attributes but has no dedicated mixed-edge class with typed semantics. Representing a true mixed graph requires manual attribute manipulation with no type enforcement. [^graph-dag]: `graph` has no dedicated DAG class. `graphNEL` and `graphAM` can hold directed graphs (via `edgemode = "directed"`) that happen to be acyclic, but the package enforces no acyclicity invariants and provides no DAG-specific construction or validation. [^graph-ug]: No dedicated UG class. Undirected graphs are represented by setting `edgemode = "undirected"` on a general `graphNEL` / `graphAM` object; the package stores reciprocal edges internally but does not expose a named UG class. [^graph-mixed]: `MultiGraph` holds a shared node set with multiple edge sets, each independently directed or undirected. It does not support a single edge that is both directed and bidirected, and is not designed around causal mixed-edge semantics (e.g. MAG/PAG/ADMG). [^grbase-dag]: gRbase provides a `dag()` constructor and `is_dag()` type-check, with the underlying representation being an igraph object or adjacency matrix. There is no dedicated DAG class enforcing acyclicity invariants at the object level. [^grbase-ug]: Same reasoning as `[^grbase-dag]`: `ug()` constructs an undirected graph and `is_ug()` tests for it, but the result is a plain igraph or matrix. [^pcalg-dag]: pcalg produces DAGs (via `randomDAG()` / `randDAG()` returning `graphNEL` objects, or the `ParDAG` / `GaussParDAG` class), and `isValidGraph(amat, type = "dag")` validates them. The dominant user-facing representation is an untyped `graphNEL` rather than a pcalg-owned DAG class. [^pcalg-cpdag]: `dag2cpdag()` produces a CPDAG and `isValidGraph(amat, type = "cpdag")` validates one; `pcAlgo` objects carry a graph with attribute `"amat.cpdag"`. There is no dedicated exported CPDAG class. [^pcalg-mpdag]: `addBgKnowledge()` augments a CPDAG with background knowledge and re-applies Meek's rules to produce an MPDAG. As with CPDAG, no dedicated exported MPDAG class exists; the object is a bare adjacency matrix with convention-level typing only. [^pcalg-mag]: MAG-type adjacency matrices (0/2/3 coding from Richardson & Spirtes 2002) are used throughout pcalg (e.g. `dsepAM()`, `backdoor()`, `adjustment(amat.type = "mag")`, `pcalg2dagitty()`), but there is no exported MAG class. [^pcalg-pag]: `fciAlgo` objects hold a PAG as an adjacency matrix in the `@amat` slot. `fci()` / `rfci()` / `fciPlus()` / `dag2pag()` all return `fciAlgo`. The class is exported but is a *result* container for FCI-family algorithms, not a freely constructable PAG class. [^dagitty-cpdag]: `dagitty` recognises only `"dag"`, `"pdag"`, `"mag"`, `"pag"` via `graphType()`. `equivalenceClass()` calls `dagToCpdag` internally and is documented as producing a CPDAG, but the result is typed `"pdag"`---no distinct CPDAG class or invariant enforcement. MPDAG is likewise representable as `"pdag"` without Meek-completion logic. `"ug"`, `"admg"`, `"swig"` are absent from both the R API and the JS engine. [^bnlearn-cpdag]: `cpdag()` computes the CPDAG of a DAG and `valid.cpdag()` validates structure, but bnlearn uses a single generic `bn` class for all graph types; no dedicated CPDAG class exists and CPDAG invariants are not enforced at the object level. [^bnlearn-ug]: `skeleton()` produces a fully undirected graph and `valid.ug()` validates whether a `bn` object is completely undirected, but the result is still a plain `bn` object---no dedicated UG class with enforced undirected invariants exists. [^bnlearn-mixed]: A `bn` object can hold a mix of directed and undirected arcs (representing a PDAG), making mixed graphs implicitly representable, but there is no explicit untyped mixed-edge graph class distinct from the PDAG interpretation. [^ggm-dag]: `ggm` represents DAGs as plain integer adjacency matrices (entries 0, 1, 10, 100) built with `DAG()`. There is no S3/S4 class enforcing DAG invariants; `isAcyclic()` must be called separately. [^ggm-cpdag]: `essentialGraph()` converts a DAG to its essential graph (= CPDAG), returning an adjacency matrix. There is no dedicated CPDAG class; the result is an untagged matrix. [^ggm-mag]: `MAG()` derives a maximal ancestral graph after marginalisation/conditioning, and `isAG()` validates ancestral-graph conditions. The representation is still an untagged adjacency matrix. [^ggm-admg]: `isADMG()` checks whether an adjacency matrix satisfies ADMG conditions. No dedicated ADMG constructor exists; the matrix encoding is the same general integer format used for all mixed graphs. [^ggm-ug]: Undirected graphs are built with `UG()`, which produces an adjacency matrix with edge-type value 10. No distinct UG class enforces undirected invariants. [^ggm-mixed]: `makeMG()` combines directed, undirected, and bidirected components into one adjacency matrix; `grMAT()` converts graphNEL/igraph inputs to this format. The representation is a general integer adjacency matrix with no commitment to a specific typed mixed-graph semantics. [^mixedgraphs-dag]: `MixedGraphs` has no dedicated DAG class. `is_DAG()` checks that a `mixedgraph` carries only directed edges and is acyclic, but the constructor does not enforce DAG invariants. The `mixedgraph` object is an untyped container. [^mixedgraphs-admg]: `is_ADMG()` tests whether a `mixedgraph` contains only directed and bidirected edges and is acyclic, but there is no dedicated ADMG class. ADMG-typed objects from the companion `ADMGs` package can be converted to/from `mixedgraph` via `convert()`, but `MixedGraphs` itself does not expose an ADMG class. [^networkx-dag]: NetworkX's `DiGraph` class stores directed edges and supports DAG-related algorithms (`is_directed_acyclic_graph`, `topological_sort`, `ancestors`, `descendants`), but the class itself does not enforce acyclicity as an invariant---a `DiGraph` can hold cycles. There is no dedicated `DAG` subclass. [^networkx-ug]: NetworkX's `Graph` class stores undirected edges, making it suitable for UGs, but it carries no type-level semantic enforcement (no bidirected edges, no directed edges, etc.). [^networkx-mixed]: NetworkX has no mixed-edge graph class. Its four classes (`Graph`, `DiGraph`, `MultiGraph`, `MultiDiGraph`) are all homogeneous-edge. The `pywhy-graphs` extension provides mixed-graph types but is a separate package. [^pgmpy-cpdag]: `pgmpy` has a single `PDAG` class (`pgmpy.base.PDAG`) documented as "also known as CPDAG". It supports CPDAG construction from a DAG via `DAG.to_pdag()` (Chickering 2002) and `to_cpdag()` / `apply_meeks_rules()` methods. There is no separate dedicated CPDAG class, and PDAG does not enforce the invariant that the graph represents a complete Markov equivalence class. [^pgmpy-mpdag]: No dedicated MPDAG class. `PDAG.apply_meeks_rules(apply_r4 = True)` applies all four Meek rules and could produce an MPDAG, but there is no type-specific MPDAG class or MPDAG-specific construction pathway. [^pgmpy-mixed]: `AncestralBase` stores mixed-edge graphs with arbitrary marks and is the parent of `MAG`. It is an internal base class, not a user-facing untyped mixed-edge graph class. Coverage of several typed mixed graphs (ADMG, MAG, PDAG) does not by itself satisfy this column. [^tetrad-cpdag]: Tetrad has no dedicated `Cpdag` class. CPDAGs are represented as `EdgeListGraph` objects containing directed and undirected edges. The library validates CPDAG status at runtime via `Paths.isLegalCpdag()` and produces CPDAGs through `GraphTransforms.dagToCpdag()` / `GraphSearchUtils.basicCpdag()`. The CPDAG concept is operationally first-class but not a distinct enforced type. [^tetrad-mpdag]: No dedicated `Mpdag` class. Tetrad validates MPAGs via `Paths.isLegalMpag()`, and `MeekRules.orientImplied(graph)` with a populated `Knowledge` object applies Meek rules under background constraints, but there is no single `dagToMpdag` factory. [^tetrad-mag]: No dedicated `Mag` class. MAGs are represented as generic `EdgeListGraph` objects and validated post-hoc via `Paths.isLegalMag()`. Constructed through `GraphTransforms.dagToMag(dag)` and converted to PAGs via `GraphTransforms.magToPag()`. [^tetrad-pag]: No dedicated `Pag` class. PAGs are `EdgeListGraph` instances with circle, tail, and arrowhead endpoints. Validated via `Paths.isLegalPag()`. Produced by `GraphTransforms.dagToPag()` and `GraphTransforms.magToPag()`. [^tetrad-admg]: `SemGraph` (in `edu.cmu.tetrad.graph`) enforces directed + bidirected edges only and automatically manages error nodes for each endogenous variable, implementing ADMG-like semantics. However, it is scoped to structural-equation-model graphs (with explicit error nodes) rather than a general-purpose ADMG class. [^tetrad-ug]: No dedicated undirected-graph class. Undirected edges can be added to `EdgeListGraph` via `addUndirectedEdge()`, and `GraphUtils.undirectedGraph(g)` converts all edges to undirected. There is no enforced `UG` type with invariant-checking. [^tetrad-mixed]: `EdgeListGraph` is the universal mixed-edge container and accepts all five endpoint combinations (directed, undirected, bidirected, partially-oriented, nondirected). It does not commit to any single graph semantics, but it is not branded as a distinct "mixed/general" class---it is simply the default implementation of the `Graph` interface. ## Graph Queries and Structural Operations | Package | Parents/children | Ancestors/descendants | d-sep | m-sep | Paths | Acyclicity | Markov blanket | Moralization | Skeleton | v-structures | MEC enumeration | | :---------- | :----------------- | :-------------------- | :---------------- | :------------- | :-------------- | :---------------------- | :----------------- | :----------- | :-------------------- | :---------------------- | :-------------- | | **caugi** | ● | ● | ● | ● | ○ | ● | ● | ● | ● | ○ | ○ | | igraph | ◐[^igraph-parents] | ◐[^igraph-ancestors] | ○ | ○ | ● | ● | ○ | ○ | ○ | ○ | ○ | | graph | ◐[^graph-parents] | ◐[^graph-ancestors] | ○ | ○ | ◐[^graph-paths] | ○ | ○ | ○ | ○ | ○ | ○ | | gRbase | ◐[^grbase-par-ch] | ◐[^grbase-anc] | ○ | ○ | ○ | ● | ○ | ● | ○ | ○ | ○ | | pcalg | ◐[^pcalg-par-ch] | ◐[^pcalg-anc-de] | ◐[^pcalg-dsep] | ◐[^pcalg-msep] | ○ | ◐[^pcalg-acyc] | ○ | ○ | ◐[^pcalg-skel] | ◐[^pcalg-vstruct] | ● | | dagitty | ● | ● | ● | ○ | ● | ● | ● | ● | ○ | ◐[^dagitty-vstructures] | ● | | bnlearn | ● | ● | ◐[^bnlearn-dsep] | ○ | ● | ● | ● | ● | ● | ● | ● | | ggm | ◐[^ggm-pa-ch] | ◐[^ggm-anc] | ● | ● | ◐[^ggm-paths] | ● | ○ | ○ | ○ | ○ | ◐[^ggm-mec] | | MixedGraphs | ● | ● | ○ | ● | ○ | ◐[^mixedgraphs-acyclic] | ◐[^mixedgraphs-mb] | ● | ● | ○ | ○ | | NetworkX | ◐[^networkx-par] | ● | ◐[^networkx-dsep] | ○ | ● | ● | ○ | ● | ◐[^networkx-skeleton] | ● | ○ | | pgmpy | ● | ◐[^pgmpy-desc] | ● | ● | ○ | ● | ● | ● | ○[^pgmpy-skel] | ● | ○ | | Tetrad | ● | ● | ◐[^tetrad-dsep] | ● | ● | ◐[^tetrad-acyclic] | ● | ○ | ◐[^tetrad-skeleton] | ◐[^tetrad-vstructs] | ● | : Overview of available graph queries and structural operations in the packages in the comparison. [^igraph-parents]: Parents and children are obtainable via `neighbors(graph, v, mode = "in" / "out")`, but there are no dedicated `parents()` / `children()` exported functions. [^igraph-ancestors]: Ancestors and descendants are obtainable via `subcomponent(graph, v, mode = "in" / "out")`, but there are no dedicated `ancestors()` / `descendants()` exported functions. [^graph-parents]: No dedicated `parents()` or `children()` functions. `inEdges()` returns the set of in-neighbours (functional equivalent of parents on a directed graph); `edges()` returns out-neighbours (functional equivalent of children). Both are exported S4 generics. [^graph-ancestors]: `acc()` returns all nodes reachable from a given node on a directed graph, equivalent to the descendant set. There is no `ancestors()` equivalent exported by the package. [^graph-paths]: `DFS()` (exported) provides depth-first graph traversal; `pathWeights()` computes weights along a user-specified path. No all-paths enumeration, shortest-path, or directed-path-existence function is provided; those require the companion `RBGL` package. [^grbase-par-ch]: `parents()` and `children()` are exported (`R/graph-querygraph.R`). There is no corresponding `descendants()` function in the exported API; `ancestors()` / `ancestralSet()` / `ancestralGraph()` cover the ancestor side but the symmetric descendent query is absent. [^grbase-anc]: `ancestors()`, `ancestralSet()`, and `ancestralGraph()` are exported. A dedicated `descendants()` is absent from the exported API. [^pcalg-par-ch]: `searchAM(amat, x, type = "pa" / "ch")` returns parents and children for DAG, CPDAG, MAG, and PAG adjacency matrices. There is no standalone `parents()` or `children()` function; retrieval goes through the general `searchAM()` dispatcher. [^pcalg-anc-de]: `searchAM(amat, x, type = "an" / "de")` returns ancestors and descendants for DAG/CPDAG/MAG/PAG. `possAn()` and `possDe()` / `possibleDe()` return possible ancestors/descendants in PAGs. All operate on raw adjacency matrices via `searchAM()`. [^pcalg-dsep]: `dsep(a, b, S, g)` tests d-separation in a DAG (using a `graphNEL` object and moralization; Lauritzen 2004). `dsepTest()` wraps it for use as a CI oracle. There is no exported d-separation function for CPDAGs or PDAGs directly; `dsep()` is DAG-only. [^pcalg-msep]: `dsepAM(X, Y, S, amat)` tests m-separation on MAG adjacency matrices. `dsepAMTest()` wraps it for algorithmic use. Coverage is MAG-only; there is no exported m-separation function for PAGs in pcalg's own API. [^pcalg-acyc]: `isValidGraph(amat, type = "dag")` checks for directed cycles as part of DAG validation. There is no standalone `is_acyclic()` function; acyclicity is only surfaced through `isValidGraph()`. [^pcalg-skel]: The `skeleton()` function *estimates* a skeleton from data using conditional independence tests (it is a causal discovery function). There is no separate exported function to extract or query the skeleton of an already-known graph. [^pcalg-vstruct]: `udag2pdag()` and `dag2cpdag()` identify v-structures as part of the CPDAG-completion process, but there is no standalone exported function to enumerate or check v-structures of a given graph. [^dagitty-vstructures]: No function enumerates v-structures as a set. `isCollider(x, u, v, w)` tests a single triple, but there is no exported `vStructures()` or equivalent. Skeleton (undirected version of the graph) is also absent as a dedicated function. [^bnlearn-dsep]: `dsep()` tests d-separation on `bn` objects. When the input contains undirected arcs (PDAG/CPDAG), the function calls `cpdag.extension()` to produce a single consistent DAG extension first; d-separation is then tested on that DAG rather than natively on the PDAG. [^ggm-pa-ch]: `pa()` returns parents and `ch()` returns children of a node set in a DAG. `bd()` returns the boundary (neighbours + parents). These operate on raw adjacency matrices and are undocumented for MAG/mixed-graph inputs; no `descendants()` or `spouses()` analogue is exported. [^ggm-anc]: `ancGraph()` / `anGraph()` compute the transitive closure (ancestor relation) of a DAG as a Boolean adjacency matrix rather than returning a node list. No `descendants()` function is exported. [^ggm-paths]: `findPath()` finds a single path between two nodes in an undirected graph (used internally by `fundCycles()`); it is not intended for direct user calls and does not enumerate all paths. [^ggm-mec]: `essentialGraph()` returns the CPDAG (encoding the full MEC) but does not enumerate the Markov-equivalent DAGs. `MarkEqRcg()` / `MarkEqMag()` test pairwise Markov equivalence; `RepMarDAG()` / `RepMarUG()` / `RepMarBG()` find a single representative. [^mixedgraphs-acyclic]: `is_cyclic()` is exported and checks directed cycles, and `topologicalOrder()` / `isTopological()` implicitly validate acyclicity, but `is_cyclic()` documentation notes it is "Not tested"---`◐` reflects this caveat. [^mixedgraphs-mb]: `mb()` finds the "Markov blanket for a vertex in an ancestral set" but requires the user to supply an explicit ancestral set `A` and the vertex must be childless in that set---a constrained helper rather than a standard graph-wide Markov-blanket function. [^networkx-par]: `DiGraph.predecessors()` and `DiGraph.successors()` return parent/child iterators. There is no dedicated `parents()` / `children()` API for a named causal DAG. [^networkx-dsep]: `networkx.algorithms.d_separation` provides `is_d_separator`, `is_minimal_d_separator`, and `find_minimal_d_separator`. All three require a DAG and raise `NetworkXError` on cyclic or undirected graphs. They do not support PDAGs, MAGs, or any other mixed-edge graph type. [^networkx-skeleton]: NetworkX has no dedicated `skeleton()` function. The moral graph (`nx.moral_graph`) is available, but skeleton extraction (dropping directionality without moralization) is not an exported function. [^pgmpy-desc]: `DAG.get_ancestors()` is implemented. There is no `DAG.get_descendants()` method; `nx.descendants()` from NetworkX can be called on the underlying `nx.DiGraph` directly, but it is not part of pgmpy's exported API. `ADMG` does provide both `get_ancestors()` and `get_descendants()`. [^pgmpy-skel]: No `skeleton()` method exists in any pgmpy graph class. The undirected skeleton is accessible via the inherited `nx.DiGraph.to_undirected()`, but this is not a pgmpy-level exported function. [^tetrad-dsep]: `Paths.isMSeparatedFrom(x, y, z, isPag)` implements d-/m-separation. The simpler overload's Javadoc says "DAG only", yet the `isPag` parameter explicitly switches PAG semantics. Per-graph-type documentation is inconsistent; users access d-separation as `isMSeparatedFrom(..., false)`. The back-door helper `Paths.isSatisfyBackDoorCriterion()` constructs a `Dag` internally and throws if input is not a DAG. [^tetrad-acyclic]: There is no `isAcyclic()` method. Callers use `!graph.paths().existsDirectedCycle()` or `Paths.isLegalDag()` / `GraphUtils.isDag()`. Fully supported but requires negation of a cycle-detection predicate. [^tetrad-skeleton]: No dedicated `skeleton()` method. `GraphUtils.undirectedGraph(g)` converts all edges to undirected (producing the skeleton graph object), but it is a workaround. [^tetrad-vstructs]: `GraphUtils.listColliderTriples(graph)` returns all definite collider triples. However, `EdgeListGraph.isDefCollider()` checks arrowhead endpoints without testing non-adjacency of the outer nodes, so the method returns both shielded and unshielded colliders rather than v-structures (unshielded colliders) exclusively. ## Causal-Inference Algorithms | Package | Back-door adj. | Generalized adj. | Optimal adj. | ID algorithm | Do-calculus | Counterfactuals | Interventions/mutilation | | :---------- | :------------------- | :--------------- | :----------------- | :----------- | :---------- | :-------------- | :----------------------- | | **caugi** | ● | ● | ◐[^caugi-optimal] | ○ | ○ | ○ | ○ | | igraph | ○ | ○ | ○ | ○ | ○ | ○ | ○ | | graph | ○ | ○ | ○ | ○ | ○ | ○ | ○ | | gRbase | ○ | ○ | ○ | ○ | ○ | ○ | ○ | | pcalg | ◐[^pcalg-backdoor] | ● | ● | ○ | ○ | ○ | ◐[^pcalg-ivent] | | dagitty | ◐[^dagitty-backdoor] | ○ | ○ | ○ | ○ | ○ | ◐[^dagitty-mutilate] | | bnlearn | ○ | ○ | ○ | ○ | ○ | ● | ● | | ggm | ○ | ○ | ○ | ○ | ○ | ○ | ○ | | MixedGraphs | ○ | ○ | ○ | ○ | ○ | ○ | ◐[^mixedgraphs-mutilate] | | NetworkX | ○ | ○ | ○ | ○ | ○ | ○ | ○ | | pgmpy | ● | ● | ○ | ○ | ○ | ○ | ● | | Tetrad | ◐[^tetrad-backdoor] | ● | ◐[^tetrad-optimal] | ○ | ○ | ○ | ○ | : Overview of available causal-inference algorithms in the packages in the comparison. [^caugi-optimal]: `adjustment_set(cg, X, Y, type = "optimal")` returns the O-set of Henckel/Perković/Maathuis (2019), but only for DAGs---the underlying Rust implementation errors on non-DAG graph classes. There is no O-set support for CPDAGs, MPDAGs, MAGs, or PAGs. [^pcalg-backdoor]: `backdoor(amat, x, y, type)` implements the Generalized Backdoor Criterion (GBC) of Maathuis & Colombo (2015), which subsumes Pearl's classical back-door criterion. For DAGs the GBC reduces to Pearl's back-door, so back-door is available as a special case, but the function does not specifically enforce or name the classical back-door separately. Generalized adjustment is `adjustment(amat, amat.type, x, y, set.type)` (full GAC, Perkovic et al. 2015/2018), and optimal is `optAdjSet(graphEst, x.pos, y.pos)` (Henckel/Perkovic/Maathuis 2019, DAG/CPDAG/PDAG only). [^pcalg-ivent]: `rmvnorm.ivent(n, object, target, target.value)` simulates from an interventional distribution of a `GaussParDAG` by fixing target nodes --- implements the do-operator semantics within the simulation framework but does not mutilate the graph object itself. There is no exported `mutilate()` or `do()` helper that returns a modified graph with incoming edges removed from intervention nodes. [^dagitty-backdoor]: `adjustmentSets(effect = "total")` and `isAdjustmentSet()` both invoke `GraphTransformer.backDoorGraph()` in the JS engine---Pearl's back-door graph construction, not the generalized Perkovic criterion. The R documentation cites Perkovic et al. (2015), but the JS implementation (`listMsasTotalEffect` in `jslib/graph/GraphAnalyzer.js`) applies the back-door graph uniformly across dag/pdag/mag/pag. Generalized adjustment scores `○` because no separate generalized-adjustment code path exists. `type = "canonical"` returns the ancestor-based canonical set but is not an optimality criterion in the Rotnitzky/Henckel sense. [^dagitty-mutilate]: `backDoorGraph(x)` removes the first directed edge on every proper causal path (with PAG handling via `pagToPdag` conversion)---the back-door-graph helper, not a general `do()`-style mutilation that removes all incoming edges to an arbitrary intervention set. No `mutilate()` or `do()` function is exported. [^mixedgraphs-mutilate]: `mutilate()` removes edges adjacent to a specified vertex set and supports a `dir = -1` option that removes only incoming edges. It can mechanically produce the mutilated graph required by `do()`, but the function is documented as a general edge-deletion utility with no causal semantics, no treatment of bidirected edges in the do-calculus sense, and no convenience wrapper that enforces "remove all incoming edges to the intervention set." [^tetrad-backdoor]: `Paths.isSatisfyBackDoorCriterion(graph, x, y, z)` implements Pearl's back-door criterion check, but the implementation constructs `new Dag(graph)` internally and throws if the input is not a DAG. Back-door adjustment *sets* are not separately enumerated; the generalized `RecursiveAdjustment.adjustmentSets()` subsumes the back-door case for DAGs. [^tetrad-optimal]: `OSet.oSetDag(graph, X, Y)` and `OSet.oSetCpdag(graph, X, Y, maxPathLength)` implement the Henckel--Perković--Maathuis O-set for DAGs and (amenable) CPDAGs respectively. The `Ida` and `PdagPagIda` classes wrap this as `IDA_TYPE.OPTIMAL`. Coverage is restricted to DAG/CPDAG; O-sets for MAGs/PAGs are not implemented. ## I/O and Interoperability | Package | DOT | Mermaid | GraphML | JSON | Coerce to/from other graph classes | | :---------- | :--------------- | :------ | :------ | :------------- | :--------------------------------- | | **caugi** | ◐[^caugi-dot] | ● | ● | ●[^caugi-json] | ● | | igraph | ◐[^igraph-dot] | ○ | ● | ○ | ◐[^igraph-coerce] | | graph | ◐[^graph-dot] | ○ | ○ | ○ | ◐[^graph-coerce] | | gRbase | ○ | ○ | ○ | ○ | ◐[^grbase-coerce] | | pcalg | ○ | ○ | ○ | ○ | ◐[^pcalg-coerce] | | dagitty | ○ | ○ | ○ | ○ | ◐[^dagitty-coerce] | | bnlearn | ◐[^bnlearn-dot] | ○ | ○ | ○ | ● | | ggm | ○ | ○ | ○ | ○ | ◐[^ggm-coerce] | | MixedGraphs | ○ | ○ | ○ | ○ | ● | | NetworkX | ◐[^networkx-dot] | ○ | ● | ● | ◐[^networkx-coerce] | | pgmpy | ◐[^pgmpy-dot] | ○ | ○ | ○ | ◐[^pgmpy-coerce] | | Tetrad | ◐[^tetrad-dot] | ○ | ○ | ● | ○ | : Comparison of supported graph I/O formats and interoperability features. [^caugi-dot]: `to_dot()` / `write_dot()` write Graphviz DOT format, but caugi has no DOT reader---round-trip via DOT is not supported. [^caugi-json]: `caugi_serialize()` / `caugi_deserialize()` (and `read_caugi()` / `write_caugi()`) read and write caugi's own JSON-based serialization format (Rust-side: `src/rust/src/lib.rs`). It is not a standardized cross-package JSON schema. [^igraph-dot]: DOT (Graphviz) format is supported for writing only (`write_graph(format = "dot")`). Reading DOT format is not supported. `read_graph()` accepts: edgelist, pajek, ncol, lgl, graphml, dimacs, graphdb, gml, dl. [^igraph-coerce]: igraph provides explicit coercion to/from `graphNEL` objects from the Bioconductor `graph` package via `as_graphnel()` and `graph_from_graphnel()`. No coercion helpers exist for dagitty, bnlearn, pcalg, or other causal R package object classes. [^graph-dot]: Write only. `toDotWithRI()` and `toDotR()` serialise `graphNEL` objects to DOT (Graphviz) format. No DOT reader is provided in the `graph` package. [^graph-coerce]: Internal coercions are provided: `as(graphNEL, "graphAM")`, `as(graphAM, "graphNEL")`, `as(graphBAM, "graphNEL")`, `as(matrix, "graphNEL")`, `as(matrix, "graphAM")`, `graph2SparseM()` / `sparseM2Graph()` (requiring optional `SparseM`). No coercions to or from `igraph`, `dagitty`, or `bnlearn` objects are exported by `graph` itself. [^grbase-coerce]: gRbase provides `as()` S4 coercion methods that convert among igraph, graphNEL (Bioconductor `graph`), dense adjacency matrix, and sparse `dgCMatrix` representations. There are no explicit coercion helpers for other *causal-graph* packages (dagitty, bnlearn, pcalg). [^pcalg-coerce]: `pcAlgo` and `fciAlgo` objects can be coerced to `graphAM` / `graphNEL` (from `graph`) via S4 coercion methods. The package wraps `graphNEL` as its primary DAG container, and `iplotPC()` converts to `igraph` for plotting. Incoming coercion (from igraph, bnlearn, dagitty, etc. into pcalg objects) is not provided. [^dagitty-coerce]: `convert(x, to = "igraph")` and `convert(x, to = "causaleffect")` produce igraph objects (one-way: dagitty → igraph only). `as.dagitty.bn()` converts bnlearn `bn` objects to dagitty (one-way the other direction). `lavaanToGraph()` converts lavaan models to dagitty. No coercion to/from `graphNEL`, `pcalg`, or other classes. [^bnlearn-dot]: `write.dot()` exports the network structure to Graphviz DOT format, but there is no corresponding `read.dot()`---DOT support is write-only. [^ggm-coerce]: `grMAT()` converts graphNEL (from `graph`) and igraph objects *into* ggm adjacency matrices. `plotGraph()` also accepts graphNEL/igraph as input. There is no function to export a ggm adjacency matrix back to a graphNEL or igraph object; coercion is one-directional (into ggm's format). [^networkx-dot]: DOT format read (`read_dot`) and write (`write_dot`) are available via `networkx.drawing.nx_pydot`, but `pydot` is an optional external dependency that must be installed separately. A pending deprecation warning has also been filed for `nx_pydot` (issue #5723). [^networkx-coerce]: NetworkX provides explicit conversion helpers to/from NumPy arrays, SciPy sparse arrays, and Pandas DataFrames. Conversion to/from other *graph* objects (igraph, graph-tool) is handled by those packages' own `from_networkx()` / `to_networkx()` methods, not by NetworkX itself. [^pgmpy-dot]: `DAG.to_graphviz()` returns a `pygraphviz.AGraph` from which DOT text can be obtained via `.to_string()`. There is no direct `to_dot()` / `from_dot()` API; reading DOT is not supported through pgmpy's own API. [^pgmpy-coerce]: pgmpy graph classes (`DAG`, `PDAG`, `ADMG`) inherit from `networkx.DiGraph`/`networkx.MultiDiGraph`, so they are directly usable as NetworkX objects. There are no explicit coercion helpers to or from other graph-package objects (igraph, R dagitty objects, bnlearn objects). [^tetrad-dot]: `GraphSaveLoadUtils.graphToDot(graph)` writes Graphviz DOT format (string and file variants). There is no DOT reader, so round-trip is not supported. Write-only. ## Implementation and Ergonomics | Package | Backend | Sparse storage | Built-in plotting | Layout algorithms | Pipeable/fluent API | Lazy mutation | | :---------- | :---------------- | :--------------------- | :------------------- | :----------------- | :------------------- | :------------ | | **caugi** | R + Rust | ● | ● | ● | ● | ● | | igraph | C | ◐[^igraph-sparse] | ● | ● | ○ | ○ | | graph | R + C | ●[^graph-sparse] | ○[^graph-plot] | ○ | ○ | ○ | | gRbase | R + C++ | ● | ◐[^grbase-plot] | ○ | ○ | ○ | | pcalg | R + C++ | ○ | ◐[^pcalg-plot] | ○ | ○ | ○ | | dagitty | R + JS | ○ | ● | ◐[^dagitty-layout] | ○ | ○ | | bnlearn | R + C | ◐[^bnlearn-sparse] | ◐[^bnlearn-plot] | ◐[^bnlearn-layout] | ○ | ○ | | ggm | R | ○ | ● | ◐[^ggm-layout] | ○ | ○ | | MixedGraphs | R + C++ | ◐[^mixedgraphs-sparse] | ◐[^mixedgraphs-plot] | ○ | ◐[^mixedgraphs-pipe] | ○ | | NetworkX | Python | ◐[^networkx-sparse] | ◐[^networkx-plot] | ● | ○ | ○ | | pgmpy | Python (NetworkX) | ○ | ◐[^pgmpy-plot] | ◐[^pgmpy-layout] | ○ | ○ | | Tetrad | Java | ● | ◐[^tetrad-plotting] | ● | ○ | ○ | : Comparison of implementation details and ergonomic features of the packages in the comparison. [^igraph-sparse]: The C igraph library uses adjacency-list storage internally. On the R side, `as_adjacency_matrix(sparse = TRUE)` and `graph_from_adjacency_matrix()` accept/return sparse `Matrix` objects (`dgCMatrix`), but the igraph object itself is an opaque external pointer backed by the C library's adjacency-list representation, not an R-level sparse matrix. [^graph-sparse]: `graphNEL` uses an adjacency-list representation (sparse by nature). `graphBAM` (experimental) stores adjacency as a compact bit-array. `graph2SparseM()` / `sparseM2Graph()` convert to/from `SparseM` sparse matrices when that package is installed. [^graph-plot]: No built-in plotting. DOT serialisation helpers (`toDotWithRI`, `toDotR`) are provided, but actual rendering requires the separate `Rgraphviz` package. No layout algorithms are implemented in `graph` itself. [^grbase-plot]: gRbase exports `iplot()` / `graph-iplot`, a thin wrapper around igraph's `plot.igraph()`. There are no built-in layout algorithms: layout selection is delegated entirely to igraph. [^pcalg-plot]: `pcAlgo` and `fciAlgo` objects have `plot()` S4 methods that delegate to `Rgraphviz`. `iplotPC()` delegates to `igraph`'s plot. `plotSG()` also requires `Rgraphviz`. There are no native layout algorithms in pcalg itself. [^dagitty-layout]: `graphLayout(x, method = "spring")` implements a single spring/force-directed layout algorithm. The documentation explicitly states `"currently, only 'spring' is supported"`. [^bnlearn-sparse]: The `bn` class stores arcs as a 2-column character matrix (`$arcs`) plus per-node children/parents lists in `$nodes`. The exported `alst()` function exposes an adjacency-list view. This is effectively sparse but is not a formal CSR/CSC or `Matrix`-class sparse type. [^bnlearn-plot]: A rudimentary `plot.bn()` S3 method draws nodes in a circle with no external dependencies, but the package documentation describes it as "a last resort for when Rgraphviz is not available." The full-featured `graphviz.plot()` requires the Bioconductor `Rgraphviz` package. [^bnlearn-layout]: All graph layout computation is delegated to Graphviz via the `Rgraphviz` package (algorithms: dot, neato, twopi, circo, fdp); bnlearn implements no native layout algorithms. [^ggm-layout]: `plotGraph()` and `drawGraph()` delegate layout to igraph (default `layout.auto`), so any igraph layout function can be passed via the `layout` argument. The layouts are not implemented natively in ggm. [^mixedgraphs-sparse]: Edge data can be stored in adjacency-list format (`adjList` class), which is a sparse representation. However, the package also supports dense adjacency matrices (`adjMatrix` class) and does not commit to a single internal format. No CSR/CSC or other documented sparse-matrix format is enforced. [^mixedgraphs-plot]: `plot.mixedgraph()` is an S3 method but it delegates entirely to the `pcalg` / `Rgraphviz` stack by converting the graph to a `fciAlgo` object. Layout is determined by Rgraphviz; no layout algorithms are implemented within MixedGraphs. The function is listed under `Suggests` (`pcalg`, `Rgraphviz`), so plotting is unavailable without those packages. [^mixedgraphs-pipe]: The package exports a `%G%` infix operator that automatically converts a `mixedgraph` into the format expected by the right-hand-side function (from another package), enabling cross-package chaining. This is a narrow inter-package bridge operator rather than a full tidyverse-style verb chain that takes and returns `mixedgraph` objects. [^networkx-sparse]: NetworkX stores graphs internally as a dict-of-dicts-of-dicts (adjacency dictionary), which is *effectively* sparse for large, sparse graphs (only present edges are stored), but this is not a CSR/CSC/COO sparse matrix---it is a plain Python dictionary structure. Conversion to SciPy sparse arrays is available via `to_scipy_sparse_array`. [^networkx-plot]: NetworkX includes `nx.draw()`, `nx.draw_networkx()`, and related functions that render graphs via Matplotlib (`networkx.drawing.nx_pylab`). Matplotlib is an optional dependency --- NetworkX raises `ImportError` when it is absent. The drawing module is described in the documentation as basic/non-primary functionality. [^pgmpy-plot]: `DAG.to_graphviz()` returns a pygraphviz `AGraph` (requires the optional `pygraphviz` dependency); `DAG.to_daft()` returns a `daft` PGM object. Neither is a `plot()` method that renders to screen directly. No built-in `matplotlib`-based `plot()` is present. [^pgmpy-layout]: Layout computation is delegated to pygraphviz (via `to_graphviz()`) or to NetworkX spring-layout variants (inside `to_daft()`). pgmpy does not implement its own layout algorithms. [^tetrad-plotting]: Tetrad's GUI module (`tetrad-gui`) provides rich interactive graph visualization, but this is a Swing-based desktop application component, not a programmatic plotting API. From the library (`tetrad-lib`), `LayoutUtil` computes node coordinates (Kamada-Kawai, Fruchterman-Reingold, circle, square, causal-order layouts) and `GraphSaveLoadUtils.graphToDot()` exports to Graphviz for external rendering. There is no `plot(graph)` method in the library that renders directly to a file or screen without the GUI. ## Contributing If you find any errors in the comparison above or would like to add another package for comparisons, please file an issue or submit a pull request with the relevant information.