Skip to content

Vis Node statistics with evalstats - #437

Draft
ianarawjo wants to merge 8 commits into
mainfrom
claude/evalstats-vis-node-integration-0c75d8
Draft

ianarawjo wants to merge 8 commits into
mainfrom
claude/evalstats-vis-node-integration-0c75d8

Conversation

@ianarawjo

@ianarawjo ianarawjo commented Sep 14, 2026

Copy link
Copy Markdown
Owner

What it does

When ChainForge runs locally with pip install chainforge[stats] (evalstats 0.3.2 or newer), a Stats switch in the Vis Node's header shows evalstats results under the plot:

  • Executive summary: rank band, mean with 95% CI, and a verdict from evalstats' rank bands (Highest / Tied for highest / Significantly lower) for each group. The wording is direction-neutral, since a higher score isn't always better.
  • Pairwise differences: collapsible, with difference, CI and p-value. Pairs evalstats marks significant are bold.
  • Methods and notes from evalstats: collapsed. Built from methods(): design, score type, CI methods, simultaneous CIs, the p-value test and correction, the Friedman test, the rank-band rule, resampling, and the evalstats version.
  • Error bars on the plot: on true/false accuracy bars, grouped bars, box plots (which also show their means), and numeric bars.

It always uses evalstats' paired design. Items without a score for every group and run are dropped with evalstats.complete_items() and listed, with a prompt to rerun those queries. Below evalstats.MIN_ITEMS (15), no statistics are shown.

