Source code for knowledgespaces.metrics.agreement

"""
Agreement measures between knowledge structures or relations.

Provides Cohen's kappa and related indices for comparing
prerequisite matrices or state memberships.
"""

from __future__ import annotations

from knowledgespaces.structures.relations import SurmiseRelation


[docs] def cohens_kappa(rel1: SurmiseRelation, rel2: SurmiseRelation) -> float: """Cohen's kappa for agreement on prerequisite relations. Compares two surmise relations on the same domain. For every ordered pair (a, b) with a ≠ b, counts agreement/disagreement on whether (a, b) is a prerequisite. Parameters ---------- rel1, rel2 : SurmiseRelation Must have the same item domain. Returns ------- float Cohen's kappa in [-1, 1]. 1 = perfect agreement, 0 = chance agreement, <0 = worse than chance. Raises ------ ValueError If the relations have different domains. """ if rel1.items != rel2.items: raise ValueError("Relations must have the same domain for kappa.") # Normalize to transitive closure so that equivalent partial orders # (e.g. a→b→c vs a→b→c + a→c) produce the same result. rel1 = rel1.transitive_closure() rel2 = rel2.transitive_closure() items = sorted(rel1.items) n = len(items) if n < 2: return 1.0 # trivial case # Count agreement on all ordered pairs (a, b), a ≠ b both_yes = 0 both_no = 0 r1_yes_r2_no = 0 r1_no_r2_yes = 0 for a in items: for b in items: if a == b: continue in1 = (a, b) in rel1.relations in2 = (a, b) in rel2.relations if in1 and in2: both_yes += 1 elif not in1 and not in2: both_no += 1 elif in1: r1_yes_r2_no += 1 else: r1_no_r2_yes += 1 total = both_yes + both_no + r1_yes_r2_no + r1_no_r2_yes if total == 0: return 1.0 p_observed = (both_yes + both_no) / total p1_yes = (both_yes + r1_yes_r2_no) / total p2_yes = (both_yes + r1_no_r2_yes) / total p_expected = p1_yes * p2_yes + (1 - p1_yes) * (1 - p2_yes) if p_expected == 1.0: return 1.0 # both raters always agree return (p_observed - p_expected) / (1 - p_expected)