This website is meant to be read and understood quickly by humans, but is only fully parsable, on a technical level, with the aid of an AI system. Read why →
Loop MMT
One Running-Statistics Record Over a Numeric Streamfold← all gifts

Running-stats

running-stats reads a stream of numbers — one JSON number per line (JSON Lines) — and folds them in ONE PASS into a single aggregate record: {count, mean, variance, stddev, min, max}, with zero dependencies. mean and variance are computed by Welford's online algorithm, which stays numerically stable where the textbook one-pass shortcut (sum of squares minus square of sum) catastrophically cancels — a tight cluster around a huge mean. Population variance by default; --sample gives the Bessel-corrected (n-1) sample statistics. The record is a pure function of the input SEQUENCE, so the same stream folds to a byte-identical record every run; but mean/variance are IEEE-754 doubles, so reordering the same values can move the low-order bits — it is deterministic for a given input order, NOT an exact order-independent fingerprint (that is histogram-fold's job). Same stream in → byte-identical record out, in Node or a browser.

The honest edge
running-stats is a one-pass SUMMARY {count,mean,variance,stddev,min,max} — NOT a median/percentile/quantile summary (it keeps no order statistics beyond min and max), and NOT a mode or histogram (see histogram-fold). Its mean/variance are floats: deterministic for a given input order, reorder-agreement within tolerance, NOT an exact multiset fingerprint. Welford's honest guarantee is bounded, non-catastrophic ONE-PASS error — strictly better than the naive sum-of-squares shortcut, NOT a claim to beat a careful two-pass.
Run it
printf '%s\n' 2 4 4 4 5 5 7 9 | node running-stats.js # {count,mean,variance,stddev,min,max}; --sample (n-1); exit 0 ok | 2 input error test_running-stats.js (34/34: frozen hand goldens ([2,4,4,4,5,5,7,9] → mean 5 / popvar 4 / stddev 2; sample variance 32/7) cross-checked by an INDEPENDENT two-pass oracle — a genuinely different method than Welford; numerical stability (Welford tracks truth on the 1e9 cluster while the naive shortcut cancels) with the mutation bite; reorder-agreement within tolerance; numeric-honesty hard errors (NaN/1e999→Infinity/string/bad-JSON); variance ≥ 0 clamp) + out-of-band conformance conform_running-stats.cjs (20/20 GREEN, subprocess-driven, signed a2c317a082332c83) Zero dependencies, Node or browser, deterministic
The code — every file that ships
running-stats.js193 lineson GitHub →
#!/usr/bin/env node
/**
 * running-stats — fold a JSONL numeric stream into ONE running-statistics record.
 *
 * WHAT
 *   Reads a stream of numbers — one JSON number per line (JSON Lines) — and folds
 *   them in ONE PASS into a single aggregate record:
 *       { count, mean, variance, stddev, min, max }
 *   mean and variance are computed by WELFORD'S online algorithm, which is
 *   numerically stable where the textbook one-pass shortcut (sum of squares minus
 *   square of sum) catastrophically cancels — e.g. a tight cluster around a huge
 *   mean like 1e9. By default `variance`/`stddev` are POPULATION (divide by n);
 *   with --sample they are the Bessel-corrected SAMPLE statistics (divide by n-1).
 *
 * HOW
 *   Welford: for each x:  n += 1;  d = x - mean;  mean += d / n;  M2 += d * (x - mean).
 *   Then population variance = M2 / n, sample variance = M2 / (n - 1).
 *   The record is a PURE function of the input sequence — no clock, no randomness,
 *   no files written — so the same stream folds to a byte-identical record every
 *   run. NOTE: mean and variance are IEEE-754 doubles, so reordering the SAME values
 *   can change the low-order bits (float addition is not associative). running-stats
 *   is a summary statistic, deterministic for a given input order — NOT an exact
 *   multiset fingerprint (that is histogram-fold's job, with integer counts).
 *
 * USAGE
 *   node running-stats.js [--sample] [FILE]      # stdin if no FILE
 *   printf '%s\n' 2 4 4 4 5 5 7 9 | node running-stats.js
 *     -> {"count":8,"mean":5,"variance":4,"stddev":2,"min":2,"max":9}
 *   --sample   sample (n-1) variance/stddev instead of population (n)
 *   --help
 *
 * EXIT CODES
 *   0  success
 *   2  input error: missing file / a directory / a line that is not a finite JSON
 *      number (incl. a value that overflows to +/-Infinity, e.g. 1e999) or NaN /
 *      --sample with fewer than 2 values (sample variance is undefined). Always a
 *      clean one-line message on stderr, never a stack trace.
 *
 * EDGE (what this is NOT)
 *   NOT a median/percentile summary (keeps no order statistics beyond min & max),
 *   NOT a mode or histogram (see histogram-fold), and its mean/variance are floats:
 *   deterministic for a given input order, but reordering can move the low-order bits.
 *
 * Zero dependencies. Node builtin `require('fs')` for file reads only; runs in a
 * browser with no require (attaches `runningStats` to window.ForestGifts). MIT.
 */
