Skip to content

Evidence

Evidence is the set of numbers describing the related cases around one decision: how many there are, how densely linked, how much linked cases agree, how unusual this case is, and how many related cases point the same way as the proposed action. This package has the four per-decision checks that read those numbers (neighbour agreement, knife-edge, unusual case, support) and snapshot_from_graph, which computes the numbers from your own labelled graph. Open this page when you build an EvidenceSnapshot from data or want to call one check on its own.

Public here: snapshot_from_graph, reconstruction_report, threshold_report (top level); conformal_report, conformal_p_value, lambda_from_agreements, operational_width and theorem_width from the submodules; support_report from grounded_decision.evidence. Recipe: Build evidence from your data; default values: Defaults and limits.

reconstruction

Kesten–Stigum reconstruction gate (family 229).

Lean covers the supercritical direction: on a b-ary (or Poisson-mean-d) tree, root reconstruction holds when d λ² > 1. This module implements that gate and an estimator of λ from parent–child agreements.

Non-reconstruction at equality is treated as abstain by policy, and is tagged PAPER rather than LEAN.

lambda_from_agreements

lambda_from_agreements(
    agreements: Sequence[bool], n_states: int = 2
) -> float

Symmetric-channel correlation from empirical parent–child equality.

For a q-state symmetric channel, λ = (q p_same − 1) / (q − 1). This is the default estimator (agreement_baseline="uniform"): it assumes labels in equal proportions, as the Kesten–Stigum model does. See lambda_against_label_mix for when the other baseline fits.

Source code in src/grounded_decision/evidence/reconstruction.py
def lambda_from_agreements(agreements: Sequence[bool], n_states: int = 2) -> float:
    """Symmetric-channel correlation from empirical parent–child equality.

    For a q-state symmetric channel, λ = (q p_same − 1) / (q − 1).
    This is the default estimator (``agreement_baseline="uniform"``): it
    assumes labels in equal proportions, as the Kesten–Stigum model does.
    See ``lambda_against_label_mix`` for when the other baseline fits.
    """

    if not agreements:
        return 0.0
    q = max(2, n_states)
    p_same = sum(1 for a in agreements if a) / len(agreements)
    return (q * p_same - 1.0) / (q - 1.0)

lambda_against_label_mix

lambda_against_label_mix(
    labels: Sequence[int], edges: Sequence[tuple[int, int]]
) -> float

Neighbour agreement measured against the graph's own label mix.

lambda_from_agreements compares agreement with what random labels in equal proportions would give (1/q). When one label dominates (most accounts are clean), random links already agree most of the time, so that estimate is inflated: with a 90/10 split, links that carry no information still score about 0.64.

This estimator compares instead with the agreement expected if the same labels were attached to the same graph at random: the chance that both ends of a random edge carry the same label, weighting each label by the total degree of the vertices that carry it. It is Cohen's kappa for edge endpoints:

λ = (p_same − p_chance) / (1 − p_chance).

It equals the uniform estimate when labels are balanced, and is 0 for links unrelated to the labels whatever the mix.

It is not the default. The Kesten–Stigum result behind the reconstruction check is stated for labels in equal proportions, which is what lambda_from_agreements estimates. This estimator also misjudges homogeneous rings: a ring that is nearly all fraud is strong evidence, but with little variation inside it the chance agreement is close to 1 and the estimate falls toward 0. Use it when the imbalance is population-wide and links may be random. Which way the related cases point is the support check's job, and learning certifies the whole pipeline's error rate.

Source code in src/grounded_decision/evidence/reconstruction.py
def lambda_against_label_mix(
    labels: Sequence[int], edges: Sequence[tuple[int, int]]
) -> float:
    """Neighbour agreement measured against the graph's own label mix.

    ``lambda_from_agreements`` compares agreement with what random labels
    *in equal proportions* would give (1/q). When one label dominates
    (most accounts are clean), random links already agree most of the time,
    so that estimate is inflated: with a 90/10 split, links that carry no
    information still score about 0.64.

    This estimator compares instead with the agreement expected if the same
    labels were attached to the same graph at random: the chance that both
    ends of a random edge carry the same label, weighting each label by the
    total degree of the vertices that carry it. It is Cohen's kappa for edge
    endpoints:

        λ = (p_same − p_chance) / (1 − p_chance).

    It equals the uniform estimate when labels are balanced, and is 0 for
    links unrelated to the labels whatever the mix.

    It is not the default. The Kesten–Stigum result behind the
    reconstruction check is stated for labels in equal proportions, which
    is what ``lambda_from_agreements`` estimates. This estimator also
    misjudges homogeneous rings: a ring that is nearly all fraud is strong
    evidence, but with little variation inside it the chance agreement is
    close to 1 and the estimate falls toward 0. Use it when the imbalance
    is population-wide and links may be random. Which way the related
    cases point is the support check's job, and learning certifies the
    whole pipeline's error rate.
    """

    valid_edges = [
        (u, v) for u, v in edges if 0 <= u < len(labels) and 0 <= v < len(labels)
    ]
    if not valid_edges:
        return 0.0
    agreeing = sum(1 for u, v in valid_edges if labels[u] == labels[v])
    endpoint_label_counts: dict[int, int] = {}
    for u, v in valid_edges:
        endpoint_label_counts[labels[u]] = endpoint_label_counts.get(labels[u], 0) + 1
        endpoint_label_counts[labels[v]] = endpoint_label_counts.get(labels[v], 0) + 1
    # With E edges, A agreeing, T = 2E endpoints and S the sum of squared
    # endpoint counts: p_same = A / E and p_chance = S / T², so
    # λ = (A T² − S E) / (E (T² − S)), a ratio of integers rounded once
    # (the same on every platform, design decision D69).
    edge_count = len(valid_edges)
    endpoint_total_squared = (2 * edge_count) ** 2
    squared_counts = sum(count * count for count in endpoint_label_counts.values())
    if squared_counts >= endpoint_total_squared:
        return 0.0
    return (agreeing * endpoint_total_squared - squared_counts * edge_count) / (
        edge_count * (endpoint_total_squared - squared_counts)
    )

