learn-data-observability-with-phoebe / Builder session 5 of 8
Learn Data Observability with Phoebe · Builder session 5 of 8

Distribution drift from metadata only

Freshness, volume, uniqueness and schema all answer the question "did the load work". Distribution answers a harder one: the load worked, the counts are right, and the values are no longer the values they were. Four statistics get you a surprisingly long way into that question without reading a single row, and one of them catches the last defect in Bellwether's history. The same statistic also pages on a night when nothing happened, and both of those facts are taught here.

🟠 Advanced Analytics engineers 45 min
0-3 · Welcome 3-20 · How far you get without reading a row 20-40 · Build-along: the null-rate monitor 40-45 · Q&A
Part 0

Where the monitor set stands

The artifact so far: a metrics table on analytics.orders_daily at Bellwether recording one row per night, plus four monitors. Learned freshness, same-weekday volume, duplicate rate and the column-set diff. Four of the five nights that go wrong in the 30-night history are now caught, and one is not: night 25, when customer_id stops arriving for one order in nine. Tonight is the signal that sees it, and the point at which the cheap approach runs out. Session 6 wires the whole set into expectations as code, session 7 adds lineage, session 8 scores it.

Live = presented in the 45 minutes Self-study = the full depth, after class 🔍 Every diagram zooms

★ What you walk out with tonight. Four aggregate statistics that describe the shape of a table without reading its rows, a trailing 3-sigma monitor in working SQL, and the two arithmetic reasons that monitor is fragile on a near-zero signal. Plus the sentence to say when somebody asks for anomaly detection on values: that is a different set of algorithms and here is the course that owns them.

LiveWhat "distribution" means when you refuse to read rows3 min▶

Reading rows to compare distributions is a solved problem with well-understood tools: a two-sample test, a divergence between histograms, a drift score per column. All of them need the values, which means a full scan or a sample, which means a cost per column per night on every table you care about.

The metadata alternative is to describe the shape with a handful of numbers that a single aggregate pass produces anyway, and then watch those numbers over time. A null rate is not a distribution. Four numbers that each move when the distribution moves are close enough to catch the failures that matter, at a cost of one query per table per night, which is what makes it the layer that survives the second invoice.

Real world

Bellwether's checkout team made a phone-number field optional in a release. Nothing failed. The order rows kept arriving, the counts stayed normal, and customer_id null rate went from 0.5 to 11.4 percent on night 25 because the same form change removed the account lookup for guest checkouts. Every customer-level aggregate downstream quietly dropped one order in nine, and the row count, the load time, the key uniqueness and the column list were all perfect.

Self-studyWhy distribution is the last signal to instrument3 min▶

It is fifth in the order for a reason, and the reason is precision rather than difficulty. The null-rate query is easier than the same-weekday volume median. What makes it last is that a distribution statistic has no natural threshold: lateness has a promise, volume has a same-weekday history, uniqueness has a key. A null rate has only its own past, so every threshold is learned, and a learned threshold on a signal with almost no variance is the single most reliable way to manufacture false pages.

  • Instrument it last so you have at least two weeks of history to learn from before you switch on an alert.
  • Record it from night one even so, because the history is the only thing that makes the threshold defensible later.
  • Expect to tune it more than any other monitor in the set. This is the one where the first threshold is usually wrong.
Part 1 · covers the four shape statistics and the 3-sigma rule

Four cheap shape statistics

9 min live

Each one is a single aggregate over the nightly partition, each one catches a failure class the other three miss, and each one has a characteristic false alarm that you will meet in its first fortnight. Knowing the false alarm in advance is what stops the monitor being muted before it earns anything.

Four statistics. One aggregate pass each. Not a row read between them. STATISTIC WHAT IT CATCHES ITS CHARACTERISTIC FALSE ALARM COST Null rate per column A field goes optional upstream A near-zero baseline trips 3 sigma on 0.3 points one aggregate pass Cardinality distinct / count A join fans out; a code list collapses A legitimate new product code lands overnight count distinct: the dearest of the four Min and max per column A bad date, a negative amount The first order over 1,000 in a year one aggregate pass Segment share by status, channel A category stops arriving, rows hold A campaign shifts the channel mix for a week one group by Segment share is the underrated one: rows hold, a category vanishes. Every false alarm above is a real change in the data. That is what makes them hard.
🔍 Click to zoom - four statistics, four failure classes, four false alarms to expect in week one.
LiveNull rate per column3 min▶

