refactor(policy): extract pure scoring and ranking functions
This commit is contained in:
parent
de4cae1591
commit
5719faa05b
1 changed files with 53 additions and 0 deletions
53
policy/scoring.py
Normal file
53
policy/scoring.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
"""Pure score arithmetic. No model types, game rules, I/O, or policy thresholds.
|
||||
|
||||
Invalid or incomplete inputs raise ValueError. Callers own evidence checks and
|
||||
fallbacks; missing evidence must never become a neutral score.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from collections.abc import Mapping
|
||||
|
||||
|
||||
def _finite(value: float) -> bool:
|
||||
return isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value)
|
||||
|
||||
|
||||
def normalize_score(value: float, minimum: float, neutral: float, maximum: float) -> float:
|
||||
"""Map an ordered scale to [-1, 1], with neutral at zero.
|
||||
|
||||
Each side is linear. Equal score intervals are a modeling assumption, not
|
||||
a calibrated measure of game value. Values outside the scale are rejected.
|
||||
"""
|
||||
if not all(_finite(v) for v in (value, minimum, neutral, maximum)):
|
||||
raise ValueError("score and scale must be finite numbers")
|
||||
if not minimum < neutral < maximum:
|
||||
raise ValueError("scale must satisfy minimum < neutral < maximum")
|
||||
if not minimum <= value <= maximum:
|
||||
raise ValueError("score is outside the scale")
|
||||
span = maximum - neutral if value >= neutral else neutral - minimum
|
||||
return (value - neutral) / span
|
||||
|
||||
|
||||
def weighted_utility(components: Mapping[str, float], weights: Mapping[str, float]) -> float:
|
||||
"""Combine complete normalized components using explicit nonnegative weights.
|
||||
|
||||
Weights must sum to one. Do not silently remove missing components or
|
||||
renormalize weights. Confidence is not a component or a utility multiplier.
|
||||
"""
|
||||
if not weights or components.keys() != weights.keys():
|
||||
raise ValueError("components must exactly match nonempty weights")
|
||||
if any(not _finite(w) or w < 0 for w in weights.values()):
|
||||
raise ValueError("weights must be finite and nonnegative")
|
||||
if not math.isclose(sum(weights.values()), 1.0, rel_tol=0, abs_tol=1e-9):
|
||||
raise ValueError("weights must sum to one")
|
||||
if any(not _finite(v) or not -1 <= v <= 1 for v in components.values()):
|
||||
raise ValueError("components must be finite normalized scores")
|
||||
return sum(weights[axis] * components[axis] for axis in weights)
|
||||
|
||||
|
||||
def rank_candidates(utilities: Mapping[str, float]) -> list[str]:
|
||||
"""Rank highest utility first; ties retain input order. Empty input is valid."""
|
||||
if any(not _finite(value) for value in utilities.values()):
|
||||
raise ValueError("candidate utilities must be finite numbers")
|
||||
return sorted(utilities, key=utilities.__getitem__, reverse=True)
|
||||
Loading…
Add table
Add a link
Reference in a new issue