thresholds

Friedgut–Kalai sharp-threshold window (family 186).

Lean: for every n ≥ 2, every nontrivial increasing relabeling-invariant graph property, and 0 < ε < 1/2, the p-interval from probability ε to 1−ε has width at most

2^{19} log(1/(2ε)) / (log n)².

At operational n this width is often ≥ 1 (vacuous on [0,1]). The runtime therefore reports both the theorem width and an operational width with prefactor Limits.operational_fk_prefactor. Channel C abstains when p_hat sits in the operational window around p_threshold.

conformal

Split conformal wrap.

The catalog does not replace finite-sample coverage. This channel is the practical high-probability wrapper around reconstruction's label.

conformal_p_value

conformal_p_value(
    label_score: float, calibration: Sequence[float]
) -> float

Higher scores are less conforming. Conservative p-value.

Source code in src/grounded_decision/evidence/conformal.py
def conformal_p_value(label_score: float, calibration: Sequence[float]) -> float:
    """Higher scores are less conforming. Conservative p-value."""

    if not calibration:
        return 1.0
    n = len(calibration)
    greater = sum(1 for s in calibration if s >= label_score)
    return (1 + greater) / (1 + n)

support

Support check: do the related cases support the proposed action?

The reconstruction check asks whether the related cases are informative at all: whether linked cases share labels more often than chance. It does not ask which way they point. A dense ring of accounts that are almost all confirmed fraud is very informative, and it says "fraud"; a proposal to clear an account inside that ring should be held even though every other check passes.

This check reads EvidenceSnapshot.support: the share (0 to 1) of related cases whose label is the label the proposed action implies. It passes when that share is at least Limits.min_support. It is a practical rule with an explicit constant, not a catalog result, so its trust level is always operational.

support_report

support_report(
    support: float | None, limits: Limits = DEFAULT_LIMITS
) -> ChannelReport

Report whether the related cases support the proposed action.

Parameters:

Name Type Description Default
support float | None

Share of related cases whose label matches the label the proposed action implies, or None when it was not measured.

required
limits Limits

Supplies min_support and require_support.

DEFAULT_LIMITS

Returns:

Type Description
ChannelReport

The support check. With support=None it passes unless

ChannelReport

limits.require_support is set; otherwise it passes when

ChannelReport

support >= limits.min_support.

Source code in src/grounded_decision/evidence/support.py
def support_report(
    support: float | None, limits: Limits = DEFAULT_LIMITS
) -> ChannelReport:
    """Report whether the related cases support the proposed action.

    Args:
        support: Share of related cases whose label matches the label the
            proposed action implies, or ``None`` when it was not measured.
        limits: Supplies ``min_support`` and ``require_support``.

    Returns:
        The ``support`` check. With ``support=None`` it passes unless
        ``limits.require_support`` is set; otherwise it passes when
        ``support >= limits.min_support``.
    """

    metrics: dict[str, float | int | bool | str] = {
        "min_support": limits.min_support,
        "require_support": limits.require_support,
        "measured": support is not None,
    }
    if support is None:
        passed = not limits.require_support
        summary = (
            "support not measured (require_support)"
            if limits.require_support
            else "support not measured"
        )
    else:
        metrics["support"] = support
        passed = support >= limits.min_support
        verdict = (
            "supports the proposed action"
            if passed
            else "does not support the proposed action"
        )
        summary = (
            f"support {support:.2f} vs minimum {limits.min_support:.2f} ({verdict})"
        )
    return ChannelReport(
        name=ChannelName.SUPPORT,
        passed=passed,
        trust=Trust.OPERATIONAL,
        summary=summary,
        metrics=metrics,
    )

