ergodicDocs
Products: switch from ergodic to another product
Get started

GuidesDecode a DAG from edge probabilities

Decode a DAG from edge probabilities

Turn a matrix of edge probabilities into a DAG with a named decoder, and see which edges it dropped and why. You'll compare decoders, draw candidate DAGs with a seeded sampler, and constrain the result with domain knowledge.

You'll need

Run the program#

The matrix is the one from Work with edge probabilities: four edges pass 0.5, and three of them form a cycle.

decode_a_graph.pyDownload
import numpy as np

from ergodic import DomainKnowledge, EdgeProbabilities, decode

# 1. Independent edge probabilities; three edges above 0.5 form a cycle.
names = ("rain", "sprinkler", "wet", "slippery")
values = np.array(
    [
        [0.00, 0.45, 0.91, 0.06],
        [0.40, 0.00, 0.83, 0.05],
        [0.04, 0.07, 0.00, 0.88],
        [0.58, 0.02, 0.10, 0.00],
    ]
)
probs = EdgeProbabilities(names, values, kind="independent")

# 2. The default decoder: greedy pair-MAP.
result = decode(probs)
print(result)
print(result.graph)
print(result.diagnostics["rejected_cycles"], result.diagnostics["note"])

# 3. A lower threshold nominates more edges.
print(decode(probs, threshold=0.3).graph)

# 4. Exact MAP and the expected-count decoder.
print(decode(probs, "exact_map").graph)
print(decode(probs, "expected_count").graph)

# 5. Candidate DAGs from a seeded sampler, and their consensus.
sampled = decode(probs, "pair_sampler", samples=200, seed=0)
print(sampled)
print(sampled.graph)

# 6. Knowledge: nothing causes the sprinkler.
knowledge = DomainKnowledge().root("sprinkler")
print(decode(probs, threshold=0.3, knowledge=knowledge).graph)
Recorded output
DecodeResult(decode:greedy_v1: decoded dag, 4 nodes, 3 edges)
MixedGraph(kind=dag, nodes=4, edges=[rain --> wet, slippery <-- wet, sprinkler --> wet])
(1,) not a graph posterior; orientation not certified
MixedGraph(kind=dag, nodes=4, edges=[rain --> sprinkler, rain --> wet, slippery <-- wet, sprinkler --> wet])
MixedGraph(kind=dag, nodes=4, edges=[rain --> wet, slippery <-- wet, sprinkler --> wet])
MixedGraph(kind=dag, nodes=4, edges=[rain --> sprinkler, rain --> wet, slippery <-- wet, sprinkler --> wet])
DecodeResult(decode:pair_sampler_v1: decoded dag, 4 nodes, 3 edges, consensus of 200 candidates)
MixedGraph(kind=dag, nodes=4, edges=[rain --> wet, slippery <-- wet, sprinkler --> wet])
MixedGraph(kind=dag, nodes=4, edges=[rain <-- sprinkler, rain --> wet, slippery <-- wet, sprinkler --> wet])

Choose a decoder#

Every decoder reads the pair-state view (see Work with edge probabilities) and scores an edge by w(i -> j) = log P(i -> j) - log P(no edge).

Decoder What it does Settings
"greedy" (greedy_v1, default) Each pair's better direction when w > 0 (or its value is above threshold), strongest first, skipping cycles. threshold
"exact_map" (exact_map_v1) The DAG with the best total score, found exactly by A* search over orderings. max_nodes
"expected_count" (expected_count_v1) The best K edges compatible with the exact ordering, K the rounded expected edge count. max_nodes
"soft_f1" (soft_f1_v1) The same, with K from a label-free F1 estimate. max_nodes
"pair_sampler" (pair_sampler_v1) Draws each pair's state, adds edges in random order, drops those that close a cycle. samples, seed
"perturb_and_map" (perturb_and_map_v1) Exact MAP under randomly perturbed pair scores, once per sample. samples, seed, max_nodes

Greedy is the default. In the research it came from it matched exact MAP on 238 of 240 graphs. It isn't best everywhere: on one physical benchmark the expected-count decoder scored better. The decoders that search orderings exactly refuse more than max_nodes variables (20 by default), because that search can grow exponentially.

Read the result#

decode returns a DecodeResult. graph is a DAG over every variable, method is "decode:<id>", candidates holds every decoded DAG and probabilities the input. diagnostics records:

Key What it holds
decoded, native Always True and False.
decoder, parameters The versioned id and its settings, such as threshold.
threshold_source "decoder" when you passed a threshold, None otherwise. Through Welt it can be "welt_native".
seed, samples The sampler's seed and draw count (None and 1 otherwise).
rejected_cycles Per candidate, how many nominated edges were skipped for closing a directed cycle.
order_skipped Per candidate, how many were skipped only because they'd break the order knowledge.
objective The total log score of graph.
edge_budget K, for the cardinality decoders.
source_kind, source_provenance Where the probabilities came from.
note "not a graph posterior; orientation not certified".

A decoded graph is your choice, not the model's claim

A model that scores edges one at a time doesn't promise a DAG. The decoder picks one, and a different decoder, threshold or seed can pick another. Sampler candidates are not draws from a posterior over graphs, and nothing certifies the orientations.

Decoding is deterministic: ties go to the earlier variable, the same seed gives the same candidates, and neither the input nor numpy's global random state changes.

Use domain knowledge#

decode(..., knowledge=...) applies hard constraints:

Every variable the knowledge names must be in the probabilities. The decoded graph is checked against the knowledge before it's returned.

Decode a Welt model's scores#

AVICI and CDFM return directed graphs that may hold cycles, so ergodic.welt.discover refuses them before anything is paid for. Pass decoder= to opt in: discover(data, "cdfm", decoder="greedy") runs the model once, reads its scores as independent edge probabilities and decodes them. Greedy then uses the model's own threshold (0.5 for AVICI, CDFM's adaptive one). The result keeps the scores in diagnostics["edge_probabilities"], so you can try another decoder locally without a new run. The exact decoders' max_nodes cap (20 by default) is checked against your data's variables before anything is uploaded. See Decode AVICI and CDFM.

Arrow returns a DAG of its own, so ergodic uses that by default. Its scores are decoded only if you ask, with decoder=, and are then read as independent marginals, like AVICI's and CDFM's. That's a modelling choice: Welt doesn't state that Arrow's scores are marginals.

Next steps#

Documentation

Search ergodic 0.2.0

Search ergodic by task, function or error.