Source code for andrey.metrics.pairwise

"""Pairwise-direction metrics: cause-effect direction accuracy for two-variable methods (ANM, PNL).

Scored over a set of ordered pairs, each with a predicted and a true direction. Accuracy alone is
gameable by abstaining on the hard pairs, so ``decision_rate`` is reported alongside it.
"""

from __future__ import annotations

import numpy as np


def _signs(labels: object) -> np.ndarray:
    """Per-pair direction as ``{-1, 0, +1}`` (``+1`` = ``X -> Y``, ``0`` = abstain)."""
    return np.sign(np.asarray(labels, dtype=np.float64))


[docs] def direction_accuracy(pred: object, truth: object, *, weights: object | None = None) -> float: """Fraction of decided pairs whose predicted cause -> effect direction matches the truth. ``pred`` and ``truth`` are per-pair signed labels (``+1`` = ``X -> Y``, ``-1`` = ``Y -> X``); a ``pred`` of 0 is an abstention, dropped before scoring. ``weights`` gives optional per-pair weights (the Tuebingen-pairs standard). Returns 0.0 when nothing is decided; pair it with :func:`decision_rate` so a high accuracy from few decisions is visible. Examples -------- >>> direction_accuracy([1, 1, -1], [1, -1, -1]) 0.6666666666666666 """ p, t = _signs(pred), _signs(truth) if p.shape != t.shape: raise ValueError(f"pred/truth length mismatch: {p.shape} vs {t.shape}") w = np.ones_like(p) if weights is None else np.asarray(weights, dtype=np.float64) decided = p != 0 if not decided.any() or w[decided].sum() == 0: return 0.0 correct = (p == t) & decided return float(w[correct].sum() / w[decided].sum())
[docs] def decision_rate(pred: object, *, weights: object | None = None) -> float: """Fraction of pairs the method commits to (``pred != 0``); 1.0 when it decides every pair. Examples -------- >>> decision_rate([1, 0, -1, 0]) 0.5 """ p = _signs(pred) w = np.ones_like(p) if weights is None else np.asarray(weights, dtype=np.float64) total = w.sum() return float(w[p != 0].sum() / total) if total else 0.0