"use strict";

/**
 * Fold a JSONL numeric stream into a running-statistics record.
 * @param {string} text  the whole input (newline-separated JSON numbers)
 * @param {{sample?: boolean}} [opts]
 * @returns {{count:number, mean:number, variance:number|null, stddev:number|null, min:number|null, max:number|null}}
 * @throws {Error} on a non-finite / non-number line, or --sample with count < 2.
 */
function fold(text, opts) {
  opts = opts || {};
  var sample = !!opts.sample;
  var n = 0;
  var mean = 0;
  var M2 = 0;
  var min = null;
  var max = null;

  var lines = String(text).split("\n");
  for (var i = 0; i < lines.length; i++) {
    var raw = lines[i];
    if (raw.charCodeAt(raw.length - 1) === 13) raw = raw.slice(0, -1); // trim trailing \r (CRLF)
    if (raw.length === 0) continue; // blank line: skipped, not a value
    var v;
    try {
      v = JSON.parse(raw);
    } catch (e) {
      throw new Error("line " + (i + 1) + ": not valid JSON: " + raw);
    }
    if (typeof v !== "number") {
      throw new Error("line " + (i + 1) + ": not a number: " + raw);
    }
    if (!isFinite(v)) {
      // catches NaN and +/-Infinity (incl. 1e999, which JSON.parse yields as Infinity)
      throw new Error("line " + (i + 1) + ": not a finite number: " + raw);
    }
    // Welford online update
    n += 1;
    var delta = v - mean;
    mean += delta / n;
    var delta2 = v - mean;
    M2 += delta * delta2;
    if (min === null || v < min) min = v;
    if (max === null || v > max) max = v;
  }

  if (n === 0) {
    return { count: 0, mean: null, variance: null, stddev: null, min: null, max: null };
  }

  var variance, stddev;
  if (sample) {
    if (n < 2) {
      throw new Error("--sample: need at least 2 values for a sample variance (got " + n + ")");
    }
    variance = M2 / (n - 1);
  } else {
    variance = M2 / n;
  }
  // M2 is a sum of squares -> variance is >= 0 mathematically; clamp a tiny negative
  // float artifact (e.g. -1e-13 from cancellation) to exactly 0 so stddev is real.
  if (variance < 0) variance = 0;
  stddev = Math.sqrt(variance);

  return { count: n, mean: mean, variance: variance, stddev: stddev, min: min, max: max };
}

/* ------------------------------------------------------------------ exports */
if (typeof module !== "undefined" && module.exports) {
  module.exports = { fold: fold };
}
if (typeof window !== "undefined") {
  window.ForestGifts = window.ForestGifts || {};
  window.ForestGifts.runningStats = fold;
}

/* ----------------------------------------------------------------- CLI main */
function main(argv) {
  var args = argv.slice(2);
  var sample = false;
  var file = null;
  for (var i = 0; i < args.length; i++) {
    var a = args[i];
    if (a === "--help" || a === "-h") {
      process.stdout.write(
        "usage: running-stats.js [--sample] [FILE]\n" +
          "  (no FILE)   read numbers from stdin\n" +
          "  FILE        read numbers from a file\n" +
          "  --sample    sample (n-1) variance/stddev instead of population (n)\n" +
          "  --help\n" +
          "Each non-blank line is one finite JSON number. Output is one line of compact JSON:\n" +
          '  {"count":N,"mean":..,"variance":..,"stddev":..,"min":..,"max":..}\n'
      );
      process.exit(0);
    } else if (a === "--sample") {
      sample = true;
    } else if (a.charAt(0) === "-" && a !== "-") {
      process.stderr.write("running-stats: unknown option: " + a + "\n");
      process.exit(2);
    } else {
      if (file !== null) {
        process.stderr.write("running-stats: more than one FILE given\n");
        process.exit(2);
      }
      file = a;
    }
  }

  function run(text) {
    var rec;
    try {
      rec = fold(text, { sample: sample });
    } catch (e) {
      process.stderr.write("running-stats: " + e.message + "\n");
      process.exit(2);
      return;
    }
    process.stdout.write(JSON.stringify(rec) + "\n");
  }

  if (file === null || file === "-") {
    var chunks = [];
    process.stdin.on("data", function (c) { chunks.push(c); });
    process.stdin.on("end", function () { run(Buffer.concat(chunks).toString("utf8")); });
  } else {
    var fs = require("fs");
    var text;
    try {
      var st = fs.statSync(file);
      if (st.isDirectory()) {
        process.stderr.write("running-stats: is a directory: " + file + "\n");
        process.exit(2);
        return;
      }
      text = fs.readFileSync(file, "utf8");
    } catch (e) {
      process.stderr.write("running-stats: cannot read file: " + file + "\n");
      process.exit(2);
      return;
    }
    run(text);
  }
}

