# Read a ledger row

> One row, field by field: what moved, how sure we are, and whether to read it at all.

Source: https://www.patterna.nl/docs/read-a-ledger-row

A ledger row answers one question for one surface and one period: how much better or worse did treatment do than control on the primary metric? Read it in this order: is it readable, is the SRM p-value clear, then the difference and its interval.

## An example row

```json
{
  "period_start": "2026-07-20",
  "period_end": "2026-10-11",
  "surface": "home_articles",
  "metric": "tap_rate",
  "control_rate": 0.042,
  "treatment_rate": 0.052,
  "difference": 0.010,
  "ci_low": 0.004,
  "ci_high": 0.016,
  "n_control": 14800,
  "n_treatment": 15020,
  "n_users_control": 1480,
  "n_users_treatment": 1502,
  "srm_p": 0.61,
  "readable": true,
  "revenue_cents_control": null,
  "revenue_cents_treatment": null,
  "export_uri": null
}
```

The numbers are an example, not a result.

## Field by field

| Field | What it says |
|---|---|
| `period_start`, `period_end` | The period you asked for. Only slates inside the experiment's window count. |
| `surface` | The surface key. |
| `metric` | The primary metric: `tap_rate`, `second_item_rate`, `join_rate` (community surfaces) or `engaged_after_tap`. |
| `control_rate` | Slates with at least one tap over slates served, in the control arm (the editorial list). Here 4.2%. |
| `treatment_rate` | The same for Patterna's slate. Here 5.2%. |
| `difference` | Treatment minus control: +1.0 percentage point. |
| `ci_low`, `ci_high` | The 95% interval around the difference: +0.4 to +1.6 points. `null` while an arm has fewer than two units. |
| `n_control`, `n_treatment` | Slates served in each arm. |
| `n_users_control`, `n_users_treatment` | Randomised units in each arm: users, or sessions in `non_profiled` mode. |
| `srm_p` | The sample-ratio-mismatch p-value. Below 0.001 the row carries a `note` and must not be read. |
| `readable` | `true` when each arm has reached the experiment's `readable_sample` (from the power calculation) and the interval exists. |
| `revenue_cents_control`, `revenue_cents_treatment` | Reserved for revenue attributed by last touch. Not computed yet: always `null`. |
| `export_uri` | Link to the export for this period, once one has been made. |
| `note` | Present when there is no interval yet or when SRM is flagged. |

## How to read it

**The interval decides, not the point.** If the whole interval is above zero, treatment did better. If it crosses zero, the data cannot tell the arms apart yet. Here +0.4 to +1.6 is above zero: Patterna's slate got more tapped slates than the editorial list.

**The metric is per slate, the interval is per unit.** One user sees many slates, and their taps are not independent. So the interval is clustered on the randomised unit with the delta method. That is why the row carries both slates (`n_*`) and units (`n_users_*`). See [Proof and the ledger](https://www.patterna.nl/docs/proof-and-the-ledger).

**Every served slate counts.** The ledger is intention to treat: slates the user never scrolled to, and slates the app gave up on, count as served with no tap, and so do slates answered with `shown: false`. Only slates requested with `context.measured: false` are left out, in both arms. See [Timeouts and what counts in the ledger](https://www.patterna.nl/docs/timeouts-and-the-ledger).

**`readable: false` means wait** until each arm reaches the experiment's `readable_sample`; the panel shows the interval dashed amber with the projected "readable on" date until then.

**An SRM flag means stop.** If the arm sizes differ from the split you asked for more than chance allows, do not read the row. Find the cause first. See [Run a holdout](https://www.patterna.nl/docs/run-a-holdout).

**Revenue is not computed yet.** When it is, it will be a last-touch diagnostic, never proof. Retention and conversion are reported, not proven, until the user base is large enough.

**Synthetic rows never count.** Example data from the twin is flagged `synthetic` and excluded from the ledger. The "Real / Example" switch never mixes them.

## Check it yourself

A row is computed from the logged impressions and the interactions reported on them. The export below gives you both, so you can check the slate and tap counts. It does not yet carry the clustering unit, `measured`, `not_shown`, `tester`, `fallback`, blocks or the experiment window, so it cannot reproduce a row's interval on its own; that is an open item on our side.

```curl
curl -sS -X POST "https://patterna-api.orven.am/v1/proof/export" \
  -H "Authorization: Bearer $PATTERNA_ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "2026-07-20",
    "to": "2026-10-11",
    "surface": "home_articles"
  }'
```

The answer is `202` with a `job_id`. Poll `GET https://patterna-api.orven.am/v1/proof/exports/{job_id}` until it is ready, then download it. In the panel, use **Proof → Export**: pick the period, wait for the file, download it. The note there spells out the arithmetic.

Fetch the rows themselves with:

```curl
curl -sS "https://patterna-api.orven.am/v1/proof/ledger?surface=home_articles&from=2026-07-20&to=2026-10-11" \
  -H "Authorization: Bearer $PATTERNA_ADMIN_KEY"
```

Without `from` and `to` the ledger covers the last 28 days.

## Weekly rows

`GET https://patterna-api.orven.am/v1/proof/ledger?surface=home_articles&bucket=week` returns one row per ISO week and arm: `week_start`, `arm`, `impressions`, `taps`, `rate` and a Wilson interval (`lo`, `hi`). The weeks are disjoint, not cumulative. They show the trend; the summary row above is the result.

## Rows per block

When one slate is shown in two blocks of a page and the interactions carry `block` (for example `top` and `end`), ask the ledger with `by=block` (every block) or `block=<name>`: it returns one row per surface and block, over the slates reported seen in that block and the taps in it. Those rows are descriptive; the whole-slate row decides.
