Sessions 5-7a: 955 tests, deployment ready

This commit is contained in:
Kev
2026-06-08 18:35:13 -04:00
parent 06b82624a2
commit 1fa04dc776
371 changed files with 49366 additions and 955 deletions
@@ -0,0 +1,66 @@
/**
* Consistency score — how predictable is this player for this stat?
*
* cv = stddev / mean
*
* Coefficient of variation collapses sample-size differences and lets us
* compare a 25-point scorer with low variance to a 12-point scorer with
* the same absolute variance. Lower cv = more reliable.
*
* The consistency score modifies Engine 2's confidence. An "elite"
* consistency player gets a tighter projection range; a "boom_bust"
* player gets a wider one.
*/
const gameLogService = require('./gameLogService');
function statFromGameLog(row, statType) {
if (!row) return null;
switch (statType) {
case 'pts_reb_ast':
return (Number(row.points) || 0) + (Number(row.rebounds) || 0) + (Number(row.assists) || 0);
case 'pts_reb':
return (Number(row.points) || 0) + (Number(row.rebounds) || 0);
case 'pts_ast':
return (Number(row.points) || 0) + (Number(row.assists) || 0);
case 'reb_ast':
return (Number(row.rebounds) || 0) + (Number(row.assists) || 0);
case 'stl_blk':
return (Number(row.steals) || 0) + (Number(row.blocks) || 0);
default: {
const v = Number(row[statType]);
return Number.isFinite(v) ? v : null;
}
}
}
function classify(cv) {
if (cv < 0.15) return { consistency: 'elite', score: 1.0 };
if (cv < 0.30) return { consistency: 'reliable', score: 0.7 };
if (cv < 0.50) return { consistency: 'volatile', score: 0.4 };
return { consistency: 'boom_bust', score: 0.1 };
}
function statsFor(values) {
const clean = values.filter((v) => Number.isFinite(v));
if (clean.length < 2) return null;
const mean = clean.reduce((a, b) => a + b, 0) / clean.length;
if (mean === 0) return null;
const variance = clean.reduce((s, v) => s + (v - mean) ** 2, 0) / (clean.length - 1);
const stddev = Math.sqrt(variance);
return { mean, stddev, cv: stddev / Math.abs(mean), games: clean.length };
}
async function getConsistency(input = {}) {
const { playerName, sport, statType, gameLogs: providedLogs } = input;
const logs = providedLogs || await gameLogService.getGameLogs(playerName, sport, 20);
if (!logs || logs.length < 2) {
return { consistency: 'unknown', score: null, games: logs?.length ?? 0 };
}
const values = logs.map((row) => statFromGameLog(row, statType)).filter((v) => v != null);
const s = statsFor(values);
if (!s) return { consistency: 'unknown', score: null, games: values.length };
return { ...s, ...classify(s.cv) };
}
module.exports = { getConsistency, classify, statsFor, statFromGameLog };