Skip to content

Learn

Analysis

Some numbers always wobble, so a change is only news once it is bigger than the noise and you know where it happened.

How the engine finds what changed and who moved it, with SQL facts, noise bands, and contribution and price-volume-mix splits.

Stage 3 of 10. MEASURE, DETECT and DECOMPOSE turn the metrics into facts, find the changes worth your time, and split each change into who caused it. No language model runs here either.

Six months of revenue is easy to look at. Six months of five metrics by six customers, two channels and five products is not: some numbers always wobble, a seasonal peak is not news, and “revenue fell” is useless until you know where it fell. A model asked to eyeball the table will find patterns in noise and do the subtraction in its head.

So detection is a fixed library of tests, each one SQL plus a small function, and every number it produces is a fact with an ID.

MEASURE · DETECT · DECOMPOSE · the coffee-roaster run

In

  • revenue 2026-01$41,200N14
  • revenue 2026-02$42,900N15
  • revenue 2026-03$44,100N12
  • revenue 2026-04$36,200N11
  • revenue 2026-05$35,800N16
  • revenue 2026-06$37,000N17

MEASURE · DETECT · DECOMPOSE

  1. 1 · CompareThe latest month against the one before, as SQL facts and a function fact for the change.
  2. 2 · Test against noiseIs the move bigger than the series usually wobbles? A robust z-score and a noise band answer it.
  3. 3 · Split the changeWho moved? Contribution by channel, and price × volume × mix by product.

Out

  • change−$7,900N29
  • change %−17.9%N30
  • robust z−24.55N18threshold ±3.5
  • wholesale−$8,500N144
  • share of the drop108%N147
  • PVM mix effect−$7,170.84N174

From facts, insights and decompositions of the run.

Compare. March revenue was $44,100N12, April’s $36,200N11.⁠ Both are SQL facts: each chip opens its query, parameters and cited rows in the fact ledger at the end of the page. The change, −$7,900N29, and the percentage, −17.9%N30, are function facts: delta and pct_change applied to the two SQL facts, so each one points at its inputs.

Is it bigger than the noise? Every series wobbles. The engine measures how much revenue usually moves and asks whether April is outside that. Two tests agree here:

  • a robust z-score of −24.55N18, against a threshold of ±3.5: far outside the usual wobble;⁠ Robust because it measures distance from the median in units of the median absolute deviation, so one outlier can’t stretch the scale. See the glossary.
  • a noise band of ±741.3, against a level shift of −$6,400N28 after March (template trend_break.v1).⁠ The band is not a fact but a parameter of the test, stored in the insight’s details: 2 × 1.4826 × the median absolute deviation of the series’ other month-to-month changes, so the change being tested can’t widen its own band (noiseBand in baselines.ts).

March’s own rise from February was small enough to stay inside the band, so it is not reported at all. On this run 20 candidate insights came out of 8 templates (new_lost.v1, anomaly.v1, trend_break.v1, period_change.v1, ratio_shift.v1, trend.v1, concentration.v1, mix_shift.v1); ranking keeps the ones that matter.

Who moved it: contribution. Split by channel, the change adds up exactly:

ChannelChangeShare of the change
wholesale−$8,500N144108%N147
retail$600N145−7.6%N148
total−$7,900N146

Wholesale is more than 100% of the drop (108%N147) because retail grew a little ($600N145).⁠ A share above 100% is not an error: it means the other members moved the other way.

Price, volume or mix? Split by product with price-volume-mix: fewer units sold (volume) −$1,911.07N173, a shift toward cheaper products (mix) −$7,170.84N174, prices themselves $1,181.91N175. The three add up to −$7,900N176. Most of the fall is mix: sales moved away from the 1 kg bags, the products wholesale customers buy.

The same kind of split by customer, drawn as a waterfall:⁠Drawn from the build’s own run of the example, the same run as every chip on this page.

Contribution by channel · Revenue fell after March, driven by wholesale

  1. Revenue 2026-03$44,100 N12
  2. wholesale−$8,500 N144108% of the change N147
  3. retail$600 N145−7.6% of the change N148
  4. Revenue 2026-04$36,200 N11

