Replies: 2 comments 3 replies
-
Beta Was this translation helpful? Give feedback.
-
Algorithm Reference: Explain, Opportunity, Sensitivity, PredictThis comment provides a precise description of each algorithm as implemented in PR #24. 1. Explain (Root-Cause Analysis)Fast Mode (default): Greedy Adtributor SearchThe fast mode is a greedy recursive algorithm based on the Adtributor paper (Bhagwan et al., NSDI 2014). At each level, it evaluates two types of splits and picks whichever has the highest concentration (signed fraction of the parent's delta explained by the child): Component splits follow mathematical identities in the metric tree. For Dimension splits use Adtributor's surprise-based ranking. For each available dimension:
This means surprise selects which dimension to split on, while concentration selects which element within that dimension. A small segment with a large proportional shift beats a large segment with a modest shift, because its behavior is more unexpected given its historical share. Recursion strategy:
Stopping criteria:
Detection heuristics (always checked):
Driver attribution: After the component decomposition, if the target has declared Deep Mode (
|
| # | Strategy | Element selection | Dimension ranking |
|---|---|---|---|
| 1 | max_concentration |
Highest signed delta / parent_delta |
By that element's concentration |
| 2 | topk_concentration |
Highest ` | delta / parent_delta |
| 3 | jsd_smoothed |
Highest Laplace-smoothed JSD surprise (filtered by ` | EP |
| 4 | iv_woe |
Highest ` | WOE |
Strategies 1 and 2 diverge when opposing elements exist (some segments rising, others falling). Strategy 1 picks the largest same-direction contributor; Strategy 2 picks the largest absolute contributor. Strategies 3 and 4 diverge from 1/2 when share distributions shift in ways not captured by raw delta — e.g., a segment that was 1% of volume growing to 5% has high JSD/WOE but may have modest absolute delta.
Laplace smoothing (strategies 3 and 4): ε = 1 / (total_prev + total_curr), applied to all shares as (value + ε) / (total + ε × n). This handles zero-share elements without introducing NaN/Inf in log computations.
Adaptive EP threshold: 0.05 / √n (where n = number of elements). Prevents high-cardinality dimensions from filtering out all elements in uniform degradation scenarios.
Uniform degradation detection: When no single element has |EP| ≥ 5% but collectively they explain >50% of the parent delta, emit a UniformDegradation split instead of drilling into individual elements.
Phase 3: Cross-cutting detection. After all per-measure searches complete, scan all paths for the same (dimension_name, value) appearing across multiple measures from different views. E.g., ads.region=EMEA and subs.region=EMEA both explaining drops → emit a CrossCutting annotation with combined root_fraction.
Phase 4: Rank and truncate. Sort all paths by root_fraction descending, deduplicate by terminal (measure, filter_set), keep top max_alternatives (default 5).
Phase 5: Statistical significance. For each top-K path:
- Query 12 months of monthly data before the previous period for the terminal
(measure, filters)combination - Compute period-over-period deltas from the monthly values
- Run a two-tailed t-test:
t = (current_delta - mean) / (std / √n), p-value from Student's t-distribution withn-1degrees of freedom - Requires ≥6 historical periods; returns
Noneotherwise - Deduplication: historical queries are cached by
(measure, filter_set)key so paths sharing terminal nodes don't trigger redundant queries
2. Opportunity Sizing
Answers: "Where should we focus to grow this metric?"
Input: target measure, time dimension, period range.
Step 1: Query overall value. Execute the target measure aggregated over the period (no dimension breakdown).
Step 2: Discover dimensions. Find all non-time dimensions (string, number, boolean) in the target's view.
Step 3: Per-dimension gap analysis. For each dimension:
- Query the measure broken down by dimension value over the period
- Compute the benchmark:
- Additive measures (
count,sum,count_distinct,avg,min,max): benchmark =overall_value / n_segments(fair share) - Ratio measures (
type: number): benchmark =overall_value(the true weighted average)
- Additive measures (
- For each segment below the benchmark:
gap = benchmark - current_valuesegment_share = 1 / n_segmentsweighted_gap = gap × segment_share
- Sum weighted gaps →
total_weighted_gapfor the dimension - Sort dimensions by
total_weighted_gapdescending
Step 4: Downstream propagation. Take the top dimension's total_weighted_gap and propagate it through the metric tree via predict(), showing the estimated impact on upstream metrics.
Output: per-dimension opportunities (sorted by potential), per-segment detail within each, and downstream impacts.
3. Sensitivity Analysis
Answers: "What influences this metric?"
Algorithm: Backward BFS from the target through the metric tree.
- Build reverse adjacency:
target_measure → [edges pointing to it] - Seed BFS with all direct inputs (component and driver edges)
- For each edge traversed:
- Effective coefficient = product of edge coefficients along the path (chain rule). Component edges have coefficient 1.0. If any edge in the chain is qualitative (no coefficient), the effective coefficient is
None. - Direction: inferred from coefficient sign (positive coefficient → positive direction, negative → negative). Falls back to declared
directionfor qualitative edges. - Strength: inferred from
|coefficient|(≥0.5 → strong, ≥0.1 → moderate, else weak). Falls back to declaredstrength.
- Effective coefficient = product of edge coefficients along the path (chain rule). Component edges have coefficient 1.0. If any edge in the chain is qualitative (no coefficient), the effective coefficient is
- Collect all reachable drivers with their paths, effective coefficients, forms, lags, and descriptions
- Sort: quantitative drivers first (by
|effective_coefficient|descending), then qualitative (by strength rank)
Result: an ordered list of all direct and transitive drivers of the target, each with its influence path and magnitude.
4. Predict (What-If Analysis)
Answers: "What would happen if this metric changed by X?"
Algorithm: Forward BFS from each input change through the metric tree.
- For each input
(measure, delta):- Follow outgoing edges (component and driver) to parent measures
- Component edges: pass delta through exactly (
confidence: "exact") - Driver edges with coefficient:
output_delta = coefficient × input_delta(linear approximation for all forms;confidence: "estimated") - Driver edges without coefficient:
output_delta = 0(qualitative-only, can't quantify) - Continue propagating upward through the tree
- Diamond accumulation: each input gets its own
visitedset, so the same target reached from two different inputs accumulates both impacts. Impacts at the same node from multiple paths are summed. - Confidence tracking: if any edge in the path is "estimated", the entire path is "estimated". Only pure component-edge paths are "exact".
- Lag accumulation: lags from edges along the path are summed (total days before effect manifests)
- Collapse: one impact per target measure (sum of deltas from all paths), sorted by
|estimated_delta|descending
Output: list of all impacted measures with estimated deltas, confidence levels, propagation paths, forms, and cumulative lags.
Implementation Stats
- 304 tests total (76 for metric tree operations alone)
- 52 explain tests covering: pathological cases (hidden multi-dimension drops, JSD/IV-overblown signals), uniform degradation, Simpson's paradox, opposing offsets, cross-cutting patterns, significance testing, beam search convergence
- 8 sensitivity tests: coefficient chains, mixed quant/qualitative, diamond graphs, zero coefficients
- 7 predict tests: forward propagation, diamond accumulation, zero-coefficient pruning, confidence tracking, deep chains
- 10 opportunity tests: additive/ratio benchmarks, downstream propagation, gap sizing, edge cases
Beta Was this translation helpful? Give feedback.


Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Proposal:
driversannotation on measuresThe semantic layer today encodes what metrics are (dimensions, measures, expressions) but not how they relate. This RFC proposes a
driversfield on measures that declares quantitative and qualitative relationships, enabling automated sensitivity analysis, impact prediction, root-cause explanation, and opportunity sizing.The syntax
Two types of edges in the metric tree
Component edges (implicit) — extracted automatically from
type: numberexpressions containing{{view.measure}}references. These represent mathematical identity (e.g.,aov = gmv / orders). Edge signs (+/-) inferred from expression context.Driver edges (explicit) — declared via
drivers. These represent correlative or causal business relationships.Quantitative forms
linearΔy = c × Δxlog-log%Δy = c × %Δx(elasticity)log-linear%Δy = c × Δxlinear-logΔy = c × ln(1 + Δx/x)Operations enabled by this
sensitivity <measure>— rank all drivers by influence magnitudepredict --if measure=delta [--if ...]— propagate hypothetical changes through the treeexplain <measure> --time <dim> --current start:end --previous start:end— recursive root-cause analysis decomposing a metric change into (component, segment) pairsopportunity <measure> --time <dim> --period start:end— find underperforming segments, size the upside, propagate through driversWorked examples
Prototype branch:
feature/metric-tree(PR #24)Three examples demonstrating different use cases:
examples/metric-tree/) — ARR → MRR → churn/expansion/new, with marketing driversexamples/metric-tree-ecommerce/) — orders × AOV × take rate → revenue, cross-view drivers between orders/sellers/traffic, all four quantitative formsexamples/metric-tree-funnel/) — host onboarding funnel (HLP → LYS → Amenities → Address → ... → Active Listing), opportunity sizing finding that Android+India has 0% address conversionInteractive visualization
airlayer visualizegenerates a standalone HTML file with a force-directed graph of the full metric tree — no external dependencies, works offline.The visualization encodes relationship types visually:
The detail panel (shown on the right in the screenshot) surfaces the
driversmetadata — direction, strength, descriptions, and reference URLs — making the metric tree explorable by anyone on the team, not just whoever wrote the YAML.inspect --metric-treeprovides the same information as CLI text output:Explain: Adtributor-style root-cause analysis
airlayer explaindecomposes a metric change into the smallest (component, segment) pairs that explain it. The algorithm uses two types of splits:revenue = commission). Exact decomposition.Dimension ranking uses surprise, not raw delta. Instead of "which segment had the biggest change?", explain asks "which segment's share of the metric shifted the most?" This is based on the Adtributor algorithm (Bhagwan et al., NSDI 2014):
S = 0.5 × (p × log(2p/(p+q)) + q × log(2q/(p+q)))This means a segment that represents 5% of volume but drops 80% will be flagged over a segment representing 50% of volume that drops 30% proportionally — because the small segment's behavior is more unexpected given its historical share.
Driver attribution is included when the target measure has declared drivers. After the component decomposition, explain queries each driver's change across periods and estimates its impact using the declared coefficient and functional form:
Output shows both the component decomposition (what changed) and driver attribution (why it matters):
Open questions
Where should drivers live? Currently inline on measures via a
driversfield. Alternatives:.driver.ymlfilesrelationshipssection in the viewAre four quantitative forms the right set? Should we add exponential, polynomial, or custom expressions?
Static vs dynamic coefficients? Current approach is explicit YAML declaration. Future work could auto-fit coefficients from historical data.
Cross-view drivers: Drivers can reference measures in other views (e.g.,
orders.revenuedriven bysellers.active_listings_total). This is powerful but creates implicit coupling. Should we require explicit joins or keep it flexible?Lag semantics: The
lagfield (in days) declares a delayed effect. Should this affect query time ranges automatically, or is it purely documentation?Looking for feedback on the syntax design and which operations are most valuable.
Beta Was this translation helpful? Give feedback.
All reactions