What it catches. A field that went optional upstream, an authentication or lookup step that stopped running, a join that started missing on the right side. Anything where the row still arrives but one of its values does not.

What it costs. One aggregate pass for every column you name, all in the same query. On a nightly partition this is the cheapest statistic on the list.

Its characteristic false alarm. The near-zero baseline. A column that is null 0.4 percent of the time has a trailing standard deviation measured in hundredths of a percentage point, so an ordinary night at 0.7 percent is more than three standard deviations out. This is night 8 in Bellwether's history and it is a page nobody wanted, covered properly in part 2.

The monitor itself is a trailing window with a sigma rule, computed exactly the way the bench in session 8 computes it: take the null rate for the last 14 nights, take their mean and standard deviation, and fire when tonight exceeds the mean plus three standard deviations.

Real world

On night 25 that rule produces "customer_id null rate 11.4 percent vs a trailing mean of 0.5". That is the sentence that catches the fifth and last defect in the history, and no other monitor in the set sees the night at all: the load was on time, the row count was normal, the duplicate rate was at baseline and the column list was unchanged.

LiveCardinality: distinct over count3 min▶

What it catches. Two opposite failures with one number. Cardinality collapsing means a code list shrank or a lookup started returning a default for everything, so 40 distinct ship_region values became 3. Cardinality exploding on a column that should be low means a join fanned out or a free-text field got pointed at the wrong source.

What it costs. The most of the four. count(distinct) is a shuffle on most engines, and on a wide table with many columns it is the statistic that shows up on the bill. Approximate counting functions cut the cost hard and are accurate enough for a drift signal, which is the right trade here: you are watching a ratio move, not auditing a number.

Its characteristic false alarm. A legitimate new category. Product codes, promo codes and region codes get added by the business without telling anybody, and cardinality is by design sensitive to exactly that. Watch the ratio rather than the raw count, and set the band wider than instinct suggests.

One thing worth separating: cardinality on a key column is not this statistic. That is the duplicate rate from session 3, it has a defensible absolute threshold, and it belongs in its own monitor.

LiveMin and max per column3 min▶

What it catches. The failures that a rate cannot see because they affect one row. A single order dated 1970 from a null-to-epoch conversion. A negative amount from a refund path that started writing to the orders table. A maximum that jumps by three orders of magnitude the night somebody changed a currency field from pounds to pence.

What it costs. One aggregate pass, and it comes free alongside the null-rate query.

Its characteristic false alarm. Genuine extremes. The first order over 1,000 pounds in a year is a real order, and a max-based monitor is right to notice and wrong to page. This is the statistic to log and chart rather than to alert on, with two exceptions worth an actual page: a value outside the physically possible range for the column, and a min or max that changes type of magnitude, such as a date before the company existed.

Store min and max for dates and amounts, alert only on the impossible, and read the chart weekly. That division is what stops this one becoming the monitor everybody mutes.

Self-studySegment share, and the failure only it can see4 min▶

What it catches. The proportion of rows in each value of a low-cardinality column: status, channel, currency. Its unique contribution is the failure where one category stops arriving entirely while the row count holds steady, because something else grew to fill the gap.

What it costs. One group by, and the result is a handful of rows per night per column. Store it long and wide rather than as a single number, because the interesting version of the alert names the segment.

Its characteristic false alarm. Real mix changes. A marketing push moves the app share from 30 to 45 percent in a week, which is a true change in the business and a false page about the data. The mitigation is a wider band plus a minimum absolute share, so a segment that was 2 percent and became 3 percent does not page.

  • A new category appearing is the case worth a separate rule. status gaining held is not a schema change, the column list is identical, and it is the exact failure that breaks every downstream case statement. Segment share is where that gets caught.
  • A category disappearing is the mirror image and the more dangerous one, because a missing segment reads downstream as a genuine absence rather than as an error.
  • Row count blindness is the reason it earns a slot. If mobile orders vanish and web orders grow by the same amount, volume is perfect, freshness is perfect, uniqueness is perfect, and every channel-split report in the estate is wrong.
Part 2 · covers the fragility of the 3-sigma rule and the boundary

