fix(fundamentals): pure-core review — tie-aware percentile, null-safe reads

1. Peer percentile is now a tie-aware rank against the OTHER issuers
   ((worse + 0.5*tied)/(peers-1)): an all-equal group maps to 50 (not 100), the
   median maps to 50, a unique best to 100, a unique worst to 0.
2. Deterministic reads use the consecutive non-null suffix ending at the latest
   point (>=3 values): a null latest or an internal gap yields no read, so a read
   never reflects a period displayed as n/a.
3. Peer filtering excludes non-finite (NaN/±inf) as well as null, including an
   invalid subject.

Tests updated + added (all-equal, median rank, non-finite, latest-null history,
internal gap). 15 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 20:41:18 +02:00
co-authored by Claude Opus 4.8
parent 2038b84b72
commit 979b4047dc
4 changed files with 73 additions and 22 deletions
+29 -7
View File
@@ -14,11 +14,18 @@ a peer percentile** — leverage is compared via net_debt_to_ebitda.
from __future__ import annotations
import math
import statistics
from dataclasses import dataclass
from typing import Any
MIN_PEERS = 5
def _finite(v: Any) -> bool:
"""True for a finite number — excludes None, bool, NaN, ±inf (plan: null/invalid)."""
return isinstance(v, (int, float)) and not isinstance(v, bool) and math.isfinite(v)
# Metric -> is a higher value more favorable? (Peer-eligible metrics only;
# absolute net_debt is intentionally absent — size-dependent.)
HIGHER_IS_BETTER: dict[str, bool] = {
@@ -50,18 +57,33 @@ def peer_stat(
"""Median + favorable percentile for ``subject`` within its group.
``group_values`` is every issuer's value for the metric (including the
subject), CIK-deduplicated by the caller. Nulls are excluded. Returns None
when fewer than ``min_peers`` valid values exist, or the subject is null.
subject), CIK-deduplicated by the caller. Null/invalid (non-finite) values are
excluded. Returns None when fewer than ``min_peers`` valid values exist, or
the subject is null/invalid.
The percentile is a **tie-aware rank against the other issuers** —
``(worse + 0.5·tied) / (peers 1)`` — so a whole group of equal values maps
to 50, not 100, and the median maps to 50.
"""
valid = [v for v in group_values if v is not None]
if subject is None or len(valid) < min_peers:
valid = [v for v in group_values if _finite(v)]
if not _finite(subject) or len(valid) < min_peers:
return None
median = statistics.median(valid)
others = valid.copy()
try:
others.remove(subject) # rank the subject against the OTHER issuers
except ValueError:
pass
denom = len(others)
if denom == 0:
return None
if higher_is_better:
favorable = sum(1 for v in valid if v <= subject)
worse = sum(1 for v in others if v < subject)
else:
favorable = sum(1 for v in valid if v >= subject)
percentile = round(favorable / len(valid) * 100)
worse = sum(1 for v in others if v > subject)
tied = sum(1 for v in others if v == subject)
percentile = round((worse + 0.5 * tied) / denom * 100)
return PeerStat(median=median, favorable_percentile=percentile, peer_count=len(valid))
+17 -6
View File
@@ -22,13 +22,23 @@ PEER_FAVORABLE = 60
PEER_ADVERSE = 40
def _history_values(history: list[Any]) -> list[float]:
return [p.value for p in history if p.value is not None]
def _latest_run(history: list[Any]) -> list[float]:
"""The consecutive non-null values ending at the latest point (oldest->newest).
A null latest, or an internal gap, truncates the run — so a read never reflects
a period whose displayed value is n/a."""
run: list[float] = []
for p in reversed(history):
if p.value is None:
break
run.append(p.value)
run.reverse()
return run
def growth_read(history: list[Any]) -> str | None:
"""Change in a YoY-growth series: latest prior. Needs >= 3 periods."""
vals = _history_values(history)
"""Change in a YoY-growth series: latest prior. Needs >= 3 consecutive
non-null values ending at the latest point."""
vals = _latest_run(history)
if len(vals) < MIN_PERIODS:
return None
delta = vals[-1] - vals[-2]
@@ -40,8 +50,9 @@ def growth_read(history: list[Any]) -> str | None:
def margin_read(history: list[Any]) -> str | None:
"""Latest margin vs the mean of prior periods (pp). Needs >= 3 periods."""
vals = _history_values(history)
"""Latest margin vs the mean of prior periods (pp). Needs >= 3 consecutive
non-null values ending at the latest point."""
vals = _latest_run(history)
if len(vals) < MIN_PERIODS:
return None
delta = vals[-1] - mean(vals[:-1])