Skip to content

Analysis and dashboards

Cross-run questions ("does round success fall as channel noise rises, and by how much?") are answered in the web UI: pick a cohort, group it, measure it, chart it, save it. The same questions are answerable from a terminal with glossogen analyze, which reads the runs directory directly.

The query model

Three things make a query.

Grain is what one row is. run is one row per run, round one per (run, round) that some selected metric reported, agent one per agent on the run's registered roster. The round and agent grains follow the same row rules as the CSV export's tables, so a chart and the table it could have come from cover the same observations.

keyed is the fourth grain: one row per number a metric wrote along an axis of its own. Feature presence scores a confidence per ontology category, probe similarity a number per (agent, question, cutoff), language repetition a factor per message. None of those fit per_round or per_agent, so the metrics wrote them to a file beside the report. At this grain those files are read and their keys become dimensions, prefixed key.: key.category_id, key.question_id, key.message_id. Nothing in the query path knows what any of them mean. The metric that wrote the file declares how to read it.

Two things follow from the keys being the metric's own. Two metrics keyed differently never share a row, so asking for both puts each one's numbers on its own rows with the other's cell blank. And the metric's score_unit is not claimed on that axis: it describes the run-level score (communication_feature_presence counts categories over a threshold) while the keyed values are something else (a confidence), so the axis carries no unit rather than the wrong one.

Dimensions are what a row can be grouped or filtered by: run metadata, knobs (knob.*), key=value labels (label.*), bare tags (label_flag.*), per-agent identity (agent_model.*), lineage (lineage.*), plus the grain's own keys (round_number at the round grain, agent_id / agent_role / agent_model / agent_provider at the agent grain). None of these come from a list anyone maintains. They come from what the selected runs recorded.

Measures are what gets aggregated: any evaluator metric the selected runs' reports carry, or a numeric run column (total_cost_usd, duration_seconds, total_messages, current_round). Aggregates are mean, median, sum, count, min, max, stddev, and sem.

A query groups by at most two dimensions: the first is the x axis, the second the series.

An empty cell is not a zero

A measure with no number is dropped before the aggregate is computed and counted as missing beside it. A run that was never evaluated therefore lowers no average, and a metric that decided it did not apply to a run does not drag it toward zero.

A measured 0.0 is the opposite: the metric ran and counted zero, and it is in the aggregate like any other number.

Every answer carries both counts, so a mean over three runs and a mean over ninety are told apart on sight: the chart tooltips show n, the table shows n and how many were missing, and glossogen analyze prints both as columns.

stddev and sem over a single observation are empty rather than 0, since one observation has no spread to report.

From the command line

# what this cohort can be grouped, filtered, and measured by
glossogen analyze --runs-dir ./runs --scenario veyru --label channel_noise --list-fields

# which communication features the cohort's protocols show, strongest first
glossogen analyze --runs-dir ./runs --scenario veyru --label baseline \
  --grain keyed --group-by key.category_id \
  --measure communication_feature_presence:mean --sort measure_descending

# one row per noise level, with the mean and the standard error of round success
glossogen analyze --runs-dir ./runs --scenario veyru --label channel_noise \
  --group-by knob.channel_noise_level \
  --measure round_success:mean --measure round_success:sem
knob.channel_noise_level  runs  obs  metric.round_success:mean  n   metric.round_success:sem  n
------------------------  ----  ---  -------------------------  --  ------------------------  --
0.2                       60    60   0.4967                     60  0.03663                   60
0.4                       60    60   0.3767                     60  0.03413                   60
0.6                       60    60   0.2011                     60  0.02765                   60

Selection flags match glossogen export: --scenario, --label, --run-id-contains, --status, --contains-agent-id, --knob, or --run-id to name runs outright. The two forms cannot be combined.

--knob takes one <knob><operator><value> condition on the run's recorded scenario_config and is repeatable. Quote it when it contains > or <, or the shell reads them as redirection. See Filtering by knob.

Other flags:

Flag Does
--grain run\|round\|agent\|keyed what one observation is
--group-by KEY repeatable, at most twice
--measure [source:]key:aggregate repeatable; source defaults to metric
--filter key:operator[:values] repeatable; operators are in, not_in, contains, gte, lte, is_empty, is_not_empty
--sort group\|measure_ascending\|measure_descending with --sort-measure N row order
--json the full answer, including per-cell counts
--list-fields print the dimensions and measures, then stop

