Docs/Skills and code/skill script-cohort-analysis

.claude/skills/script-cohort-analysis/scripts/lib.js

Code136 lines4.1 KBSkill: script-cohort-analysis

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.