Docs/Skills and code/skill script-cohort-analysis
.claude/skills/script-cohort-analysis/scripts/lib.js
What the file says about itself
Shared helpers: parsing, seeded RNG, statistics. No dependencies.
// Shared helpers: parsing, seeded RNG, statistics. No dependencies.
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
function readJson(p) { return JSON.parse(fs.readFileSync(p, 'utf8')); }
function writeJson(p, obj) {
fs.mkdirSync(path.dirname(p), { recursive: true });
fs.writeFileSync(p, JSON.stringify(obj, null, 2) + '\n', 'utf8');
}
function sha256(s) { return crypto.createHash('sha256').update(s).digest('hex'); }
function parseArgs(argv) {
const out = {};
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (!a.startsWith('--')) continue;
const key = a.slice(2);
const next = argv[i + 1];
if (next === undefined || next.startsWith('--')) out[key] = true;
else { out[key] = next; i++; }
}
return out;
}
// "mm:ss" | "h:mm:ss" | number -> seconds
function toSec(t) {
if (t === null || t === undefined || t === '') return null;
if (typeof t === 'number') return t;
const parts = String(t).trim().replace(/^\[|\]$/g, '').split(':').map(Number);
if (parts.some(Number.isNaN)) return null;
return parts.reduce((acc, v) => acc * 60 + v, 0);
}
// mulberry32
function rng(seed) {
let a = seed >>> 0;
return () => {
a = (a + 0x6d2b79f5) >>> 0;
let t = a;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
function shuffle(arr, rand) {
const a = arr.slice();
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(rand() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
// ---------- statistics ----------
const mean = (xs) => xs.reduce((s, x) => s + x, 0) / xs.length;
function sd(xs) {
if (xs.length < 2) return 0;
const m = mean(xs);
return Math.sqrt(xs.reduce((s, x) => s + (x - m) ** 2, 0) / (xs.length - 1));
}
function ranks(xs) {
const idx = xs.map((x, i) => [x, i]).sort((a, b) => a[0] - b[0]);
const r = new Array(xs.length);
for (let i = 0; i < idx.length;) {
let j = i;
while (j + 1 < idx.length && idx[j + 1][0] === idx[i][0]) j++;
const avg = (i + j) / 2 + 1;
for (let k = i; k <= j; k++) r[idx[k][1]] = avg;
i = j + 1;
}
return r;
}
function pearson(x, y) {
const mx = mean(x), my = mean(y);
let num = 0, dx = 0, dy = 0;
for (let i = 0; i < x.length; i++) {
num += (x[i] - mx) * (y[i] - my);
dx += (x[i] - mx) ** 2;
dy += (y[i] - my) ** 2;
}
if (dx === 0 || dy === 0) return null; // constant feature: no information
return num / Math.sqrt(dx * dy);
}
const spearman = (x, y) => pearson(ranks(x), ranks(y));
function permutations(n) {
const res = [];
const a = [...Array(n).keys()];
const rec = (k) => {
if (k === n) { res.push(a.slice()); return; }
for (let i = k; i < n; i++) {
[a[k], a[i]] = [a[i], a[k]]; rec(k + 1); [a[k], a[i]] = [a[i], a[k]];
}
};
rec(0);
return res;
}
// Permutation p-value for Spearman rho. direction: +1/-1 one-sided, 0 two-sided.
// Exact for n <= 8, Monte Carlo otherwise.
function permTest(x, y, direction, seed = 1) {
const obs = spearman(x, y);
if (obs === null) return { rho: null, p: null };
const rx = ranks(x), ry = ranks(y);
const stat = (r) => {
const v = pearson(rx, r);
if (direction === 0) return Math.abs(v);
return direction * v;
};
const target = direction === 0 ? Math.abs(obs) : direction * obs;
let hits = 0, total = 0;
const eps = 1e-12;
if (x.length <= 8) {
for (const perm of permutations(x.length)) {
total++;
if (stat(perm.map((i) => ry[i])) >= target - eps) hits++;
}
} else {
const rand = rng(seed);
total = 20000;
for (let t = 0; t < total; t++) if (stat(shuffle(ry, rand)) >= target - eps) hits++;
}
return { rho: obs, p: hits / total };
}
function zscores(xs) {
const m = mean(xs), s = sd(xs);
return xs.map((x) => (s === 0 ? 0 : (x - m) / s));
}
module.exports = {
readJson, writeJson, sha256, parseArgs, toSec, rng, shuffle,
mean, sd, ranks, pearson, spearman, permTest, zscores,
};
Shown as it is in the repository at build time (2026-11-03); the home directory appears as <home>. If this page and the file differ, the file is right. Back to the catalogue.