Programming for data science

JavaScript and TypeScript for Data Scientists: Model Diagnostics

Turn reviewed Python predictions and an independent R audit into a validated JSON contract and an accessible, progressively enhanced diagnostic report.

2026 edition · Reproduced 15 August 2026

On this page

Reproduction contract

What this guide lets you reproduce

The workflow, data snapshot and checks below are versioned together. Use the stated commands and compare your results with the tested outputs in the repository.

Before you begin

  • Ability to read modern JavaScript, TypeScript types and command-line output
  • Understanding of paired prediction errors, holdout evaluation and grouped diagnostics
  • Completion of the Python and R guides or equivalent predictions.csv, r_audit.csv and r_diagnostics.csv artefacts

You will be able to

  • Validate untrusted CSV values at runtime instead of mistaking TypeScript annotations for input validation
  • Independently recalculate paired holdout metrics and hour- and month-level diagnostic groups in JavaScript
  • Compare the browser-reporting layer with an independently produced R audit under explicit tolerances
  • Publish one deterministic JSON contract as static, accessible HTML with optional interactive SVG enhancement
  • Test analytical invariants, fallback content, keyboard controls and narrow-viewport behaviour in an isolated Node package

Tested environment

Pinned dataset

UCI Bike Sharing — hour.csv, LF-normalised repository copy

Historical 2011–2012 Washington, DC rentals; no station capacity, bicycle availability, rebalancing, price or reliable basis for present-day or causal claims. The UCI page reports ten more instances than the archived hourly file contains.

Runnable companion

Review and disclosure

AI assistance helped consolidate legacy JavaScript source 2997 and TypeScript source 3016 into this reporting workflow. Source 2997 remains the primary provenance record; source 3016 is retained as merged editorial input in the migration ledger. Clean-environment reproduction was verified in the linked GitHub Actions run on 15 August 2026, followed by human editorial review; no causal, ranking or deployment-performance claim is made.

The last mile of a model workflow is still data science. A browser chart can silently change a denominator, drop an invalid row or detach a result from its uncertainty just as easily as modelling code can leak a target. This extension begins only after the Python workflow has fixed the model and the R workflow has independently audited its holdout predictions. JavaScript receives evidence; it does not reopen model selection.

The task is to turn three tabular artefacts into two durable outputs:

  • javascript_diagnostics.json, a machine-readable reporting contract; and
  • javascript_diagnostics.html, a self-contained report that remains complete when browser JavaScript is unavailable.

TypeScript checks the implementation at build time. Runtime code separately checks the values crossing the CSV boundary. Observable Plot adds an interactive SVG only after the static report is already usable.

Start from the analytical handover

The reporting package consumes:

InputProducerReporting responsibility
predictions.csvPythonRecalculate test errors from actual, baseline and candidate predictions
r_audit.csvBase RConfirm the same point estimates and retain the day-block bootstrap interval
r_diagnostics.csvBase RCheck every hourly and monthly group against an independent implementation

predictions.csv contains 2,208 validation rows followed by 2,168 test rows. The extension calculates published diagnostics only from the test rows. Validation predictions remain in the handover so the package can reject a changed split boundary or row count.

The underlying observations are historical 2011–2012 bicycle rentals from Washington, DC. They are useful for a stable reporting exercise but cannot establish current transport performance, unmet demand, causal effects or transferability to another city.

This separation matters. Importing the fitted Python model into Node would make the report depend on training internals and would not independently check the metric calculation. Consuming row-level predictions lets another implementation reconstruct the claim while keeping model choice fixed.

Install an isolated, exact toolchain

The website itself uses a different TypeScript version. The example therefore owns a nested package.json, .nvmrc and lockfile instead of changing the Astro application. The baseline uses the official Node.js 24.19.0 release, TypeScript 7.0.2 and Observable Plot 0.6.17.

Run the core SQL → Python → R chain first. Then install and test the extension:

cd examples/programming-for-data-science/javascript-typescript
npm ci
npx playwright install chromium
npm test

npm ci treats package-lock.json as the installation contract. A dependency upgrade belongs in a separate review: regenerate the lock, rerun the analytical checks, inspect the HTML and compare browser behaviour. Exact pins make a change diagnosable; they are not an argument for leaving dependencies unmaintained.

Types do not validate CSV data

A TypeScript interface describes what the program is allowed to assume after validation. It does not transform arbitrary CSV text into trustworthy PredictionRow values. A cast such as record as PredictionRow would only silence the compiler.

The parser first requires this exact ordered schema:

const PREDICTION_COLUMNS = [
  'instant',
  'observed_at',
  'observation_date',
  'split',
  'actual',
  'baseline_prediction',
  'model_prediction',
] as const;

const table = parseCsv(source);
assertExactColumns(table.columns, PREDICTION_COLUMNS, 'predictions.csv');

Each record is then narrowed at runtime. The implementation rejects empty or non-finite numbers, malformed timestamps, an observation date that disagrees with its timestamp, negative counts, unknown split labels, duplicate identifiers and duplicate timestamps. It checks the two split counts, pins validation to 1 July–30 September 2012 and test to 1 October–31 December 2012, and requires every validation timestamp to precede the test period.

