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.
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
- The differences, in plain language
- The browser runtime: webR against native R
- How to check this yourself
- How this page is produced
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.
| Analysis | What is compared | What is not |
|---|---|---|
| Summary statistics (Table 1) 1 casesummary-table1 |
| the prose wrapped around the table; the rendered distribution plots |
| Kaplan-Meier 1 casekm-twoarm |
| 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 |
| the prose wrapped around the numbers; the rendered box or bar plot |
| Cox regression 1 casecox-adjusted |
| 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 |
| 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
| Case | Analysis | Values compared | Declared coverage | Result |
|---|---|---|---|---|
| logistic-confounding | Logistic regression | 44 | complete | no differences |
| cox-adjusted | Cox regression | 28 | complete | no differences |
| km-twoarm | Kaplan-Meier | 106 | complete | no differences |
| groupcompare-numeric | Group comparison | 14 | complete | no differences |
| groupcompare-categorical | Group comparison | 12 | complete | no differences |
| groupcompare-dirty | Group comparison | 14 | complete | no differences |
| logistic-dirty | Logistic regression | 52 | complete | no differences |
| summary-table1 | Summary statistics (Table 1) | 61 | complete | no 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.
| Case | Result | Strings compared | Detail |
|---|---|---|---|
| logistic-confounding | identical | 23 | every displayed string matched native R exactly |
| logistic-dirty | identical | 26 | every displayed string matched native R exactly |
| cox-adjusted | identical | 13 | every displayed string matched native R exactly |
| km-twoarm | identical | 2 | every displayed string matched native R exactly |
| groupcompare-numeric | identical | 3 | every displayed string matched native R exactly |
| groupcompare-categorical | identical | 1 | every displayed string matched native R exactly |
| groupcompare-dirty | identical | 4 | every displayed string matched native R exactly |
| summary-table1 | identical | 41 | every 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.
- Re-run your own analysis in R. Run it in Figura, then press
.Rin the Console pane's toolbar. Figura downloads the exact script it just ran — the statistical calls are the expressions the app evaluated, not a rewrite of them. Run that script in your own R, or hand it to your statistician, and compare it against what the app showed you. - Re-run this whole comparison. From a checkout of the repository,
make -C stats-validation allre-derives every number on this page from the raw CSVs and rewrites the evidence files. It needs R, Python and Node.
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/).