if (typeof require !== "undefined" && require.main === module) {
  main(process.argv);
}
test_running-stats.js223 lineson GitHub →
#!/usr/bin/env node
/**
 * test_running-stats.js — out-of-band battery for the running-stats gift.
 *
 * The gift folds a JSONL numeric stream into {count,mean,variance,stddev,min,max}
 * by WELFORD'S online algorithm (one pass). This battery checks it against an
 * INDEPENDENTLY-authored oracle written by a genuinely different method — the
 * textbook TWO-PASS formula (pass 1: mean = sum/n; pass 2: variance =
 * sum((x-mean)^2)/n) — plus frozen hand goldens, the numerical-stability hard
 * case (a tight cluster around a huge mean), determinism, order behaviour, the
 * numeric-honesty guards, and a MUTATION BITE proving the gift is genuinely
 * Welford-stable and not the naive sum-of-squares shortcut.
 *
 * GREEN (exit 0) / RED (exit 1).
 */
"use strict";
var assert = require("assert");
var { fold } = require("./running-stats.js");

var pass = 0, fail = 0;
function ok(name, fn) {
  try { fn(); pass++; }
  catch (e) { fail++; console.error("  RED  " + name + "\n       " + e.message); }
}
function jsonl(nums) { return nums.map(function (x) { return JSON.stringify(x); }).join("\n"); }

/* ---- the INDEPENDENT oracle: textbook two-pass (a different method) -------- */
function twoPass(nums, sample) {
  var n = nums.length;
  if (n === 0) return { count: 0, mean: null, variance: null, stddev: null, min: null, max: null };
  var sum = 0, i;
  for (i = 0; i < n; i++) sum += nums[i];
  var mean = sum / n;
  var s = 0;
  for (i = 0; i < n; i++) { var d = nums[i] - mean; s += d * d; }
  var variance;
  if (sample) variance = n < 2 ? null : s / (n - 1);
  else variance = s / n;
  if (variance !== null && variance < 0) variance = 0;
  var stddev = variance === null ? null : Math.sqrt(variance);
  var mn = nums[0], mx = nums[0];
  for (i = 1; i < n; i++) { if (nums[i] < mn) mn = nums[i]; if (nums[i] > mx) mx = nums[i]; }
  return { count: n, mean: mean, variance: variance, stddev: stddev, min: mn, max: mx };
}

/* ---- the NAIVE (unstable) shortcut — used ONLY for the mutation bite ------- */
function naiveVariance(nums) {
  var n = nums.length, s = 0, ss = 0, i;
  for (i = 0; i < n; i++) { s += nums[i]; ss += nums[i] * nums[i]; }
  return (ss - (s * s) / n) / n; // catastrophic cancellation when the mean is large
}

var REL = 1e-9; // tight relative tolerance for float comparison
function close(a, b, rel) {
  rel = rel || REL;
  if (a === b) return true;
  if (a === null || b === null) return a === b;
  var scale = Math.max(1, Math.abs(a), Math.abs(b));
  return Math.abs(a - b) <= rel * scale;
}
function matchesOracle(rec, oracle) {
  assert.strictEqual(rec.count, oracle.count, "count");
  assert.ok(close(rec.mean, oracle.mean), "mean " + rec.mean + " vs " + oracle.mean);
  assert.ok(close(rec.variance, oracle.variance), "variance " + rec.variance + " vs " + oracle.variance);
  assert.ok(close(rec.stddev, oracle.stddev), "stddev " + rec.stddev + " vs " + oracle.stddev);
  assert.ok(close(rec.min, oracle.min), "min");
  assert.ok(close(rec.max, oracle.max), "max");
}