graph

Small evidence-graph helpers (density, degree, agreement).

snapshot_from_graph

snapshot_from_graph(
    labels: Sequence[int],
    edges: Sequence[tuple[int, int]],
    *,
    p_threshold: float,
    n_states: int = 2,
    scores: Sequence[float] = (),
    label_score: float = 0.0,
    observations: Mapping[str, object] | None = None,
    agreement_baseline: Literal[
        "uniform", "label_mix"
    ] = "uniform",
    supporting_label: int | None = None,
) -> EvidenceSnapshot

Compute an EvidenceSnapshot from a graph of related cases.

Parameters:

Name Type Description Default
labels Sequence[int]

One label per vertex (for example 1 = confirmed fraud).

required
edges Sequence[tuple[int, int]]

Index pairs into labels; each is one link.

required
p_threshold float

The decision line for link density.

required
n_states int

Number of distinct label values.

2
scores Sequence[float]

Calibration scores for the conformal check.

()
label_score float

This case's unusualness score.

0.0
observations Mapping[str, object] | None

Extra values recorded with the snapshot.

None
agreement_baseline Literal['uniform', 'label_mix']

How neighbour agreement is measured. "uniform" (the default) compares with labels in equal proportions, which is what the Kesten–Stigum result behind the reconstruction check assumes. "label_mix" compares with this graph's own label proportions; see :func:~grounded_decision.evidence.reconstruction.lambda_against_label_mix. Use "label_mix" when the imbalance is population-wide (most accounts everywhere are clean) and links may be unrelated to labels. Do not use it to judge a homogeneous ring: a ring that is nearly all fraud is strong evidence but has little variation inside it, so "label_mix" scores it near zero. Which way the related cases point is the support check's job (supporting_label), and the whole pipeline's error rate is what learning certifies.

'uniform'
supporting_label int | None

The label the proposed action implies (for example 1 = confirmed fraud when the action is block). When given, support is the share of vertices with this label (0 for an empty graph). When omitted, support is None (not measured).

None
Source code in src/grounded_decision/evidence/graph.py
def snapshot_from_graph(
    labels: Sequence[int],
    edges: Sequence[tuple[int, int]],
    *,
    p_threshold: float,
    n_states: int = 2,
    scores: Sequence[float] = (),
    label_score: float = 0.0,
    observations: Mapping[str, object] | None = None,
    agreement_baseline: Literal["uniform", "label_mix"] = "uniform",
    supporting_label: int | None = None,
) -> EvidenceSnapshot:
    """Compute an ``EvidenceSnapshot`` from a graph of related cases.

    Args:
        labels: One label per vertex (for example 1 = confirmed fraud).
        edges: Index pairs into ``labels``; each is one link.
        p_threshold: The decision line for link density.
        n_states: Number of distinct label values.
        scores: Calibration scores for the conformal check.
        label_score: This case's unusualness score.
        observations: Extra values recorded with the snapshot.
        agreement_baseline: How neighbour agreement is measured.
            ``"uniform"`` (the default) compares with labels in equal
            proportions, which is what the Kesten–Stigum result behind the
            reconstruction check assumes. ``"label_mix"`` compares with
            this graph's own label proportions; see
            :func:`~grounded_decision.evidence.reconstruction.lambda_against_label_mix`.
            Use ``"label_mix"`` when the imbalance is population-wide (most
            accounts everywhere are clean) and links may be unrelated to
            labels. Do not use it to judge a homogeneous ring: a ring that
            is nearly all fraud is strong evidence but has little variation
            inside it, so ``"label_mix"`` scores it near zero. Which way
            the related cases point is the support check's job
            (``supporting_label``), and the whole pipeline's error rate is
            what learning certifies.
        supporting_label: The label the proposed action implies (for
            example 1 = confirmed fraud when the action is ``block``). When
            given, ``support`` is the share of vertices with this label
            (0 for an empty graph). When omitted, ``support`` is ``None``
            (not measured).
    """

    from grounded_decision.evidence.reconstruction import (
        lambda_against_label_mix,
        lambda_from_agreements,
    )
    from grounded_decision.types import EvidenceSnapshot

    n = len(labels)
    p_hat = edge_density(n, len(edges))
    degree = mean_degree(n, len(edges))
    if agreement_baseline == "label_mix":
        lam = lambda_against_label_mix(labels, edges)
    else:
        lam = lambda_from_agreements(pairwise_agreements(labels, edges), n_states)
    support: float | None = None
    if supporting_label is not None:
        support = (
            sum(1 for label in labels if label == supporting_label) / n if n else 0.0
        )
    return EvidenceSnapshot(
        n=n,
        p_hat=p_hat,
        p_threshold=p_threshold,
        degree=degree,
        lambda_hat=lam,
        n_states=n_states,
        scores=tuple(scores),
        label_score=label_score,
        observations=dict(observations or {}),
        support=support,
    )