Source code for gasm.metrics

"""Matching quality and accuracy metrics (Section 2 of the reference article)."""

from __future__ import annotations

import networkx as nx
import numpy as np
import scipy.sparse as sp


def _binary_matching_matrix(matchups, nodes_a, nodes_b):
    """Build the binary matching matrix ``M`` (eq. 1)."""
    ia = {label: i for i, label in enumerate(nodes_a)}
    ib = {label: i for i, label in enumerate(nodes_b)}
    M = sp.lil_matrix((len(nodes_a), len(nodes_b)), dtype=np.float64)
    for a, b in matchups:
        if a in ia and b in ib:
            M[ia[a], ib[b]] = 1.0
    return M.tocsr()


[docs] def structural_quality(G1, G2, matching) -> float: """Structural quality ``qS`` of a matching (eq. 3). Parameters ---------- G1, G2: The two networkx graphs that were matched. matching: A :class:`gasm.matching.Matching`, or any iterable of ``(a, b)`` pairs. Returns ------- float ``qS`` in ``[0, 1]``; higher is a better structural match. Returns ``0`` when both graphs have no edge. """ matchups = getattr(matching, "matchups", matching) nodes_a = list(G1.nodes()) nodes_b = list(G2.nodes()) LA = nx.to_scipy_sparse_array(G1, nodelist=nodes_a, format="csr", dtype=np.float64) LB = nx.to_scipy_sparse_array(G2, nodelist=nodes_b, format="csr", dtype=np.float64) M = _binary_matching_matrix(matchups, nodes_a, nodes_b) Z = LA @ M - M @ LB discrepancies = float((Z.multiply(Z)).sum()) mA = G1.number_of_edges() mB = G2.number_of_edges() if mA == 0 and mB == 0: return 0.0 if G1.is_directed() or G2.is_directed(): denom = mA + mB else: muA = sum(1 for u, v in G1.edges() if u == v) muB = sum(1 for u, v in G2.edges() if u == v) denom = 2 * (mA + mB) - muA - muB if denom == 0: return 0.0 return 1.0 - discrepancies / denom
[docs] def accuracy(matching, ground_truth) -> float: """Accuracy ``gamma`` of a matching against a ground truth (Section 2.2). Parameters ---------- matching: A :class:`gasm.matching.Matching`, or any iterable of ``(a, b)`` pairs. ground_truth: Mapping ``{a: b}`` of the true correspondence, or an iterable of ``(a, b)`` pairs. Returns ------- float Proportion of matched pairs that agree with the ground truth. """ matchups = getattr(matching, "matchups", matching) if not isinstance(ground_truth, dict): ground_truth = dict(ground_truth) if not matchups: return 0.0 correct = sum(1 for a, b in matchups if ground_truth.get(a) == b) return correct / len(matchups)