/* ======================================================= FROZEN HAND GOLDENS */
// The classic textbook example: mean 5, population variance 4, stddev 2.
ok("golden: [2,4,4,4,5,5,7,9] -> mean 5, popvar 4, sd 2", function () {
  var r = fold(jsonl([2, 4, 4, 4, 5, 5, 7, 9]));
  assert.strictEqual(r.count, 8);
  assert.ok(close(r.mean, 5), "mean");
  assert.ok(close(r.variance, 4), "variance");
  assert.ok(close(r.stddev, 2), "stddev");
  assert.strictEqual(r.min, 2);
  assert.strictEqual(r.max, 9);
});
ok("golden: same, --sample -> var 32/7, sd sqrt(32/7)", function () {
  var r = fold(jsonl([2, 4, 4, 4, 5, 5, 7, 9]), { sample: true });
  assert.ok(close(r.variance, 32 / 7), "sample variance = 32/7, got " + r.variance);
  assert.ok(close(r.stddev, Math.sqrt(32 / 7)), "sample stddev");
});
ok("golden: single value -> mean=value, popvar 0, sd 0", function () {
  var r = fold(jsonl([42]));
  assert.deepStrictEqual(r, { count: 1, mean: 42, variance: 0, stddev: 0, min: 42, max: 42 });
});
ok("golden: two equal values -> variance 0", function () {
  var r = fold(jsonl([7, 7]));
  assert.ok(close(r.variance, 0), "variance 0");
});
ok("golden: negatives [-5,-1,-1,3] -> mean -1, popvar 8", function () {
  var r = fold(jsonl([-5, -1, -1, 3]));
  assert.ok(close(r.mean, -1), "mean -1");
  assert.ok(close(r.variance, 8), "popvar 8, got " + r.variance);
  assert.strictEqual(r.min, -5);
  assert.strictEqual(r.max, 3);
});

/* ==================================== GIFT == INDEPENDENT TWO-PASS ORACLE === */
var corpus = [
  [1, 2, 3, 4, 5],
  [10, 10, 10, 10],
  [0.5, 1.5, 2.5, 3.5, 4.5],
  [-3, -1, 0, 1, 3, 100],
  [1e6, 1e6 + 3, 1e6 - 3, 1e6 + 1],
  [3.14159, 2.71828, 1.41421, 1.61803],
  [0, 0, 0, 1],
  [42],
];
corpus.forEach(function (nums, k) {
  ok("oracle[pop] #" + k + " (n=" + nums.length + ")", function () {
    matchesOracle(fold(jsonl(nums)), twoPass(nums, false));
  });
});
corpus.forEach(function (nums, k) {
  if (nums.length < 2) return;
  ok("oracle[sample] #" + k, function () {
    matchesOracle(fold(jsonl(nums), { sample: true }), twoPass(nums, true));
  });
});

