Figura ← Back to the app

Published evidence

Statistical validation

On eight fixed test datasets, the statistics Figura reports are re-derived from the same raw CSV by a second implementation, written separately from the one that ships. What each comparison covers, and what it leaves out, is set out case by case in what was checked, and what was not — the claim is about those eight files, not about a file you upload. This page is that comparison, including every difference it finds.

What was actually done, stated precisely. One specification — prose, transcribed from Figura's R sources by an agent with full source access — was implemented a second time in Python by agents that never read the R, against an acceptance suite whose expected values were computed in R. It is a re-implementation from a written spec, not two blind implementations of a shared problem statement.

That catches implementation bugs, library-default mismatches and arithmetic errors: the failure modes where a second author, writing independent code from a written rule, lands somewhere different. Those are the numerous, ordinary, expensive mistakes.

It cannot catch a misreading baked into the specification itself. If the spec described R's behaviour wrongly, both implementations reproduce the same wrong thing and agree with each other. The specifications cite the R sources line by line, they are in the repository at stats-validation/spec/, and they are the thing to attack if you want to attack this.

The full disclosure — everything the clean-room bundle contained, and two episodes where information leaked into it, named by commit hash — is in stats-validation/README.md. Nothing here is a claim of independent certification, accreditation or regulatory clearance: Figura is not a medical device, and this is a comparison you can re-run, not a validation report in the regulatory sense.

331 values compared
0 differences found
0 of 8 cases with differences
8 of 8 cases with declared coverage complete

Every compared value matched. That is the whole of what these cases checked — read what was checked before reading it as more.

What was checked, and what was not

Each case below is a real CSV run end to end through the shipped app — the same CSV parser, the same analysis, the same rendering — and then again through the independent implementation. Three comparisons run on each: the numbers as displayed, the underlying quantities at full precision, and the .R script the app offers for download, re-run in R.

AnalysisWhat is comparedWhat is not
Summary statistics (Table 1)
1 casesummary-table1
  • At full precision (agreement to a relative 1e-06) — the number of patients analysed, the number of rows dropped.
  • As displayed — every displayed Table 1 cell, the missing-value counts and the row order, compared character for character.
  • The downloaded script — the cells and the mean-vs-median choice the exported .R produces when it is re-run in R, compared against the screen.
  • The choice of statistic — not just the number but which one: the mean-vs-median choice for each variable.
the prose wrapped around the table; the rendered distribution plots
Kaplan-Meier
1 casekm-twoarm
  • At full precision (agreement to a relative 1e-06) — median survival in each group, the log-rank p-value, every step of the survival curve, the number of patients analysed, the number of events, the number of rows dropped.
  • As displayed — the median-survival and log-rank clauses of the sentence the app displays, compared character for character.
  • The downloaded script — the medians and the log-rank p the exported .R produces when it is re-run in R, compared against the screen.
the hazard-ratio clause of the displayed sentence; the rendered curve image (the curve itself is compared as coordinates)
Group comparison
3 casesgroupcompare-numericgroupcompare-categoricalgroupcompare-dirty
  • At full precision (agreement to a relative 1e-06) — the test's p-value, the test statistic, the number of patients analysed, the number of rows dropped.
  • As displayed — the displayed test name, p-value and effect size, the per-group counts, and the full set of post-hoc pairs, compared character for character.
  • The downloaded script — the p-value the exported .R produces when it is re-run in R, compared against the screen.
the prose wrapped around the numbers; the rendered box or bar plot
Cox regression
1 casecox-adjusted
  • At full precision (agreement to a relative 1e-06) — the adjusted hazard ratio, its standard error and 95% confidence interval, its p-value, the number of patients analysed, the number of events, the number of rows dropped, the global proportional-hazards test.
  • As displayed — every cell of the rendered ratio table — unadjusted and adjusted, estimate, 95% CI and p-value — exactly as the app prints it, compared character for character.
  • The downloaded script — the adjusted cells the exported .R produces when it is re-run in R, rendered through the app's own display rule, compared against the screen.
  • Advisory sentences — whether each one fires at all: the proportional-hazards advisory, the events-per-variable advisory, the separation caution.
the unadjusted column at full precision (it is compared as displayed — only the joint model is harvested from the exported script); the prose wrapped around the numbers; the rendered forest plot, which draws the same adjusted estimates
Logistic regression
2 caseslogistic-confoundinglogistic-dirty
  • At full precision (agreement to a relative 1e-06) — the adjusted odds ratio, its standard error and 95% confidence interval, its p-value, the number of patients analysed, the number of events, the number of rows dropped, the C-statistic.
  • As displayed — every cell of the rendered ratio table — unadjusted and adjusted, estimate, 95% CI and p-value — exactly as the app prints it, compared character for character.
  • The downloaded script — the adjusted cells the exported .R produces when it is re-run in R, rendered through the app's own display rule, compared against the screen.
  • Advisory sentences — whether each one fires at all: the collinearity (VIF) advisory, the events-per-variable advisory, the influential-observations advisory, the separation caution.