The limits, stated honestly

8 min live

The null-rate monitor catches the last defect in the history and it also fires on night 8, when nothing happened. That page is not a bug in the query. It is what a three-standard-deviation rule does to a signal whose standard deviation is nearly zero, and it is worth walking through the arithmetic rather than waving at it.

customer_id null rate, 30 nights Top: the real scale. Bottom: the same series zoomed to the first 0.9 percent. 0 pct 4 pct 8 pct 12 pct night 25 · 11.4 pct trailing mean 0.5 Zoomed to 0.9 percent 0 0.9 25 is off the top night 8 3 sigma envelope trailing mean Night 8 is 0.7 pct against a trailing mean of 0.4, and 3 sigma sat at 0.6. One of these two pages is the catch. The other is a page nobody wanted.
🔍 Click to zoom - the same rule produces the best catch in the set and one of its two surviving false pages.
LiveWhy 3 sigma is fragile on a near-zero signal4 min▶

Night 8 in Bellwether's history is an ordinary night. customer_id null rate came in at 0.7 percent against a trailing mean of 0.4 percent. Nothing changed upstream, nothing broke, and the monitor paged.

The arithmetic is not in dispute. The trailing nights sat between roughly 0.3 and 0.5 percent, so the standard deviation of that window is under a tenth of a percentage point, and three of those puts the alert line at about 0.6 percent. A night at 0.7 percent is genuinely outside it. The rule fired correctly on a difference of three hundredths of a percentage point, which is not a difference anybody in the business could act on.

That is the structural problem with a relative rule on a signal that barely moves: as variance goes to zero, any deviation at all becomes many sigma. Two fixes, both cheap, both with a stated cost:

  • A floor on the absolute change. Require the null rate to have moved by at least some number of percentage points, or to exceed some absolute level, before the sigma rule is allowed to fire. Two percentage points removes night 8 and keeps night 25. The cost is stated plainly: any real drift smaller than two points is now invisible, so a column creeping from 0.5 to 1.8 percent null over a month passes silently.
  • A minimum-variance guard. Floor the standard deviation before you multiply it, at something like a quarter of a percentage point. This keeps the rule relative for signals that do vary and stops it becoming a hair trigger for signals that do not. The cost is a threshold you now have to justify per column rather than one rule for the table.

A third option worth naming: require two consecutive nights outside the band. It halves the noise on wobbles and doubles the detection time on real drift, which on a nightly table is 24 extra hours of a wrong number in front of people.

LiveWhere metadata monitoring stops4 min▶

The moment you write "trailing mean plus three standard deviations" you have crossed into statistical anomaly detection, and you have done it with the weakest possible detector: a global rule, no seasonality, no trend term, no robustness to the outlier that poisons your own window. It works, it catches an 11.4 percent jump on a 0.5 percent baseline, and it should not be mistaken for the state of the art.

The algorithms that do this properly are a body of work in their own right, and they belong to learn-anomaly-detection-with-phoebe: rolling and seasonal decomposition detectors that model the weekly shape instead of averaging it away, robust statistics that a single spike cannot capture, isolation forest and other one-class methods for the multivariate case where no single column looks wrong but the combination does, and the evaluation apparatus for choosing between them.

The line between the two is worth stating as a working rule. Metadata monitoring owns the statistics that fall out of a load: rates, counts, extremes and shares, watched with rules simple enough that anybody on the rota can read the alert and understand why it fired. Statistical detection owns the models that need training, tuning, seasonality and their own validation. Reach for the second only when the first has been running long enough to show you which columns actually move.

The order matters more than the choice. A team that starts with a learned detector on an uninstrumented table has no history to train on, no idea which columns are volatile and no baseline to compare the detector against. Four cheap statistics recorded nightly for a month is the dataset that makes the sophisticated version possible.

Self-studyThe spike that poisons its own window4 min▶

A detail the arithmetic produces that is easy to miss and expensive in practice. After night 25 comes into the trailing window, the mean and the standard deviation of that window are computed over a set that contains an 11.4 percent value alongside a dozen values near 0.5. The standard deviation jumps by a factor of roughly 25, and the alert line for the following two weeks sits around 10 percent.

