"""Repeated-run finding stability metrics.""" from __future__ import annotations from collections.abc import Iterable, Sequence from typing import Any from .metrics import distribution_summary def jaccard(left: set[str], right: set[str]) -> float: union = left | right return len(left & right) / len(union) if union else 1.0 def stability_summary(runs: Sequence[Iterable[str]]) -> dict[str, Any]: sets = [set(run) for run in runs] if not sets: return { "run_count": 0, "exact": False, "intersection_count": 0, "union_count": 0, "churn_count": 0, "per_run_counts": [], "pairwise_jaccard": distribution_summary([]), } intersection = set.intersection(*sets) union = set.union(*sets) pairwise = [ jaccard(sets[left], sets[right]) for left in range(len(sets)) for right in range(left + 1, len(sets)) ] if len(sets) == 1: pairwise = [1.0] return { "run_count": len(sets), "exact": all(current == sets[0] for current in sets[1:]), "intersection_count": len(intersection), "union_count": len(union), "churn_count": len(union - intersection), "per_run_counts": [len(current) for current in sets], "pairwise_jaccard": distribution_summary(pairwise), }