the unadjusted column at full precision (it is compared as displayed — only the joint model is harvested from the exported script); the prose wrapped around the numbers; the rendered forest plot, which draws the same adjusted estimates

Two boundaries this table does not draw on its own. Explore, the plot builder, has no case here: it reports no statistics of its own, only a figure. And the whole table above is native R — the runtime that actually runs in your browser is checked separately, and how much of the roster that check reaches is stated where it is reported; see webR against native R.

Case by case

CaseAnalysisValues comparedDeclared coverageResult
logistic-confoundingLogistic regression44completeno differences
cox-adjustedCox regression28completeno differences
km-twoarmKaplan-Meier106completeno differences
groupcompare-numericGroup comparison14completeno differences
groupcompare-categoricalGroup comparison12completeno differences
groupcompare-dirtyGroup comparison14completeno differences
logistic-dirtyLogistic regression52completeno differences
summary-table1Summary statistics (Table 1)61completeno differences

The differences, in plain language

No case currently publishes a difference. That is not the same as "nothing can be wrong": read what was checked for the boundary of the claim, and the limits stated at the top of this page for what a specification-based re-implementation cannot catch in principle.

A validation page that only ever shows green is not evidence. When this harness finds something, it is published here, in this section, before it is fixed.

The browser runtime: webR against native R

Everything above ran native R on a developer machine. You do not run native R — you run R compiled to WebAssembly, in your own browser tab. wasm has no 80-bit extended precision, and webR ships reference linear-algebra libraries rather than the platform's tuned ones, so an iteratively fitted model — Cox's Newton-Raphson, logistic regression's IRLS — is where a difference would show up if there were one.

A hand-run gate drives the real interface in a real browser (upload the file, map the columns, confirm the event value, set the reference levels, tick the variables, render) and compares every string the app displays against native R's output: the tables cell by cell, and the analyses that report a sentence rather than a table sentence by sentence. It is run before a release rather than on every change, because it needs a browser and the network.

Runtime webR 0.6.1-dev+7603db7 (R 4.6.0), run 2026-07-28, against commit 103c90bc9600. read from the WebR instance's own version fields, from the module the app loads (https://webr.r-wasm.org/latest/webr.mjs); the page itself prints no version string. Runtime binaries served from 17 webr.r-wasm.org request(s) during this run.

Coverage: 8 of 8 cases. Every case on this page was also run through the real browser interface and checked against native R — the tables cell by cell, and the analyses that report a sentence rather than a table (Kaplan–Meier, group comparison) sentence by sentence.

Every one of the 113 strings the app displays across these cases was compared — 58 of them carrying a number that could actually move between native R and WebAssembly. The other 55 strings cannot drift: 47 are column headers, row labels and deliberately blank cells, and 8 are displayed sentences that carry no number.

113 = 41 table value cells + 25 displayed sentences (17 containing a number) + 25 row labels + 4 table header line(s) + 18 empty-vs-empty placeholder cells (blank by construction — a categorical variable's own header row carries no value). Only the 58 numeric cells (the table value cells plus the number-bearing sentences) can actually show wasm-vs-native drift — the row labels, header lines, and empty cells are static or intentionally blank.

CaseResultStrings comparedDetail
logistic-confoundingidentical23every displayed string matched native R exactly
logistic-dirtyidentical26every displayed string matched native R exactly
cox-adjustedidentical13every displayed string matched native R exactly
km-twoarmidentical2every displayed string matched native R exactly
groupcompare-numericidentical3every displayed string matched native R exactly
groupcompare-categoricalidentical1every displayed string matched native R exactly
groupcompare-dirtyidentical4every displayed string matched native R exactly
summary-table1identical41every displayed string matched native R exactly

How to check this yourself

You do not have to take this page's word for any of it.

A standing note on that first step, true whether or not this page is publishing a difference today. Figura parses your CSV in the browser and never calls R's read.csv, so the downloaded script is generated to read the file the same way the app read it — the same trimming, the same treatment of a blank cell and of the two letters NA typed as text. That is a parity that has to be maintained rather than a property that holds by construction, which is why it is checked here on all eight datasets. One narrow divergence is known and still open: a Kaplan–Meier script recodes a numeric status column slightly more permissively than the app does, and no case on this page exercises it. It is written up in stats-validation/issues/02-app-vs-exported-script-missing-values.md.

How this page is produced

This page is generated from stats-validation/results/findings.json — the comparator's own output — by stats-validation/build_scorecard.py --web. It performs no computation of its own and reads nothing but files in the repository, so the same commit always renders the same page. Continuous integration regenerates it on every push and fails the build if the published page is not what the current evidence renders, which is what stops it from quietly ageing into a claim nobody re-checked.

The evidence it is built from is in the repository beside it: the raw cases (stats-validation/cases/), the written specifications (stats-validation/spec/), the independent implementation (stats-validation/python/) with each module's record of the ambiguities its author hit, the comparator (stats-validation/compare/), and the results (stats-validation/results/).