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