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
+106
View File
@@ -0,0 +1,106 @@
const SIMILARITY_WEIGHTS = {
functional_role_match: 0.20,
opponent_defensive_rating: 0.14,
pace: 0.15,
lineup_context: 0.12,
rest_days: 0.09,
travel_fatigue: 0.08,
game_importance: 0.07,
referee_tendency: 0.06,
score_state_context: 0.05,
role_variance_match: 0.04,
};
/**
* Calculate similarity score between two games based on weighted factors.
* @param {object} gameA - First game context object
* @param {object} gameB - Second game context object
* @param {object} weights - Weight configuration (defaults to SIMILARITY_WEIGHTS)
* @returns {number} Similarity score between 0 and 1
*/
function calculateSimilarityScore(gameA, gameB, weights = SIMILARITY_WEIGHTS) {
let totalScore = 0;
let totalWeight = 0;
for (const [factor, weight] of Object.entries(weights)) {
if (gameA[factor] !== undefined && gameB[factor] !== undefined) {
const maxVal = Math.max(Math.abs(gameA[factor]), Math.abs(gameB[factor]), 1);
const diff = Math.abs(gameA[factor] - gameB[factor]) / maxVal;
const similarity = Math.max(0, 1 - diff);
totalScore += similarity * weight;
totalWeight += weight;
}
}
if (totalWeight === 0) return 0;
return Math.min(1, Math.max(0, totalScore / totalWeight));
}
/**
* Find the most similar historical games to a target game.
* @param {object} targetGame - The game to match against
* @param {Array} historicalGames - Array of historical game objects
* @param {number} minInstances - Minimum matches required (default 15)
* @returns {object} { games: sorted matches, confidence: HIGH|LOW, usedSeasonAvg: boolean }
*/
function findSimilarGames(targetGame, historicalGames, minInstances = 15) {
if (!historicalGames || historicalGames.length === 0) {
return { games: [], confidence: 'LOW', usedSeasonAvg: true };
}
const scored = historicalGames.map(game => ({
...game,
similarityScore: calculateSimilarityScore(targetGame, game),
}));
scored.sort((a, b) => b.similarityScore - a.similarityScore);
if (scored.length < minInstances) {
return {
games: scored,
confidence: 'LOW',
usedSeasonAvg: true,
note: `Only ${scored.length} similar games found (min: ${minInstances}). Falling back to season averages.`,
};
}
return {
games: scored.slice(0, Math.max(minInstances, Math.floor(scored.length * 0.3))),
confidence: 'HIGH',
usedSeasonAvg: false,
};
}
/**
* Calculate posterior distribution from similar games.
* @param {Array} similarGames - Array of game objects with a stat value
* @returns {object} { mean, stddev, ci_low, ci_high, n }
*/
function getPosteriorDistribution(similarGames) {
if (!similarGames || similarGames.length === 0) {
return { mean: 0, stddev: 0, ci_low: 0, ci_high: 0, n: 0 };
}
const values = similarGames.map(g => g.statValue || 0);
const n = values.length;
const mean = values.reduce((sum, v) => sum + v, 0) / n;
const variance = values.reduce((sum, v) => sum + Math.pow(v - mean, 2), 0) / (n - 1 || 1);
const stddev = Math.sqrt(variance);
const zScore = 1.96; // 95% CI
const se = stddev / Math.sqrt(n);
return {
mean: Math.round(mean * 100) / 100,
stddev: Math.round(stddev * 100) / 100,
ci_low: Math.round((mean - zScore * se) * 100) / 100,
ci_high: Math.round((mean + zScore * se) * 100) / 100,
n,
};
}
module.exports = {
SIMILARITY_WEIGHTS,
calculateSimilarityScore,
findSimilarGames,
getPosteriorDistribution,
};