181d536434
Co-Authored-By: Claude Code <noreply@anthropic.com>
99 lines
3.9 KiB
Python
99 lines
3.9 KiB
Python
# sanguo_factor/decay.py
|
||
"""衰减判定引擎——流水线 spec §4.2 决议 E/F/C.
|
||
|
||
三条件(起步默认+首年校准,数值措辞按 spec):
|
||
① 相对降幅: 滚动 12M IC t 值 < 晋级时 t 值 × RELATIVE_DROP
|
||
(各因子晋级基准不同,一刀切绝对值不公平);
|
||
② 绝对地板: 同时 t < ABSOLUTE_FLOOR(防「从很强掉到还算强」也叫衰减);
|
||
③ 连续性: 连续 CONSECUTIVE_MONTHS 个月满足①②才开告警(state=alert);
|
||
第 1~2 个月标黄预警(state=yellow)——月度 IC 一年 12 个点,单月噪声大.
|
||
成熟度门(决议 E): promotion_t 为 None(未晋级)→不判定(state=not_promoted).
|
||
集体水位(决议 C): 全体已晋级因子当月 t 中位数 < COLLECTIVE_MEDIAN_FLOOR
|
||
→ is_collective_decay=True——消费方(月度批评)据此出研判卡数据,
|
||
不开降权告警,因子权重冻结不动.阈值只负责叫人,处置永远人选.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass
|
||
from statistics import median
|
||
|
||
RELATIVE_DROP = 0.5 # 起步默认,首年校准(决议 F)
|
||
ABSOLUTE_FLOOR = 2.0
|
||
CONSECUTIVE_MONTHS = 3 # 第 3 个连续月开告警
|
||
COLLECTIVE_MEDIAN_FLOOR = 1.0 # 集体水位地板,起步默认首年校准(决议 C)
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class DecayVerdict:
|
||
factor: str
|
||
state: str # ok|yellow|alert|not_promoted|no_data
|
||
months_consecutive: int # 尾部连续满足①②的月数(含当月)
|
||
current_t: float | None
|
||
promotion_t: float | None
|
||
ratio: float | None # current_t/promotion_t
|
||
|
||
|
||
def _next_month(month: str) -> str:
|
||
"""'2026-07' → '2026-08'(连续性判定用)."""
|
||
y, m = int(month[:4]), int(month[5:7])
|
||
y, m = (y + 1, 1) if m == 12 else (y, m + 1)
|
||
return f"{y:04d}-{m:02d}"
|
||
|
||
|
||
def _prev_month(month: str) -> str:
|
||
"""'2026-08' → '2026-07'."""
|
||
y, m = int(month[:4]), int(month[5:7])
|
||
y, m = (y - 1, 12) if m == 1 else (y, m - 1)
|
||
return f"{y:04d}-{m:02d}"
|
||
|
||
|
||
def _last_t(points: list[dict]) -> float | None:
|
||
for p in reversed(points):
|
||
if p.get("t") is not None:
|
||
return float(p["t"])
|
||
return None
|
||
|
||
|
||
def judge_factor(monthly_points: list[dict], promotion_t: float | None,
|
||
factor: str = "") -> DecayVerdict:
|
||
"""monthly_points: [{"month": "2026-05", "t": 3.1}, ...] 按月升序,t 可为 None(缺).
|
||
|
||
从序列尾部往回数「日历月连续且满足①②」的月数;断月/不满足即停.
|
||
"""
|
||
if promotion_t is None:
|
||
return DecayVerdict(factor, "not_promoted", 0,
|
||
_last_t(monthly_points), None, None)
|
||
points = [p for p in monthly_points if p.get("t") is not None]
|
||
if not points:
|
||
return DecayVerdict(factor, "no_data", 0, None, promotion_t, None)
|
||
current_t = float(points[-1]["t"])
|
||
ratio = current_t / promotion_t if promotion_t else None
|
||
streak = 0
|
||
for i in range(len(points) - 1, -1, -1):
|
||
p = points[i]
|
||
weak = p["t"] < promotion_t * RELATIVE_DROP and p["t"] < ABSOLUTE_FLOOR
|
||
if not weak:
|
||
break
|
||
if streak > 0:
|
||
later = points[i + 1] # 已计入 streak 的上一个(更晚)点
|
||
if later["month"] != _next_month(p["month"]):
|
||
break # 断月:序列不日历连续
|
||
streak += 1
|
||
state = "ok"
|
||
if streak >= CONSECUTIVE_MONTHS:
|
||
state = "alert"
|
||
elif streak > 0:
|
||
state = "yellow"
|
||
return DecayVerdict(factor, state, streak, current_t, promotion_t, ratio)
|
||
|
||
|
||
def collective_level(current_ts: list[float]) -> dict:
|
||
"""集体水位:当月全体已晋级因子 t 的中位数(决议 C).空表不报警."""
|
||
if not current_ts:
|
||
return {"median": None, "floor": COLLECTIVE_MEDIAN_FLOOR,
|
||
"is_collective_decay": False, "n": 0}
|
||
med = median(current_ts)
|
||
return {"median": med, "floor": COLLECTIVE_MEDIAN_FLOOR,
|
||
"is_collective_decay": med < COLLECTIVE_MEDIAN_FLOOR,
|
||
"n": len(current_ts)}
|