Changes

  • chainforge/stats.py: builds the evalstats input, then relies on evalstats' public API:

    • complete_items() drops incomplete items;
    • to_dict() supplies rank bands, verdicts, significance and p-values;
    • methods() supplies the Methods list, and result.notes the notes;
    • the typed errors (InsufficientItemsError, MissingCellsError, TooFewGroupsError, AmbiguousLabelsError) become messages.

    No private evalstats helpers are imported.

  • flask_app.py: /app/compareEvalStats and /app/checkEvalStatsAvailable, gated on the extra like RAG, plus window.__EVALSTATS_AVAILABLE.

  • setup.py / CI / README: the [stats] extra (evalstats>=0.3.2).

  • backend/evalStats.ts: pairs results by input (vars, metavars, LLM). It leaves out values that only describe one group, such as an upstream LLM's answer text.

  • VisStatsPanel.tsx: the panel.

  • VisNode.tsx:

    • numeric bar charts show each group's mean with n on hover, with or without Stats (they used to sum scores, so a per-response metric like response length read as a total);
    • stacked true/false charts become grouped bars;
    • categorical scores become 100%-stacked bars (the share of each group's responses in each category, with count and n on hover), replacing counts titled "Number of 'true' values"; LLM × variable plots use a two-level axis;
    • the graph type menu is disabled for true/false and categorical scores, which are always bars;
    • the graph type menu and Stats switch move to the header, after the AI plot buttons, and are hidden while an AI plot shows; the graph type is saved with the node;
    • the toolbar is compact and no longer sets the node's width;
    • the dark-mode grid is dimmer and the legend matches the axis labels;
    • plots now redraw on resize in development. StrictMode reran the effect cleanup that disconnected the resize observer, without rerunning the ref that created it.

Testing

  • Python: 37 tests in tests/test_stats.py, run against evalstats 0.3.2.
    • They cover completeness and the 15-item floor, groups with no results, verdicts and bands, and the methods() lines.
    • They cover model × prompt names containing " / " and names that collide.
    • They check that each typed error becomes a message, and that the paired design is always used.
  • Frontend: 11 jest tests for pairing, and the full frontend suite (1011 tests); tsc passes.
  • Battle test, statistics: 78 generated datasets went through compare_eval_results, a few also through the Flask route.
    • Data covered: true/false, 0/1, continuous, Likert, unbounded, huge and tiny scales, constant and identical groups, junk scores (strings, None, NaN, inf), 2–10 groups, 15–1000 items, 1–5 runs, model × prompt grids, random and structured missing results, unicode, 500-character, " / ", numeric-looking and empty names, and duplicates.
    • Every result was checked for strict JSON, means inside CIs, non-decreasing bands, verdicts matching bands, pair counts and indices, significant agreeing with the CIs, and each difference equalling the gap between means. None failed.
    • 30,000 rows take about 1 s, and concurrent requests keep their own notes.
  • Battle test, pairing: 16 ChainForge-shaped scenarios, including chained flows, per-response ids, tabular metavariables, prompt variants, image variables, missing variables, and 60,000 rows (about 140 ms).
    • This found chained results failing to pair when upstream answers repeat across LLMs, and questions that differed per LLM being merged. Both are fixed.
  • In the dev app, with evalstats 0.3.2: HumanEval and sycophancy examples, plus eight edge-case flows. The edge cases were all-true scores, negative numbers, dict-valued scores (both keys), categorical scores, three runs with errors, only 10 items, eight LLMs with long, unicode and " / " names, and LLM × variable with one combination never run. Checked in each:
    • verdicts, bold pairs, the Methods list, notes and messages;
    • error bars aligned on each plot type;
    • the graph type surviving a reload;
    • resizing the node narrower.

Remaining

  • The response inspector's plot hasn't been checked in the browser. It has no header, so its graph type menu stays in its toolbar and it has no Stats switch.

🤖 Generated with Claude Code

@ianarawjo ianarawjo changed the title [WIP] Vis Node statistics with evalstats Vis Node statistics with evalstats Sep 15, 2026
ianarawjo and others added 7 commits September 17, 2026 11:46
When ChainForge runs locally with `pip install chainforge[stats]`, a Stats
switch in the Vis Node header shows evalstats' analysis under the plot:
per-group means with confidence intervals, rank bands with verdicts (tied
for best / significant drop-off), collapsible pairwise differences, and the
methods evalstats ran. Intervals are drawn on accuracy bars and box plots.

- chainforge/stats.py: always evalstats' paired design. Items without a
  score for every group and run are excluded up front and reported; no
  statistics below 15 items.
- flask_app.py: /app/compareEvalStats and /app/checkEvalStatsAvailable,
  gated on the extra like RAG; window.__EVALSTATS_AVAILABLE.
- evalStats.ts: pairs results by input (vars, metavars, LLM), leaving out
  values that only describe one group, such as upstream responses.
- VisNode: stacked true/false charts become grouped bars, so each bar can
  carry an interval; box plots show means when intervals are drawn; the
  dark-mode grid is dimmer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- The toolbar no longer sets the node's width: it wraps, and its selects are
  smaller and capped, so the node can be resized much narrower.
- The graph type menu moves into the node header beside the Stats switch,
  both aligned with the close button. The choice is saved with the node.
- With Stats on, numeric bar charts show means (not sums), so each bar can
  carry its confidence interval; errors are left out of the means.
- Legend text matches the axis labels in dark mode.
- Plots redraw when resized in development: StrictMode reran the effect
  that disconnected the resize observer without rerunning the ref that
  created it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
evalstats 0.3.2 exposes what the Vis Node shows, so ChainForge no longer
imports its private helpers or copies its rules:

- Incomplete items are dropped with evalstats.complete_items(), and its
  typed errors (InsufficientItemsError, MissingCellsError,
  TooFewGroupsError, AmbiguousLabelsError) become messages instead of
  matching on error text. The 15-item floor is evalstats.MIN_ITEMS.
- Rank bands, verdicts, per-pair significance and p-values come from
  to_dict(); the panel shows evalstats' verdicts and bolds pairs it marks
  significant.
- The Methods list is formatted from methods(), now including resampling.
- Notes come from result.notes. evalstats gets the real group names, since
  two-factor levels are now structured, so the id mapping and the rewriting
  of note text are gone.
- Requires evalstats>=0.3.2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…sults

Battle-testing the statistics against a wide range of eval-like data turned
up two problems:

- Pairing results by input kept any value that ever occurred in more than
  one group. An upstream LLM's answer passed on to a judge prompt is often
  the same across LLMs ("Paris", "yes"), so those answers split items apart
  and most were left out; and a design where each LLM answered different
  questions merged into one item, reported as "identical inputs". Items now
  keep only the dimensions needed to tell the (input, group) pairs apart:
  dropping an answer or a response id leaves them all distinct, dropping a
  real input does not.
- When a group, or a combination such as one LLM never run on one value of a
  variable, has no scores at all, the panel said only that 0 items had
  results for every group. It now names the groups with none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
In the "Compare prompt across models" example, each prompt's samples are
stored as separate responses with identical inputs. Stats rejected them with
a red "identical inputs" error, which read like a failure rather than a
limit of the data.

- Responses for the same input in the same group now count as further runs
  of that input.
- The too-few message counts inputs, and says repeated responses to one
  input count once.
- When prompt variables are compared, the panel explains that their values
  aren't counted as inputs, and how to use them as inputs instead.
- Messages that don't produce statistics carry a reason code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Likely best" assumes a higher score is better, which doesn't hold for
metrics like a bias rate. Verdicts now read "Highest", "Tied for highest"
and "Significantly lower".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Numeric bar charts summed scores unless Stats was on, so on the web, or
without chainforge[stats], a per-response metric like response length read
as the total across all responses. Bars now always show each group's mean,
under a "Mean of scores" axis, with the mean and n on hover. Results that
aren't scores are left out of both. True/false accuracy bars (a proportion)
and categorical counts are unchanged.

Also moves the new stats UI onto the type scale tokens (--fs-xs, --fs-sm).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ianarawjo
ianarawjo force-pushed the claude/evalstats-vis-node-integration-0c75d8 branch from 82d1f00 to 26c7acb Compare September 17, 2026 15:52
Categorical scores were drawn as counts under the axis title "Number of
'true' values", which only fit binary data (and binary data never reached
that chart). They are now 100%-stacked bars: each group, or each value of a
variable and LLM on a two-level axis, split into the share of its responses
in each category, under "% of responses", with the count and n on hover.
Percentages keep groups with different numbers of responses comparable, as
percent-true bars already do.

The graph type menu is now disabled for true/false and categorical scores,
which are always plotted as bars; before, categorical scores left it
enabled with no effect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant