Visualising the Markov blanket

The Markov blanket of a variable \(X\) in a DAG is the set of nodes that, when conditioned on, renders \(X\) independent of every other variable — formally, the union of its parents, children, and the parents of its children (its spouses).

Extracting the Markov blanket

bnmetrics.markov_blanket(g, var) returns a sub-GraphLike restricted to \(\{X\} \cup \mathrm{MB}(X)\), preserving every endpoint mark incident to those nodes. bnmetrics.markov_blanket_indices returns the indices only.

import numpy as np
import bnmetrics

# 6-node DAG: A → C, B → C, C → D, C → E, D → E, E → F
endpoints = np.zeros((6, 6), dtype=np.int8)
for i, j in [(0, 2), (1, 2), (2, 3), (2, 4), (3, 4), (4, 5)]:
    endpoints[i, j] = 2
    endpoints[j, i] = 1
g = bnmetrics.to_graphlike(endpoints,
                     var_names=("A", "B", "C", "D", "E", "F"))

bnmetrics.markov_blanket_indices(g, "C")     # (0, 1, 2, 3, 4)
mb_c = bnmetrics.markov_blanket(g, "C")
mb_c.var_names                          # ('A', 'B', 'C', 'D', 'E')

Node F is excluded because it is a descendant of E but not a spouse of C. The MB of C is \(\{A, B\} \cup \{D, E\} \cup \{D\} = \{A, B, D, E\}\).

Plotting the Markov-blanket subgraph

bnmetrics.plot_graph accepts the MB subgraph directly. Highlighting the target node makes the blanket structure immediately legible:

bnmetrics.plot_graph(
    mb_c,
    title="MB(C)",
    highlight=["C"],
    direction="LR",
    save="mb_c.svg",
)

Markov blanket of C with target highlighted

The highlight argument paints the listed nodes in a distinguishing colour (default pastel green); every other element follows the default style.

Comparing two graphs on the Markov blanket of a target

When two graphs are being compared and the analysis of interest is local to a particular variable, scope both graphs to the MB of that target before passing them to a metric or visualiser. The example below compares the true DAG above against a recovery that has dropped the \(D \to E\) edge:

endpoints_rec = endpoints.copy()
endpoints_rec[3, 4] = 0
endpoints_rec[4, 3] = 0
g_rec = bnmetrics.to_graphlike(endpoints_rec,
                         var_names=("A", "B", "C", "D", "E", "F"))

mb_true = bnmetrics.markov_blanket(g,     "C")
mb_rec  = bnmetrics.markov_blanket(g_rec, "C")

bnmetrics.shd(mb_true, mb_rec)              # 1 — the missing D → E edge
bnmetrics.plot_side_by_side(
    mb_true, mb_rec,
    name1="true_MB(C)", name2="recovered_MB(C)",
    direction="LR", save="mb_comparison.svg",
)

true MB(C)

recovered MB(C)

true MB(C)

recovered MB(C)

The MB-scoped SHD is identical to the global SHD here because the dropped edge falls inside \(\mathrm{MB}(C)\). In general the MB-scoped distance is a lower bound on the global distance, and the gap quantifies how much of the recovery error is local to the target.

Per-node Markov-blanket summary

bnmetrics.analyse_mb produces a multi-panel plotly figure summarising the per-node MB descriptive metrics (one panel per variable, faceted by metric). Each panel reports the descriptive metrics restricted to the corresponding MB subgraph:

fig = bnmetrics.analyse_mb(
    g,
    descriptive=["n_edges", "n_directed_arcs", "n_colliders"],
    cols=3,
)
fig.write_html("mb_summary.html")

Per-node MB descriptive summary

descriptive accepts either a list of metric names or the literal string "all". The figure is a standard plotly.graph_objs.Figure, so it can be saved with .write_html, .write_image (requires kaleido), or displayed directly in a notebook.