Both totals are drawn from a broken axis (≈), so the changes show at their own scale. Each change and its share are contribution.v1 facts recomputed by the verifier. The members drawn are all of them: their changes are the decomposition's total, −$7,900 N146.

Each number is stored with its computation: the SQL and parameters, or the function and the IDs of its inputs. That is what lets the verifier recompute it later, and you open it from its chip.

An ordinary z-score uses the mean and standard deviation, which one outlier can drag around. A robust z-score uses the median and the median absolute deviation (MAD) instead, so a single strange month cannot hide another. A value beyond about 3.5 robust z is a common rule of thumb for “unusual”.

Contribution, price-volume-mix and rate-versus-mix are identities: the parts always add up to the whole, and a test checks it on random data. There is also a Simpson’s-paradox guard: when a rate moves one way in every group but the other way in total, the engine reports the mix shift instead of a misleading headline.

A December peak in retail is not news. The engine compares a month with the same month in earlier years when it has them, and doesn’t report a move that happens every year. The coffee example has only six months, so this is shown on the public datasets of the real-data evals (evals/results/2026-10-23-baseline-real.json):

  • Hotel booking demand datasets: The summer peak: August is the top month of room revenue in 2015, 2016 and 2017 (G2). 2015 peak 2015-08; 2016 peak 2016-08; 2017 peak 2017-08; 3/3 values equal SQL (pass)
  • Electronic card transactions, September 2026: The December peak: December is the top month of 2023, 2024 and 2025 (G1). 2023 peak 2023-12; 2024 peak 2024-12; 2025 peak 2025-12; 3/3 values equal SQL (pass)
  • Electronic card transactions, September 2026: The regular January fall is not reported as news (G4: January is below December in 24 of 24 years). none (pass)
  • Online Retail II: The Nov–Dec peak: November is the top month of 2010 and 2011 (G2). 2010 peak 2010-11; 2011 peak 2011-11; 3/3 values equal SQL (pass)
  • Online Retail II: No finding about December 2011, a partial month (G3: the data ends 2011-12-09). no finding at 2011-12 (pass)

The first tutorial’s job prints these findings; aa explain opens any fact:

Terminal
AA_DEMO_MODEL=scripted bun run aa analyze examples/coffee-roaster --yes --embedder hash --today 2026-07-01 --currency USD
bun run aa explain <job-id> N30

See Analyse your first file for the recorded output.

Go deeper: Detecting changes and Finding drivers.

FactValueWhat it isHow it was computedVerified
N11 $36,200 revenue_2026-04
SQL, run in DuckDB
WITH m1 AS (SELECT strftime("date", '%Y-%m') AS period, sum("amount") AS value FROM "t_t1" WHERE NOT _is_total AND "date" IS NOT NULL AND (("status" IS NULL) OR ("status" NOT IN ($1))) AND strftime("date", '%Y-%m') BETWEEN $2 AND $3 GROUP BY ALL)
SELECT period, value FROM m1 ORDER BY period

parameters: {"$1":"refunded","$2":"2026-04","$3":"2026-04","input_rows":1020}

recomputed
N12 $44,100 revenue_2026-03
SQL, run in DuckDB
WITH m1 AS (SELECT strftime("date", '%Y-%m') AS period, sum("amount") AS value FROM "t_t1" WHERE NOT _is_total AND "date" IS NOT NULL AND (("status" IS NULL) OR ("status" NOT IN ($1))) AND strftime("date", '%Y-%m') BETWEEN $2 AND $3 GROUP BY ALL)
SELECT period, value FROM m1 ORDER BY period

parameters: {"$1":"refunded","$2":"2026-03","$3":"2026-03","input_rows":1050}

recomputed
N14 $41,200 revenue_2026-01
SQL, run in DuckDB
WITH m1 AS (SELECT strftime("date", '%Y-%m') AS period, sum("amount") AS value FROM "t_t1" WHERE NOT _is_total AND "date" IS NOT NULL AND (("status" IS NULL) OR ("status" NOT IN ($1))) AND strftime("date", '%Y-%m') BETWEEN $2 AND $3 GROUP BY ALL)
SELECT period, value FROM m1 ORDER BY period

