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:
@@ -2,24 +2,30 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
from app.services import fundamentals_peers as pr
|
||||
|
||||
|
||||
def test_peer_stat_higher_is_better_percentile_and_median():
|
||||
def test_median_ranks_at_50_tie_aware():
|
||||
s = pr.peer_stat(3, [1, 2, 3, 4, 5], higher_is_better=True)
|
||||
assert s.median == 3
|
||||
assert s.favorable_percentile == 60 # beats/ties 3 of 5
|
||||
assert s.favorable_percentile == 50 # tie-aware rank of the median
|
||||
assert s.peer_count == 5
|
||||
|
||||
|
||||
def test_all_equal_peers_rank_at_50():
|
||||
s = pr.peer_stat(3, [3, 3, 3, 3, 3], higher_is_better=True)
|
||||
assert s.favorable_percentile == 50 # not 100 — ties don't get full credit
|
||||
|
||||
|
||||
def test_peer_stat_lower_is_better_flips_direction():
|
||||
s = pr.peer_stat(3, [1, 2, 3, 4, 5], higher_is_better=False)
|
||||
assert s.favorable_percentile == 60 # 3 of 5 are >= 3
|
||||
assert pr.peer_stat(3, [1, 2, 3, 4, 5], higher_is_better=False).favorable_percentile == 50
|
||||
|
||||
|
||||
def test_peer_stat_top_and_bottom():
|
||||
def test_peer_stat_unique_top_and_bottom():
|
||||
assert pr.peer_stat(5, [1, 2, 3, 4, 5], higher_is_better=True).favorable_percentile == 100
|
||||
assert pr.peer_stat(1, [1, 2, 3, 4, 5], higher_is_better=True).favorable_percentile == 20
|
||||
assert pr.peer_stat(1, [1, 2, 3, 4, 5], higher_is_better=True).favorable_percentile == 0
|
||||
|
||||
|
||||
def test_peer_stat_requires_min_valid_peers():
|
||||
@@ -27,9 +33,11 @@ def test_peer_stat_requires_min_valid_peers():
|
||||
assert pr.peer_stat(None, [1, 2, 3, 4, 5], higher_is_better=True) is None # null subject
|
||||
|
||||
|
||||
def test_peer_stat_excludes_nulls_from_group():
|
||||
s = pr.peer_stat(3, [1, 2, 3, 4, 5, None, None], higher_is_better=True)
|
||||
assert s.peer_count == 5 # nulls dropped
|
||||
def test_peer_stat_excludes_null_and_non_finite():
|
||||
s = pr.peer_stat(3, [1, 2, 3, 4, 5, None, math.nan, math.inf, -math.inf], higher_is_better=True)
|
||||
assert s.peer_count == 5 # nulls + NaN/inf dropped
|
||||
# a non-finite subject is invalid
|
||||
assert pr.peer_stat(math.nan, [1, 2, 3, 4, 5], higher_is_better=True) is None
|
||||
|
||||
|
||||
def test_net_debt_is_not_peer_eligible():
|
||||
|
||||
@@ -18,6 +18,16 @@ def test_growth_read_boundaries():
|
||||
assert rd.growth_read(_hist(5, 6)) is None # < 3 periods
|
||||
|
||||
|
||||
def test_reads_use_latest_nonnull_suffix():
|
||||
# latest displayed value is n/a -> no read (never reflect a null latest)
|
||||
assert rd.growth_read(_hist(5, 6, 8, None)) is None
|
||||
assert rd.margin_read(_hist(19, 20, 22, None)) is None
|
||||
# an internal gap truncates the run -> fewer than 3 consecutive -> no read
|
||||
assert rd.growth_read(_hist(5, 6, None, 8)) is None
|
||||
# a clean 3-run after an older gap still reads
|
||||
assert rd.growth_read(_hist(None, 5, 6, 8)) == "accelerating"
|
||||
|
||||
|
||||
def test_margin_read_vs_mean_of_prior():
|
||||
# prior mean = (19+20)/2 = 19.5; latest 21 -> +1.5 -> improving
|
||||
assert rd.margin_read(_hist(19, 20, 21)) == "improving"
|
||||
|
||||
Reference in New Issue
Block a user