Sessions 5-7a: 955 tests, deployment ready
This commit is contained in:
@@ -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 };
|
||||
Reference in New Issue
Block a user