Files
yunke/yunke-server/app/analytics/lifecycle.py
T
浅念 9dc22cd1e6 feat(management): 生命周期判定纯函数——距今天数口径/7天新客/30天沉睡/历史门槛5条
TDD(批次A Task4):classify/day_add/days_between 纯函数,供 Task6 宽表
生命周期 4 列计算。口径已确认:新客=首次互动≤7 天(含当天共 7 天)、
cooling [7,30)、沉睡≥30 天且历史≥5 条(对齐 churn real min_msgs=5)、
回流=当日互动(last_day==as_of)且前次间隔>30 天(严格大于,恰好 30
天不算);未激活或历史不足的长期静默返回 None。analytics 现有页暂不
切换(v2 §5 口径差异另标),不动 636 既有测试。

模块为纯叶子节点,禁止 import app.storage(防 storage→analytics 成环)。
7 例测试覆盖:new 窗口边界(as_of-6 含/不含)、回流 35/9/31 天与恰好
30 天、last_day==as_of 门槛、6/7/29/30 天落位、历史门槛 4/5、
day_add 跨月。全量 643 绿。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-09 13:37:22 +08:00

72 lines
2.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""客户生命周期判定(管理域,批次A)——距今天数口径(v2 §1.1 #3 决议)。
阈值取已确认默认(v1 §21,用户拍板):新客 7 天、沉睡 30 天;历史互动
门槛 MIN_HISTORY_MSGS=5 对齐 analytics churn real 的 min_msgs=5。
analytics 现有页(churn/客户资产)**暂不切换**到此模块——两套数字的
差异在 metric-definitions 标注(v2 §5);后续统一时再接,避免动既有
测试。
本模块为纯叶子节点,禁止 import app.storage(防止 storage→analytics
消费时成环)。
"""
from __future__ import annotations
import datetime as dt
NEW_DAYS = 7
DORMANT_DAYS = 30
MIN_HISTORY_MSGS = 5
STATES = ("new", "active", "cooling", "dormant", "reactivated")
def _to_date(day: int) -> dt.date:
return dt.date(day // 10000, (day // 100) % 100, day % 100)
def day_add(day: int, delta_days: int) -> int:
"""yyyymmdd 键加减天数。"""
d = _to_date(day) + dt.timedelta(days=delta_days)
return d.year * 10000 + d.month * 100 + d.day
def days_between(day_a: int, day_b: int) -> int:
"""b − a 的天数(输入 yyyymmdd 键)。"""
return (_to_date(day_b) - _to_date(day_a)).days
def classify(
first_day: int | None,
last_day: int | None,
prev_day: int | None,
as_of: int,
hist_msgs: int,
) -> str | None:
"""as_of 日的客户状态;输入均为互动日键(yyyymmdd,上海自然日)。
- first_day: 该客户全历史首次互动日;last_day: ≤as_of 的最近互动日;
prev_day: last_day 之前的一次互动日(无则 None);hist_msgs: 历史总消息数。
- 返回 None = 未激活(first/last 缺)或历史不足的长期静默(不计入任何列)。
判定优先级:new → reactivated → active → cooling → dormant。
"""
if first_day is None or last_day is None:
return None
if first_day >= day_add(as_of, -(NEW_DAYS - 1)):
return "new"
if (
last_day == as_of
and prev_day is not None
and days_between(prev_day, last_day) > DORMANT_DAYS
):
return "reactivated"
gap = days_between(last_day, as_of)
if gap < NEW_DAYS:
return "active"
if gap < DORMANT_DAYS:
return "cooling"
if hist_msgs >= MIN_HISTORY_MSGS:
return "dormant"
return None