"""场景二 · 市场业务真实性:养卡骗补检测(R9)。 检测"脉冲式增长 + 规律性衰减"的周期性造假:渠道每月新增大量用户订购, 固定周期后这些用户集中退订(骗补后弃养)。结合佣金与业务质量匹配度。 """ from __future__ import annotations from dataclasses import dataclass from app.scenarios.base import BaseScenario, ClueDraft, ScanOutcome from app.scenarios.registry import register_scenario @dataclass class CohortPoint: """某新增批次(cohort)在第 N 个月的留存率。""" month_index: int retention: float # 0-1 @dataclass class ChurnFinding: cliff_month: int | None max_drop: float pulse_then_decay: bool def detect_pulse_decay( retention_curve: list[CohortPoint], cliff_drop: float = 0.5, ) -> ChurnFinding: """识别留存曲线中的"断崖式集中退订"。 若某月留存相对上月骤降超过 cliff_drop(默认 50%),判为规律性衰减。 """ ordered = sorted(retention_curve, key=lambda p: p.month_index) max_drop = 0.0 cliff_month: int | None = None for prev, cur in zip(ordered, ordered[1:], strict=False): drop = prev.retention - cur.retention if drop > max_drop: max_drop = drop if drop >= cliff_drop: cliff_month = cur.month_index return ChurnFinding( cliff_month=cliff_month, max_drop=round(max_drop, 3), pulse_then_decay=cliff_month is not None, ) def commission_quality_mismatch( commission_paid: float, active_ratio: float, zero_usage_ratio: float, ) -> float: """佣金与业务质量不匹配度(0-1)。 active_ratio:仍活跃用户占比;zero_usage_ratio:零通话/零流量用户占比。 佣金已发但活跃低、零使用高 → 不匹配度高。 """ if commission_paid <= 0: return 0.0 mismatch = 0.6 * zero_usage_ratio + 0.4 * (1 - active_ratio) return round(min(max(mismatch, 0.0), 1.0), 3) def churn_risk_score(finding: ChurnFinding, mismatch: float) -> float: """综合评分:断崖退订 + 佣金质量不匹配。""" if not finding.pulse_then_decay: return round(0.3 * mismatch, 3) base = 0.4 + 0.4 * finding.max_drop + 0.2 * mismatch return round(min(base, 1.0), 3) def build_rationale(finding: ChurnFinding, mismatch: float) -> str: if finding.pulse_then_decay: return ( f"渠道新增用户在第 {finding.cliff_month} 个月出现断崖式集中退订" f"(最大单月留存骤降 {finding.max_drop:.0%}),呈" f"'脉冲式增长 + 规律性衰减'特征;佣金与业务质量不匹配度 {mismatch:.0%}," f"高度疑似养卡骗补(骗补后弃养)。" ) return ( f"未见明显断崖退订,但佣金与业务质量不匹配度为 {mismatch:.0%},建议关注。" ) @register_scenario class ChurnFraudScenario(BaseScenario): """场景二养卡骗补检测的插件封装:时序断崖 + 佣金质量不匹配 → 线索草稿。""" code = "R9" title = "疑似养卡骗补(脉冲增长+规律退订)" risk_domain = "成本" label = "养卡骗补" score_threshold = 0.5 # 与原 run_churn_scan 的 score>=0.5 一致 def __init__( self, retention_curve: list[CohortPoint], commission_paid: float, active_ratio: float, zero_usage_ratio: float, channel_key: str, ) -> None: self.retention_curve = retention_curve self.commission_paid = commission_paid self.active_ratio = active_ratio self.zero_usage_ratio = zero_usage_ratio self.channel_key = channel_key def scan(self, session, *, data_version_id=None) -> ScanOutcome: finding = detect_pulse_decay(self.retention_curve) mismatch = commission_quality_mismatch( self.commission_paid, self.active_ratio, self.zero_usage_ratio ) score = churn_risk_score(finding, mismatch) draft = ClueDraft( score=score, rationale=build_rationale(finding, mismatch), evidence={ "cliff_month": finding.cliff_month, "max_drop": finding.max_drop, "commission_paid": self.commission_paid, "active_ratio": self.active_ratio, "zero_usage_ratio": self.zero_usage_ratio, "mismatch": mismatch, }, subjects={"channel": self.channel_key}, amount_involved=self.commission_paid, ) return ScanOutcome(scanned_count=len(self.retention_curve), drafts=[draft])