fix(analytics): 环比同期对齐口径+趋势窗校验+异常新员工明细
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import calendar
|
||||
import datetime as dt
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
@@ -53,13 +54,26 @@ def scope_day_keys(scope: str, today: int) -> list[int]:
|
||||
|
||||
|
||||
def _prev_scope_days(scope: str, days: list[int]) -> list[int]:
|
||||
"""紧邻的前一个等长窗口(环比;今日 scope 不调用)。"""
|
||||
n = len(days)
|
||||
last = _day_date(days[0]) - dt.timedelta(days=1)
|
||||
return [
|
||||
_date_key(last - dt.timedelta(days=i))
|
||||
for i in range(n - 1, -1, -1)
|
||||
]
|
||||
"""同期对齐环比窗(与 days 等长逐日对齐;今日 scope 不调用)。
|
||||
|
||||
week=各日-7天(本周一..今 vs 上周一..上周同日);month=上月同日
|
||||
(短月钳到上月末,如 3/31→2/28)。同期对齐而非滚动紧邻窗——后者
|
||||
周会跨周末、月取上月尾巴,管理者会误读成「上周/上月」(spec §8)。
|
||||
"""
|
||||
if scope == "week":
|
||||
return [_date_key(_day_date(d) - dt.timedelta(days=7)) for d in days]
|
||||
if scope == "month":
|
||||
out = []
|
||||
for d in days:
|
||||
cur = _day_date(d)
|
||||
y, m = cur.year, cur.month - 1
|
||||
if m == 0:
|
||||
y, m = y - 1, 12
|
||||
out.append(
|
||||
_date_key(dt.date(y, m, min(cur.day, calendar.monthrange(y, m)[1])))
|
||||
)
|
||||
return out
|
||||
raise ValueError(f"bad scope: {scope!r}")
|
||||
|
||||
|
||||
async def _day_rows(
|
||||
@@ -80,12 +94,15 @@ async def _day_rows(
|
||||
).all()
|
||||
|
||||
|
||||
async def _call_count(session: AsyncSession, channel_scope_days: list[int]) -> int:
|
||||
"""phone_call 落窗计数(3k 行小表;start_call_time ms 与 day 键边界换算)。"""
|
||||
if not channel_scope_days:
|
||||
async def _call_count(session: AsyncSession, days: list[int]) -> int:
|
||||
"""phone_call 落窗计数(3k 行小表;start_call_time ms 与 day 键边界换算)。
|
||||
|
||||
通话不按 messaging channel 过滤(spec §8:含全部 call-type)。
|
||||
"""
|
||||
if not days:
|
||||
return 0
|
||||
lo, _hi = day_bounds_ms(channel_scope_days[0])
|
||||
hi_day_start, _ = day_bounds_ms(channel_scope_days[-1])
|
||||
lo, _hi = day_bounds_ms(days[0])
|
||||
hi_day_start, _ = day_bounds_ms(days[-1])
|
||||
hi = hi_day_start + 86_400_000 # 含末日全天
|
||||
val = (
|
||||
await session.execute(
|
||||
@@ -207,6 +224,14 @@ async def anomaly(session: AsyncSession, channel: str) -> dict | None:
|
||||
"yesterday": y,
|
||||
"prev7_mean": round(pmean, 1),
|
||||
})
|
||||
elif pmean == 0 and y > 0:
|
||||
# 新员工:前 7 日无量、昨日有量——昨日新增的大异常源,不能从明细消失
|
||||
staff_out.append({
|
||||
"user_id": uid,
|
||||
"yesterday": y,
|
||||
"prev7_mean": 0.0,
|
||||
"new": True,
|
||||
})
|
||||
if totals_p <= 0:
|
||||
return None
|
||||
delta = (totals_y - totals_p) / totals_p
|
||||
@@ -215,7 +240,7 @@ async def anomaly(session: AsyncSession, channel: str) -> dict | None:
|
||||
"total_delta_pct": round(delta, 3),
|
||||
"yesterday_total": totals_y,
|
||||
"prev7_mean": round(totals_p, 1),
|
||||
"staff": sorted(staff_out, key=lambda s: s["yesterday"]),
|
||||
"staff": sorted(staff_out, key=lambda s: -s["yesterday"]),
|
||||
}
|
||||
return None
|
||||
|
||||
@@ -223,7 +248,9 @@ async def anomaly(session: AsyncSession, channel: str) -> dict | None:
|
||||
async def overview(
|
||||
session: AsyncSession, *, channel: str, scope: str, trend_days: int = 30
|
||||
) -> dict[str, Any]:
|
||||
"""A1:卡片 + 环比(仅周/月) + 近 N 天趋势 + 异常警示。"""
|
||||
"""A1:卡片 + 环比(仅周/月,同期对齐) + 近 N 天趋势 + 异常警示。"""
|
||||
if trend_days < 1 or trend_days > 90:
|
||||
raise ValueError("trend_days must be 1..90")
|
||||
today = today_key()
|
||||
days = scope_day_keys(scope, today)
|
||||
rows = await _day_rows(session, channel, days)
|
||||
@@ -238,7 +265,7 @@ async def overview(
|
||||
"first_contact": await _first_contact(session, channel, days),
|
||||
}
|
||||
|
||||
# 环比仅周/月(今日 vs 昨日全天会误导,spec §8)
|
||||
# 环比仅周/月,同期对齐(今日 vs 昨日全天会误导,spec §8)
|
||||
if scope in ("week", "month"):
|
||||
prev_days = _prev_scope_days(scope, days)
|
||||
prev_rows = await _day_rows(session, channel, prev_days)
|
||||
|
||||
Reference in New Issue
Block a user