跳转至

归因分析

每个看板都会引出同一个问题:这个指标为什么变了? 人工回答——或让 AI Agent 回答——通常意味着一轮探索式查询循环:先查两个周期的总量,再一个维度 一个维度地分组查询,然后自己对齐、比较数字,还要小心两个周期之间新出现或 消失的分段。

归因分析把这个循环压缩成一次调用。给 Dosi 一个指标、候选维度和两个日期 窗口,引擎自己跑完所有查询,返回排好序、可直接引用的分解结果:

  • 哪个维度最能解释这次变化;
  • 哪个分段驱动了变化、贡献了总变化的百分之多少;
  • 对比率类指标,变化来自结构还是率值——比如"客单价上升,是因为已完成 订单自身客单价从 75 → 100,尽管它的订单占比在下降"。

数字由引擎按每种指标类型选用精确的方法算出,并且加得起来——每次分解都与 总量对账,结论可核对,而不是即兴拼凑。

所有接口面都可用:CLI(dosi attribute)、REST (POST /v1/query/attribute)、MCP(attribute_metric 工具)和 Python (Engine.attribute)。

快速开始

dosi --model model.yaml attribute \
  --metric revenue \
  --dimensions status,customers.region,products.category \
  --baseline 2024-01-01..2024-02-01 \
  --current  2024-02-01..2024-03-01 \
  --db warehouse.duckdb

窗口是半开 ISO 区间(START..END)。--connection 指定命名数仓连接, --where 为整个分析加过滤,默认时间路由不合适时用 --time-dimension

REST 或 Python 的请求是同一个形状:

from dosi_engine import Engine

engine = Engine("model.yaml")
result = engine.attribute({
    "metric": "revenue",
    "dimensions": ["status", "customers.region"],
    "baseline": {"start": "2024-01-01", "end": "2024-02-01"},
    "current":  {"start": "2024-02-01", "end": "2024-03-01"},
}, db_path="warehouse.duckdb")

返回什么

{
  "metric": "avg_order_value",
  "strategy": "mix_shift",                  // 引擎采用的分解方式
  "total_change": {"baseline_value": 75.0, "current_value": 76.67,
                   "delta": 1.67, "pct_change": 2.22},
  "dimension_ranking": [                    // 排好序的根因候选
    {"dimension": "status", "score": 6.0}
  ],
  "top_dimension_values": [                 // 变动最大的分段
    {"dimension": "status", "value": "cancelled",
     "delta": 10.0, "contribution_pct": 600.0,
     "segment_kind": "entered",             // 本周期新出现的分段
     "drill_down": {"where_sql": "status = 'cancelled'"}},
    {"dimension": "status", "value": "completed",
     "delta": -8.33, "contribution_pct": -500.0,
     "mix_effect": -28.69, "rate_effect": 20.35,
     "baseline_rate": 75.0, "current_rate": 100.0,
     "drill_down": {"where_sql": "status = 'completed'"}}
  ],
  "factor_totals": {"mix_effect": -18.7, "rate_effect": 20.4, ...},
  "warnings": []                            // 结构化的注意事项,见下文
}

怎么读:

  • dimension_ranking 按解释力排序候选维度——第一名就是"该按这个维度 切"。
  • contribution_pct 可以直接引用:"该分段解释了 X% 的变化"。超过 100% (或为负)说明分段朝相反方向变动、相互抵消——响应会明确标记这种情况。
  • segment_kind 指出分段是本周期新出现entered)还是消失exited),而不是普通涨跌——这正是手工对比最容易悄悄出错的地方。
  • 比率类指标,每个分段的贡献拆成结构效应(它在总体中的权重变了) 和率值效应(它自身的比率变了),并附带各分段的率值与占比——"结构 vs 表现"的叙事直接从响应里读出来,且各部分永远加总等于总变化。
  • per_dimension(示例中省略)携带每个维度的完整分段明细,包括该维度 的数字是否与总量对得上账。

如何帮助 Agent 推理

响应的设计目标,是让 Agent 用一两次调用就从"指标动了"走到可验证的根因, 全程不用自己做算术:

  1. 不再探查循环。 排序和贡献值都是算好、排好的,Agent 直接引用, 不用自己编排一串查询再对账。
  2. 每个发现自带下一步。 每个分段都带 drill_down.where_sql——可直接 粘贴的过滤条件。想继续下钻,就带上它再调一次 attribute(根因递归), 或交给指标查询看该分段的走势。
  3. 注意事项机器可读。 warnings 是一组稳定代码——分段相互抵消、高基数 维度被截断、某维度对不上账、总变化接近于零——Agent 据此知道哪些结论要 打折扣,不用解析散文。
  4. 拒绝也在引导,而不是报错。 引擎无法忠实分解的指标(窗口指标、去重 计数、复杂表达式)返回 strategy: "unsupported" 和结构化原因,Agent 由此转向——例如窗口指标改为对其基础指标在显式窗口上归因——而不是陷入 重试循环。dosi list metrics 会预先报告每个指标的 attribution_strategy,调用前即可确认支持度。

指标覆盖

  • 可加指标——求和、计数及其线性组合(含线性派生指标,另附每个成员对 变化的分解):完整维度归因。
  • 比率指标——含平均值:上文的结构/率值分解。
  • 暂不支持——窗口指标、去重计数和更复杂的表达式返回结构化的 unsupported 响应(规划在后续阶段支持),而不是给出误导性的数字。

限制

  • 每次调用最多 16 个候选维度,各维度独立分析;更深的下钻用 drill_down.where_sql 递归。
  • 每个维度的分段数有上限(默认 500,最高 1000);超过后该维度打上 truncated 标记。
  • 只在一个窗口出现的分段由引擎兜底处理——零填充或标记为 entered/exited—— 对比永远不会悄悄丢掉它们。