Compare two graphs¶
The fastest path to a full comparison is bnmetrics.compare, which runs
every comparative metric and packages the results into a
Comparison value object.
Worked example: the diamond fixture¶
The canonical pedagogical fixture is the four-node diamond DAG \(A \to B,\; A \to C,\; B \to D,\; C \to D\). Its CPDAG keeps the v-structure into \(D\) directed but leaves the upper edges \(A - B\) and \(A - C\) undirected (they lie in the Markov-equivalence class). A common recovery error at moderate sample sizes is to flip these upper edges, returning \(B \to A\) and \(C \to A\) — two reversals.
import numpy as np
import bnmetrics
# True CPDAG of the diamond.
true_cpdag = bnmetrics.to_graphlike(
np.array([
# cols A B C D
[0, 1, 1, 0], # A
[1, 0, 0, 2], # B
[1, 0, 0, 2], # C
[0, 1, 1, 0], # D
], dtype=np.int8),
var_names=("A", "B", "C", "D"),
)
# Recovered CPDAG with both upper edges reversed.
recovered = bnmetrics.to_graphlike(
np.array([
# cols A B C D
[0, 1, 1, 0], # A
[2, 0, 0, 2], # B
[2, 0, 0, 2], # C
[0, 1, 1, 0], # D
], dtype=np.int8),
var_names=("A", "B", "C", "D"),
)
Single-metric calls¶
For interactive inspection of one metric, the standalone functions
are equivalent to (and faster than) extracting the same field from
a Comparison:
bnmetrics.shd(true_cpdag, recovered) # 2
bnmetrics.hd(true_cpdag, recovered) # 0 — skeleton-only
bnmetrics.f1(true_cpdag, recovered) # 0.5
bnmetrics.precision(true_cpdag, recovered) # 0.5
bnmetrics.recall(true_cpdag, recovered) # 0.5
bnmetrics.count_reversals(true_cpdag, recovered) # 2
The Hamming distance is zero because both graphs share the same skeleton; all error is concentrated in orientation. SHD = 2 and \(F_1 = 0.5\) together localise the discrepancy to two reversals.
Multi-metric Comparison¶
comp = bnmetrics.compare(true_cpdag, recovered)
comp.comparative["shd"] # 2.0
comp.comparative["f1"] # 0.5
comp.comparative["reversals"] # 2.0
comp.comparative is a dictionary keyed by metric name; the
available keys are additions, deletions, reversals, shd,
hd, tp, fp, fn, precision, recall, f1. Descriptive
metrics for each input graph are exposed on comp.g1_descriptive
and comp.g2_descriptive.
Tidy DataFrame export¶
bnmetrics.to_dataframe flattens a Comparison into a single-row
pandas.DataFrame combining descriptive and comparative metrics
(requires the pandas extra):
df = bnmetrics.to_dataframe(comp)
df[["shd", "hd", "f1", "precision", "recall",
"additions", "deletions", "reversals"]]
shd hd f1 precision recall additions deletions reversals
2.0 0.0 0.5 0.5 0.5 0.0 0.0 2.0
Variable-name alignment¶
bnmetrics aligns variables by name when both inputs expose
var_names; otherwise it aligns positionally. A BNMDataError
is raised if the two graphs have different variable counts or
non-matching name sets, since silent positional alignment of
inconsistently-named graphs is the most common source of subtle
metric bugs.