| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106 |
- 'use strict';
- /**
- * 智能问数智能体 · 层间一致性核对工具
- * ----------------------------------------------------------------------------
- * cross_check:按日核对 门店合计 vs 连锁合计 vs 公司行(营业额 + 有效订单)。
- * 从 data_quality 中独立成工具,供模型对指定区间单独抽查(如环比异常时先排除
- * 数据质量问题)。容差与判定同 data_quality:相对差 >0.5% 记不一致、>20% 判严重。
- */
- const { eachDay, ymdDash } = require('./lake.js');
- const CROSS_TOL = 0.005;
- const CROSS_FAIL = 0.2;
- function crossSanitize(args, ctx) {
- const a = args || {};
- const { dateRange } = ctx;
- const from = /^\d{8}$/.test(String(a.from || '')) ? String(a.from) : dateRange.from;
- const to = /^\d{8}$/.test(String(a.to || '')) ? String(a.to) : dateRange.to;
- return {
- from: from < dateRange.from ? dateRange.from : from,
- to: to > dateRange.to ? dateRange.to : to,
- };
- }
- function sumBy(rows, key) {
- const m = new Map();
- for (const r of rows) m.set(r.date, (m.get(r.date) || 0) + (Number(r[key]) || 0));
- return m;
- }
- function relDiff(a, b) {
- return Math.abs(a - b) / Math.max(Math.abs(b), 1);
- }
- function crossHandler(ctx, args) {
- const { svc, entityKey } = ctx;
- const store = svc.store;
- const { from, to } = args;
- const inRange = (r) => r.date >= from && r.date <= to;
- const shopRows = store.queryRows(entityKey, { from, to, scope: 'shop' }).filter(inRange);
- const chainRows = store.queryRows(entityKey, { from, to, scope: 'chain' }).filter(inRange);
- const companyRows = store.queryRows(entityKey, { from, to, scope: 'company' }).filter(inRange);
- if (!shopRows.length && !chainRows.length) {
- return { summary: `区间 ${ymdDash(from)} ~ ${ymdDash(to)} 无可核对的行(门店/连锁层均无数据)。`, data: { from, to, checked: 0 } };
- }
- const metrics = [
- { key: 'grossAmt', label: '营业额' },
- { key: 'validOrderCnt', label: '有效订单' },
- ];
- // 层×指标 各建一张按日求和表(日期字符串不做数值运算)
- const sums = {};
- for (const m of metrics) {
- sums[m.key] = { shop: sumBy(shopRows, m.key), chain: sumBy(chainRows, m.key), company: sumBy(companyRows, m.key) };
- }
- const days = eachDay(from, to).filter((d) => sums.grossAmt.shop.has(d) || sums.grossAmt.chain.has(d));
- const mismatches = [];
- let checked = 0;
- for (const d of days) {
- for (const m of metrics) {
- const shop = sums[m.key].shop.get(d);
- const chain = sums[m.key].chain.get(d);
- const company = sums[m.key].company.get(d);
- // 同层求和口径:门店合计对比连锁合计;连锁合计对比公司行(任一侧缺失跳过该对比)
- const pairs = [];
- if (shop !== undefined && chain !== undefined) pairs.push(['门店合计 vs 连锁', shop, chain]);
- if (chain !== undefined && company !== undefined) pairs.push(['连锁合计 vs 公司', chain, company]);
- for (const [kind, a, b] of pairs) {
- checked += 1;
- const diff = relDiff(a, b);
- if (diff > CROSS_TOL) mismatches.push({ date: d, metric: m.label, kind, a: Math.round(a * 100) / 100, b: Math.round(b * 100) / 100, diffPct: +(diff * 100).toFixed(2) });
- }
- }
- }
- mismatches.sort((x, y) => y.diffPct - x.diffPct);
- const severe = mismatches.some((m) => m.diffPct / 100 > CROSS_FAIL);
- const lines = [];
- if (!mismatches.length) {
- lines.push(`层间一致性通过:抽查 ${days.length} 天 × 2 指标(共 ${checked} 组对比),门店合计/连锁/公司层间一致(容差 ${(CROSS_TOL * 100).toFixed(1)}%)。`);
- } else {
- lines.push(`层间一致性${severe ? '严重不一致' : '存在不一致'}:${mismatches.length} 组对比超容差(共核对 ${checked} 组),最差 ${ymdDash(mismatches[0].date)} ${mismatches[0].kind} ${mismatches[0].metric}:${mismatches[0].a} vs ${mismatches[0].b}(差 ${mismatches[0].diffPct}%)。`);
- for (const m of mismatches.slice(0, 8)) {
- lines.push(` ${ymdDash(m.date)} ${m.metric} ${m.kind}:${m.a} vs ${m.b}(差 ${m.diffPct}%)`);
- }
- if (mismatches.length > 8) lines.push(` …其余 ${mismatches.length - 8} 组略`);
- lines.push('层间不一致通常意味着某一粒度取数异常(如门店行全零、部分门店缺数),相关结论应标注「数据质量存疑」。');
- }
- return {
- summary: lines.join('\n'),
- data: {
- from, to, checked, mismatchCount: mismatches.length, severe, mismatches: mismatches.slice(0, 20),
- verdicts: [{
- kind: 'cross-layer', check: `层间一致性 · 门店/连锁/公司(${ymdDash(from)} ~ ${ymdDash(to)})`,
- status: mismatches.length === 0 ? 'ok' : (severe ? 'fail' : 'warn'),
- detail: mismatches.length === 0
- ? `抽查 ${days.length} 天 × 2 指标共 ${checked} 组对比全部一致`
- : `${mismatches.length} 组超容差,最差 ${ymdDash(mismatches[0].date)} ${mismatches[0].metric} 差 ${mismatches[0].diffPct}%`,
- }],
- },
- };
- }
- module.exports = { crossSanitize, crossHandler, CROSS_TOL, CROSS_FAIL };
|