/* ============================= NUMERICAL STABILITY (the char of this gift) == */
// A tight cluster around a huge mean: true population variance = 2/3.
var HARD = [1e9, 1e9 + 1, 1e9 + 2];
ok("stability: gift matches two-pass on [1e9, 1e9+1, 1e9+2] (popvar 2/3)", function () {
  var r = fold(jsonl(HARD));
  var oracle = twoPass(HARD, false);
  assert.ok(close(r.variance, 2 / 3, 1e-6), "gift variance " + r.variance + " vs 2/3");
  assert.ok(close(r.variance, oracle.variance, 1e-6), "gift vs two-pass oracle");
});
ok("stability MUTATION BITE: the NAIVE shortcut is far off (proves gift isn't naive)", function () {
  var oracle = twoPass(HARD, false).variance; // ~0.6667, the true value
  var naive = naiveVariance(HARD);            // catastrophic cancellation -> ~0
  // The bite: if the gift were the naive method, it would fail the stability test.
  // Here we assert the naive method IS demonstrably wrong, so the stability test
  // above has real teeth (it is not vacuously satisfiable by any implementation).
  assert.ok(Math.abs(naive - oracle) > 1e-3,
    "naive shortcut should diverge from truth on this case (naive=" + naive + ", true=" + oracle + ")");
  // and the gift must be on the TRUTH side of that gap
  var gift = fold(jsonl(HARD)).variance;
  assert.ok(Math.abs(gift - oracle) < 1e-6, "gift must match truth, not the naive shortcut");
});
ok("stability: at 1e12 Welford stays BOUNDED near truth (0.5) where naive fails", function () {
  // Welford's guarantee is a BOUNDED, non-catastrophic error in ONE pass — not that
  // it beats a two-pass that can be exact. Here the two-pass mean (1e12+1) is exactly
  // representable so two-pass nails 0.5, while Welford drifts ~1.5e-5 (its incremental
  // mean passes through non-representable intermediates). That drift is small and
  // bounded; the naive sum-of-squares shortcut, by contrast, catastrophically cancels.
  var nums = [1e12, 1e12 + 1, 1e12 + 1, 1e12 + 2];
  var r = fold(jsonl(nums));
  assert.ok(Math.abs(r.variance - 0.5) < 5e-4, "Welford within 5e-4 of truth 0.5, got " + r.variance);
  var naive = naiveVariance(nums);
  assert.ok(Math.abs(naive - 0.5) > Math.abs(r.variance - 0.5), "naive is further from truth than Welford");
  // count/mean/min/max are still exact and must match the oracle
  var o = twoPass(nums, false);
  assert.strictEqual(r.count, o.count);
  assert.ok(close(r.mean, o.mean), "mean");
  assert.strictEqual(r.min, o.min);
  assert.strictEqual(r.max, o.max);
});

/* ==================================================== DETERMINISM & ORDER === */
ok("determinism: fold twice -> byte-identical record", function () {
  var t = jsonl([3, 1, 4, 1, 5, 9, 2, 6]);
  assert.strictEqual(JSON.stringify(fold(t)), JSON.stringify(fold(t)));
});
ok("order: reordered stream stays within float tolerance of the original", function () {
  // running-stats is a summary statistic: reordering may move the low-order bits
  // (float addition is not associative), but the result stays within tolerance.
  var base = [5, 1, 9, 3, 7, 2, 8, 4, 6];
  var rev = base.slice().reverse();
  var a = fold(jsonl(base)), b = fold(jsonl(rev));
  assert.ok(close(a.mean, b.mean), "mean invariant under reorder (within tol)");
  assert.ok(close(a.variance, b.variance), "variance invariant under reorder (within tol)");
  assert.strictEqual(a.count, b.count);
  assert.strictEqual(a.min, b.min);
  assert.strictEqual(a.max, b.max);
});

/* ============================================ NUMERIC HONESTY (fail closed) = */
ok("honesty: a NaN line throws (exit 2 in CLI)", function () {
  assert.throws(function () { fold("1\nNaN\n2"); });
});
ok("honesty: 1e999 (overflows to Infinity) throws", function () {
  assert.throws(function () { fold("1\n1e999\n2"); });
});
ok("honesty: a non-number JSON line throws", function () {
  assert.throws(function () { fold('1\n"x"\n2'); });
});
ok("honesty: a bad-JSON line throws", function () {
  assert.throws(function () { fold("1\nabc\n2"); });
});
ok("honesty: --sample with <2 values throws", function () {
  assert.throws(function () { fold(jsonl([5]), { sample: true }); });
});

/* ============================================================ SHAPE / EDGES = */
ok("empty stream -> count 0, all null", function () {
  assert.deepStrictEqual(fold(""), { count: 0, mean: null, variance: null, stddev: null, min: null, max: null });
});
ok("blank lines skipped, not counted", function () {
  var r = fold("1\n\n2\n\n3\n");
  assert.strictEqual(r.count, 3);
  assert.ok(close(r.mean, 2), "mean 2");
});
ok("CRLF line endings tolerated", function () {
  var r = fold("1\r\n2\r\n3\r\n");
  assert.strictEqual(r.count, 3);
  assert.ok(close(r.mean, 2), "mean");
});
ok("variance never negative (float artifact clamped to 0)", function () {
  var nums = [1e9, 1e9, 1e9, 1e9];
  var r = fold(jsonl(nums));
  assert.ok(r.variance >= 0, "variance >= 0");
  assert.ok(close(r.variance, 0), "variance ~0 for identical values");
});

/* ==================================================================== done = */
console.log((fail === 0 ? "GREEN" : "RED") + ": " + pass + " assertions passed, " + fail + " failed  [test_running-stats]");
process.exit(fail === 0 ? 0 : 1);
Take the whole folder → MIT Zero dependencies, Node or browser, deterministic