Only after those gates does the function return PredictionRow[]. This pattern is useful for API responses, message queues and uploaded files as well as CSV: validate at the system boundary, then let TypeScript preserve the established contract inside the program.

Recalculate the paired evidence

For each test row, JavaScript constructs signed errors for the development-only baseline and selected model. It independently calculates MAE, RMSE and mean signed model error:

const baselineErrors = test.map(
  (row) => row.baselinePrediction - row.actual,
);
const modelErrors = test.map(
  (row) => row.modelPrediction - row.actual,
);

const baselineMae = mean(baselineErrors.map(Math.abs));
const modelMae = mean(modelErrors.map(Math.abs));
const modelRmse = Math.sqrt(mean(modelErrors.map((error) => error ** 2)));
const pairedDifference = mean(
  test.map(
    (row) =>
      Math.abs(row.modelPrediction - row.actual) -
      Math.abs(row.baselinePrediction - row.actual),
  ),
);

The paired difference retains the fact that both predictors were scored against the same observed hour. A negative value means that the candidate had lower absolute error on average. The R-generated day-block interval is carried into the report rather than reimplemented with a second random-number generator, but its point estimate must match JavaScript and the interval must contain that estimate with an upper bound below zero.

The expected historical test checkpoints are:

DiagnosticValue
Test rows2,168
Test days92
Baseline MAE79.049
Model MAE49.894
Model RMSE78.457
Model mean signed error−0.927
Model minus baseline absolute error−29.155

The extension also groups the test rows independently by hour of day and calendar month. It requires 24 hourly groups, three monthly groups and exact holdout coverage in each view. Every row count, baseline MAE, model MAE and model signed error must agree with r_diagnostics.csv within 1e-9 rentals. A chart is never allowed to become the first place a grouped calculation occurs.

Make JSON the reporting contract

javascript_diagnostics.json contains a schema version, runtime versions, durable UCI creator/source/DOI/licence attribution, SHA-256 digests and row counts for all three inputs, holdout metrics, the R interval, grouped values and interpretation limits. The generator does not add a build timestamp, so unchanged inputs and code produce stable content.

This JSON is the handover to any later report, dashboard or API. Consumers should branch on schemaVersion and fail on an unsupported version rather than guessing what a renamed field means. The three input digests make it possible to distinguish a rendering change from a changed analytical source.

Do not expose raw records merely because a browser can display them. This teaching dataset is public and aggregated, but a real prediction export may contain identifiers or sensitive attributes. Build a deliberately minimised reporting contract on the server side, apply access controls there and send only the fields needed for the approved view.

Render a complete page before enhancement

The generated HTML contains the result, methodology and limitations in crawlable semantic markup. Before any client script runs, it already has:

  • a skip link, one main landmark and a logical heading structure;
  • four summary metrics in a description list;
  • static inline SVG charts with <title> and <desc> elements;
  • dashed versus solid lines and different point shapes, so colour is not the only encoding;
  • full hourly and monthly tables with captions and row and column headers; and
  • focusable scroll regions for wide tables, reduced-motion styling and print rules.

The static view is the fallback, the printable record and the evidence available to non-scripted clients. It is not a screenshot. Text and table values remain selectable and machine-readable.

When the bundled script loads, it parses the embedded JSON and replaces only the chart container with an Observable Plot SVG. Controls are revealed after successful enhancement and let a reader switch between absolute error and signed model error. A polite live region reports the change. The complete table stays visible, so hovering or precise pointer movement is never required to retrieve a value.

The JSON is escaped before it enters the document, and the generated script is bundled locally rather than loaded from a third-party content network. For a production application, add a restrictive Content Security Policy and avoid inline script where operational constraints permit; a self-contained teaching artefact optimises for offline review.

Treat tests as analytical review gates

npm test runs four distinct layers:

  1. TypeScript checks the strict source contract and compiles the Node modules.
  2. esbuild produces the browser bundle from the same locked dependency graph.
  3. Node tests verify the golden metrics, exact split dates, finite parity inputs, group coverage, changed-schema rejection, disagreement rejection and complete static output.
  4. Playwright opens the generated file in Chromium, checks progressive enhancement and accessible names, changes a chart with its radio control, and tests a narrow viewport for page-level overflow.

A browser test cannot certify conformance with every accessibility requirement, and a numerical tolerance cannot decide whether a changed result is substantively acceptable. Both are review gates: they catch known failure modes and leave the consequential judgement explicit.

Interpret the report without enlarging the claim

The candidate’s MAE is about 29.16 rentals lower per recorded hour than the calendar baseline in the October–December 2012 holdout. Hourly and monthly views help locate where that average comes from, while signed error can reveal recurring underprediction or overprediction that an absolute metric hides.

None of those views makes the historical sample current, establishes that a predictor caused demand or proves transportability to another city. The report does not include station capacity, bicycle availability, rebalancing or unmet trips. Its purpose is narrower and useful: preserve a reviewed predictive comparison as it moves from analytical code into a form that people can inspect in a browser.

For a real deployment, extend the contract with model and dataset version identifiers, prediction-generation time, subgroup definitions, drift reference windows and named owners for thresholds and response actions. Keep the same discipline: validate the handover, compute each displayed diagnostic from an explicit definition, preserve non-interactive evidence and test the path that a reader actually receives.

Opens in a new tab.