Every key a query names is checked against what the selection carries at that grain, the same list --list-fields prints. A --group-by, --filter or --measure key the selection does not carry is refused by name rather than answered with one group called "" or a column of blanks, and a gte / lte filter whose bound is not a number is refused the same way. A selection that carries no key of a kind cannot check that kind: an empty selection, or one with runs but no rows at the asked grain, answers with an empty result, and a cohort nothing has evaluated yet still answers a metric measure with blanks.

Numeric groups sort as numbers, so a sweep over 800, 2000, and 10000 charts in that order, where a string sort would put 10000 first.

In the browser

Analysis on the runs page opens the surface at /g/<group>/analysis.

The analysis surface, numbered

# What it is
1 Saved dashboards; New starts an empty one
2 The open dashboard, its description, and Save / Delete. Saving publishes it to everyone in the group
3 The cohort: the same filters as the runs list, shared by every chart below
4 Dashboard-level filters, applied on top of whatever each chart filters for itself
5 One chart, with how many groups, runs and observations sit behind it
6 The chart's own controls: its result table, a CSV of the rows, edit, delete
7 What the axes are: the measure with its aggregate and unit, and the grouping dimension
8 Add chart opens the builder

A dashboard holds the cohort and the filters. Every chart on it inherits both, and a chart can narrow further with filters of its own. Re-pointing a whole study at another cohort therefore changes one control.

Bar and line charts take one grouping key (the measures are the series) or two (the second key is the series, and the first measure is drawn). Scatter places one mark per group with two measures as the axes. Heatmap needs both grouping keys. Table is the numbers.

Bar and line charts draw error bars from a second measure: add the metric twice, once as mean and once as sem or stddev, and point the chart's "Error bars from" at the second. A chart with no error bars says nothing about spread. One with a zero-length bar says the spread was measured and was zero.

Every chart has the table one click away and downloads the rows behind it as CSV. A chart draws at most eight series, three on a scatter; series past that are left undrawn and the chart notes how many were hidden.

Saved dashboards

A dashboard stores queries. Reopening one re-runs them, so runs added or evaluated since it was saved show up without anyone rebuilding the chart.

Dashboards belong to a group and everyone in that group sees them. Names are unique per group. With DATABASE_URL set they live in the dashboards table. Without one they are JSON files under <runs-dir>/_dashboards/<group-id>/, so a checkout with no database keeps the feature and a copied runs directory carries its analyses with it.

The endpoints

Endpoint Answers
POST /api/g/{group}/runs/analysis/fields what a selection can be grouped, filtered, and measured by, at one grain
POST /api/g/{group}/runs/analysis/query one grouped, aggregated result
GET\|POST /api/g/{group}/dashboards list, create
GET\|PUT\|DELETE /api/g/{group}/dashboards/{id} read, replace, remove

They are POSTs for the reason the export endpoints are: a selection can name hundreds of runs, which does not fit in a URL.

A query naming a dimension or a measure the selection does not carry at that grain is refused with 422 naming the keys, so a typo in a chart is an error rather than an empty chart. A knob condition whose value cannot be read as the type a run recorded (round_count>=lots) is refused the same way.

A selection matching nothing is answered with an empty result rather than refused, so a saved dashboard pointing at a cohort that is empty today renders as "no runs match". Runs the selection names that no longer exist come back on the answer under missing_run_ids, since a dashboard outlives the runs it was built on. A selection over the export's run ceiling is refused with 413.

Sidecars are read only for a keyed query. They cost one file open per metric per run, and no other grain can use them, so the record cache holds two versions of a selection, with and without them, rather than paying that cost on every chart.

Loaded runs are cached in-process for a minute per selection, so editing a chart does not re-read every run's report on each change. What is cached is a projection of each run down to its dimension cells and its numbers, 18 KB a run rather than the 156 KB its full report costs, which is what lets a scenario-wide cohort stay in the cache while someone works on it. The cache budget is counted in runs, and a selection wider than the whole budget is answered without being kept.

Limits

Limit Value Why
Runs per selection the export's MAX_EXPORT_RUN_COUNT one report read per run
Group-by keys 2 a chart has an x axis and a series
Result rows 5000 past that a chart is a download
Dimension values offered per filter 200 a picker, not a listing; the true count is reported beside it