Examples

Drawing a recovered structure#

Open in Colab Download this notebook

andrey.viz.draw renders a recovered graph to a self-contained SVG. The grammar is simple: marks are drawn by shape (arrowhead, hollow circle, tail), so color stays a redundant channel; resolved edges are a calm graphite backbone and color is reserved for what remains uncertain.

Start#

Choose a CPU runtime and run this cell first. In Colab it installs andrey-core==0.9.0 from PyPI; local kernels use the installed environment.

import os
import sys

IN_COLAB = "google.colab" in sys.modules

if IN_COLAB:
    os.environ["ANDREY_DEVICE"] = "cpu"
    !pip install --quiet "andrey-core[viz]==0.9.0" pandas
import numpy as np
import pandas as pd

import andrey
import andrey.viz

Recover a CPDAG from linear-Gaussian data over a four-node structure: a fork x0 -> x1, x0 -> x2, closing into a collider at x3. The columns come in an order drawn from the seed, so their order says nothing about the causal order.

EDGES = [("x0", "x1"), ("x0", "x2"), ("x1", "x3"), ("x2", "x3")]  # cause first


def sample(seed: int, n: int = 2000, shuffle: bool = True) -> pd.DataFrame:
    """Draw n rows of x0..x3, the columns in an order drawn from the seed."""
    rng = np.random.default_rng(seed)
    x0 = rng.standard_normal(n)
    x1 = 0.9 * x0 + rng.standard_normal(n)
    x2 = 0.8 * x0 + rng.standard_normal(n)
    x3 = 0.9 * x1 + 0.9 * x2 + rng.standard_normal(n)
    frame = pd.DataFrame({"x0": x0, "x1": x1, "x2": x2, "x3": x3})
    return frame[rng.permutation(frame.columns)] if shuffle else frame


data = sample(seed=0)
out = andrey.ges(data)
print("kind:", out.structure.kind)
print("edges:", out.structure.oriented_edges())
kind: cpdag
edges: [('x1', 'x3', 'directed'), ('x1', 'x0', 'undirected'), ('x2', 'x3', 'directed'), ('x2', 'x0', 'undirected')]

Draw it in both themes at once. draw() returns an SVG, a str subclass carrying a _repr_html_, so composing a figure is plain string assembly: the Panels helper below stitches the light and dark renders into one captioned row, and every piece it stitches is markup draw() already produced. Both palettes are first-class, so a figure is never shipped in one theme only. The DataFrame’s column names label the nodes: the collider at x3 is oriented (both edges the graphite backbone), and whatever the CPDAG leaves unoriented is the undirected amber.

class Panels:
    """A labeled row of renders; ``_repr_html_`` lets the notebooks embed it."""

    def __init__(self, **renders: str) -> None:
        self.renders = renders

    def _repr_html_(self) -> str:
        cells = "".join(
            f"<figure>{svg}<figcaption>{label}</figcaption></figure>"
            for label, svg in self.renders.items()
        )
        return f'<div class="andrey-figure-pair">{cells}</div>'


Panels(Light=andrey.viz.draw(out.structure), Dark=andrey.viz.draw(out.structure, dark=True))
x1x3x2x0
Light
x1x3x2x0
Dark

Each render is also just a string. background=False drops the paper fill for a transparent SVG that composites onto an already-themed page, and the markup writes straight to a file.

svg = andrey.viz.draw(out.structure, background=False)
print("markup:", svg[:60], "...")
print("bytes:", len(svg))
markup: <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 360 240 ...
bytes: 1679