parameters: {"$1":"refunded","$2":"2026-01","$3":"2026-01","input_rows":980}

not in the report
N15 $42,900 revenue_2026-02
SQL, run in DuckDB
WITH m1 AS (SELECT strftime("date", '%Y-%m') AS period, sum("amount") AS value FROM "t_t1" WHERE NOT _is_total AND "date" IS NOT NULL AND (("status" IS NULL) OR ("status" NOT IN ($1))) AND strftime("date", '%Y-%m') BETWEEN $2 AND $3 GROUP BY ALL)
SELECT period, value FROM m1 ORDER BY period

parameters: {"$1":"refunded","$2":"2026-02","$3":"2026-02","input_rows":1010}

not in the report
N16 $35,800 revenue_2026-05
SQL, run in DuckDB
WITH m1 AS (SELECT strftime("date", '%Y-%m') AS period, sum("amount") AS value FROM "t_t1" WHERE NOT _is_total AND "date" IS NOT NULL AND (("status" IS NULL) OR ("status" NOT IN ($1))) AND strftime("date", '%Y-%m') BETWEEN $2 AND $3 GROUP BY ALL)
SELECT period, value FROM m1 ORDER BY period

parameters: {"$1":"refunded","$2":"2026-05","$3":"2026-05","input_rows":1005}

not in the report
N17 $37,000 revenue_2026-06
SQL, run in DuckDB
WITH m1 AS (SELECT strftime("date", '%Y-%m') AS period, sum("amount") AS value FROM "t_t1" WHERE NOT _is_total AND "date" IS NOT NULL AND (("status" IS NULL) OR ("status" NOT IN ($1))) AND strftime("date", '%Y-%m') BETWEEN $2 AND $3 GROUP BY ALL)
SELECT period, value FROM m1 ORDER BY period

parameters: {"$1":"refunded","$2":"2026-06","$3":"2026-06","input_rows":1040}

recomputed
N18 −24.55 I3 anomaly.v1 revenue 2026-04:robust_z robust_z N14 N15 N12 N11 N16 N17 not in the report
N28 −$6,400 I5 trend_break.v1 revenue 2026-04:shift delta N27N26 not in the report
N29 −$7,900 I5 trend_break.v1 revenue 2026-04:change delta N11 N12 recomputed
N30 −17.9% I5 trend_break.v1 revenue 2026-04:change_pct pct_change N11 N12 recomputed
N144 −$8,500 contribution.v1 revenue 2026-04 by channel:delta:wholesale delta N113N112 recomputed
N145 $600 contribution.v1 revenue 2026-04 by channel:delta:retail delta N111N110 recomputed
N146 −$7,900 contribution.v1 revenue 2026-04 by channel:delta:total sum N145 N144 recomputed
N147 108% contribution.v1 revenue 2026-04 by channel:share:wholesale contribution.v1 N144 N146 recomputed
N148 −7.6% contribution.v1 revenue 2026-04 by channel:share:retail contribution.v1 N145 N146 recomputed
N173 −$1,911.07 pvm.v1 revenue 2026-04 by product:pvm:volume pvm.v1 N115N163N116N164N117N165N118N166N119N167N120N168N121N169N122N170N123N171N124N172 recomputed
N174 −$7,170.84 pvm.v1 revenue 2026-04 by product:pvm:mix pvm.v1 N115N163N116N164N117N165N118N166N119N167N120N168N121N169N122N170N123N171N124N172 recomputed
N175 $1,181.91 pvm.v1 revenue 2026-04 by product:pvm:price pvm.v1 N115N163N116N164N117N165N118N166N119N167N120N168N121N169N122N170N123N171N124N172 recomputed
N176 −$7,900 pvm.v1 revenue 2026-04 by product:pvm:total sum N173 N174 N175 not in the report