So the fortnight immediately after an incident is the fortnight in which the monitor is least able to see the next one. A null rate creeping to 5 percent on night 27 would pass without comment. This is the opposite of what anybody wants from a monitor and it is a direct consequence of using a mean and a standard deviation over a raw window.

  • Exclude known incident nights from the trailing window. This requires a table of incident dates, which you should be keeping anyway for the scoring in session 8.
  • Use a median and a median absolute deviation instead of a mean and a standard deviation. A single spike moves a median almost not at all, which is the same argument session 3 made for volume bands.
  • Winsorise the window by clipping the top and bottom value before computing. Crude, effective, and one extra clause.

Robust statistics are the right default for every trailing threshold in the set, and the reason this session uses the mean and standard deviation version is that it is what the bench in session 8 evaluates, so the false page on night 8 is a number you can reproduce rather than a claim.

Build-along

The null-rate monitor and its limits

★ 20 min · everyone builds

Six steps. Three build the monitor, two run it against the night it earns its slot and the night it does not, and the last one is the one to write down: what your fix just cost you in sensitivity.

1 · Compute the null rate for the columns you care about. One pass, one row per night, one column per statistic. Name the columns explicitly rather than looping over all nine, because the list of columns that matter is shorter than the table and it is a decision worth making on purpose.

★ Try it now select current_date as load_date, 'analytics.orders_daily' as table_name, count(*) as rows, (count(*) - count(customer_id)) / count(*)::float as null_rate_customer_id, (count(*) - count(amount)) / count(*)::float as null_rate_amount, (count(*) - count(ship_region)) / count(*)::float as null_rate_ship_region, (count(*) - count(channel)) / count(*)::float as null_rate_channel from analytics.orders_daily where load_date = current_date;

2 · Compute the trailing mean and standard deviation with a window function. Fourteen nights, excluding tonight. The rows between 14 preceding and 1 preceding frame is the whole trick, and getting the 1 preceding right matters: include tonight in its own baseline and a real jump partially hides itself.

★ Try it now select load_date, null_rate_customer_id as rate, avg(null_rate_customer_id) over ( order by load_date rows between 14 preceding and 1 preceding ) as trailing_mean, stddev_samp(null_rate_customer_id) over ( order by load_date rows between 14 preceding and 1 preceding ) as trailing_sd, count(*) over ( order by load_date rows between 14 preceding and 1 preceding ) as n_history from analytics.metrics_daily where table_name = 'analytics.orders_daily' order by load_date;

3 · Wrap it in the 3-sigma monitor. Fire when tonight exceeds the trailing mean plus three trailing standard deviations, and refuse to fire at all until you have at least seven nights of history, because a standard deviation over three nights is a random number wearing a lab coat.

★ Try it now with s as ( /* the query from step 2 */ ) select load_date, round(rate * 100, 1) as rate_pct, round(trailing_mean * 100, 1) as mean_pct, round(trailing_sd * 100, 3) as sd_pct, 'customer_id null rate ' || round(rate * 100, 1) || ' percent vs a trailing mean of ' || round(trailing_mean * 100, 1) as page_text from s where n_history >= 7 and rate > trailing_mean + 3 * trailing_sd order by load_date;

4 · Run it against night 25. The rate is 11.4 percent, the trailing mean is 0.5 percent, and the page reads "customer_id null rate 11.4 percent vs a trailing mean of 0.5". Check every other monitor in your set against the same night: freshness, volume, uniqueness and the column-set diff all pass. This is the defect that only this signal sees, and it is the fifth of five.

5 · Now run it against night 8. The rate is 0.7 percent, the trailing mean is 0.4 percent, and the standard deviation of that window is under a tenth of a point, so three sigma sits at about 0.6 and the monitor fires. Nothing happened on night 8. Print the trailing window alongside the alert and look at the numbers: this is what a correct query firing on a meaningless difference looks like, and you need to recognise it in your own data.

6 · Add a floor, count the survivors, and write down the cost. Add one clause: and rate - trailing_mean > 0.02, a two-percentage-point floor on the absolute change. Re-run over all 30 nights and count. Night 25 survives, night 8 does not, so of the two pages the monitor produced you have kept exactly the one you wanted. Then write the cost in the README in one sentence: any real drift under two percentage points is now invisible on this column, so a slow creep from 0.5 to 2.0 percent null will never page. Try the alternative too, flooring the standard deviation at 0.25 percentage points, and see which of the two you would defend to whoever owns the dashboard.

Write the floor down where the threshold lives, not in a commit message. A sensitivity floor is invisible in the query's behaviour on a normal night and it is the first thing anybody needs to know during the incident it failed to catch. One comment line next to the number, and one sentence in the README.

Homework

Before session 6

30 min
  1. Add all four statistics to the nightly metrics row for one table: null rate for the columns that matter, cardinality ratio for one or two, min and max for your date and amount columns, and segment share for one low-cardinality column. Record them all, alert on one.
  2. Plot 30 nights of the null rate for your three most-read columns and look at the variance rather than the level. The column with almost no variance is the one that will page for nothing, and you can identify it before it does.
  3. Pick a floor or a minimum-variance guard for that column and write the sensitivity cost next to it in one sentence. If you cannot write the cost, you have not chosen a threshold, you have chosen a number.
  4. Find the low-cardinality column on your table where a segment disappearing would be invisible to every other monitor you have. That column is the argument for segment share, and it is usually status or a channel field.
Check yourself

Three questions

Night 8 has a null rate of 0.7 percent against a trailing mean of 0.4 percent, and the 3-sigma monitor pages. Why?

The arithmetic is correct and the page is still worthless. As variance approaches zero, any deviation becomes many sigma, which is why a relative rule on a near-flat signal needs either an absolute floor on the change or a minimum-variance guard.

Mobile orders stop arriving overnight and web orders grow by the same number of rows. Which of the four statistics sees it?

The row count is unchanged, nothing is null and no extreme moved, so volume, freshness, uniqueness, null rate and min and max all pass. Only the proportion per category moved, which is what makes segment share the most underrated of the four.

A stakeholder asks for seasonal decomposition and an isolation forest on the values in amount. Where does that work belong?

Rolling and seasonal detectors, isolation forest and one-class methods are a body of work with their own tuning and evaluation. The metadata layer stops at statistics that fall out of a load, watched with rules the rota can read, and it is also the layer that produces the history those models need.

Sources covered

What tonight maps to

✓ Four metadata shape statistics Null rate per column, cardinality as distinct over count, min and max, and segment share. What each catches, what each costs, and the false alarm each one produces in its first fortnight.
✓ The trailing 3-sigma null-rate monitor Built in SQL exactly as the session 8 bench computes it: 14 nights, mean plus three standard deviations, minimum seven nights of history. Catches night 25 at 11.4 percent against a trailing mean of 0.5.
✓ The fragility of a relative rule on a near-zero signal Night 8 at 0.7 percent against 0.4, real arithmetic and an unwanted page. Both fixes built and both costs stated, plus the spike that poisons its own trailing window.
✓ Distribution as one of the five signals Handled with metadata only, which is the constraint that keeps the layer cheap enough to run on every table rather than on the three that got budget.
◐ Statistical and ML anomaly detection Rolling and seasonal detectors, robust statistics, isolation forest and one-class methods are named and handed to learn-anomaly-detection-with-phoebe, which owns the algorithms and their evaluation.
◐ A new code value breaking a downstream case statement Caught here by segment share rather than by the column-set diff, because the column list and the types are unchanged. Session 4 names it as the member of the silent class that is not a schema change at all.
· Row-level value rules such as accepted-values assertions Not covered here. A build-time assertion on the values you produce yourself belongs to the transform project's own tests, which is what the analytics engineering track is for.

Session 5 cheat sheet

Four statisticsNull rate, cardinality ratio, min and max, segment share. One aggregate pass, no rows read.
The monitorTrailing 14 nights, fire above mean plus 3 sd, refuse to fire under 7 nights of history.
What it catchesNight 25: customer_id null rate 11.4 percent vs a trailing mean of 0.5. Nothing else in the set sees it.
What it costsNight 8: 0.7 percent against 0.4. Correct arithmetic, unwanted page, three hundredths of a point.
Two fixesAn absolute floor on the change, or a minimum-variance guard. Write the sensitivity cost next to whichever you pick.
Segment share is underratedThe only one that sees a category stop arriving while the row count holds steady.
Min and max: log, do not pageAlert only on the physically impossible. Genuine extremes are real data.
The boundaryRolling and seasonal detectors, isolation forest, one-class methods: learn-anomaly-detection-with-phoebe.