feat: show cutoff rating threshold in player history accordion (#21)
This commit is contained in:
@@ -35,6 +35,7 @@ function initializeDatabase() {
|
|||||||
const hasPredictedRating = columns.some(col => col.name === 'predicted_rating');
|
const hasPredictedRating = columns.some(col => col.name === 'predicted_rating');
|
||||||
const hasStdDev = columns.some(col => col.name === 'std_dev');
|
const hasStdDev = columns.some(col => col.name === 'std_dev');
|
||||||
const hasExcludedRoundsCount = columns.some(col => col.name === 'excluded_rounds_count');
|
const hasExcludedRoundsCount = columns.some(col => col.name === 'excluded_rounds_count');
|
||||||
|
const hasCutoffRating = columns.some(col => col.name === 'cutoff_rating');
|
||||||
|
|
||||||
if (!hasLastRoundUpdate) {
|
if (!hasLastRoundUpdate) {
|
||||||
logger.info('Adding last_round_update column to players table...');
|
logger.info('Adding last_round_update column to players table...');
|
||||||
@@ -67,6 +68,14 @@ function initializeDatabase() {
|
|||||||
else logger.info('Successfully added excluded_rounds_count column');
|
else logger.info('Successfully added excluded_rounds_count column');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!hasCutoffRating) {
|
||||||
|
logger.info('Adding cutoff_rating column to players table...');
|
||||||
|
db.run(`ALTER TABLE players ADD COLUMN cutoff_rating INTEGER DEFAULT NULL`, (err) => {
|
||||||
|
if (err) logger.error('Error adding cutoff_rating column:', err.message);
|
||||||
|
else logger.info('Successfully added cutoff_rating column');
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -172,11 +172,11 @@ function saveRoundHistoryToDB(pdgaNumber, roundData, isIncremental = false) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function savePredictedRatingToDB(pdgaNumber, predictedRating, stdDev = null, excludedRoundsCount = null) {
|
function savePredictedRatingToDB(pdgaNumber, predictedRating, stdDev = null, excludedRoundsCount = null, cutoffRating = null) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
db.run(
|
db.run(
|
||||||
'UPDATE players SET predicted_rating = ?, std_dev = ?, excluded_rounds_count = ? WHERE pdga_number = ?',
|
'UPDATE players SET predicted_rating = ?, std_dev = ?, excluded_rounds_count = ?, cutoff_rating = ? WHERE pdga_number = ?',
|
||||||
[predictedRating, stdDev, excludedRoundsCount, pdgaNumber],
|
[predictedRating, stdDev, excludedRoundsCount, cutoffRating, pdgaNumber],
|
||||||
function(err) {
|
function(err) {
|
||||||
if (err) reject(err);
|
if (err) reject(err);
|
||||||
else resolve();
|
else resolve();
|
||||||
|
|||||||
@@ -400,7 +400,7 @@ router.post('/api/refresh-round-history/:pdgaNumber', async (req, res) => {
|
|||||||
|
|
||||||
const result = calculatePredictedRating(roundsForPrediction);
|
const result = calculatePredictedRating(roundsForPrediction);
|
||||||
|
|
||||||
await savePredictedRatingToDB(pdgaNumber, result.rating, result.stdDev, result.excludedRoundsCount);
|
await savePredictedRatingToDB(pdgaNumber, result.rating, result.stdDev, result.excludedRoundsCount, result.cutoffRating);
|
||||||
|
|
||||||
const officialCount = allRounds.filter(r => r.source === 'official').length;
|
const officialCount = allRounds.filter(r => r.source === 'official').length;
|
||||||
const newCount = allRounds.filter(r => r.source === 'new').length;
|
const newCount = allRounds.filter(r => r.source === 'new').length;
|
||||||
@@ -410,6 +410,7 @@ router.post('/api/refresh-round-history/:pdgaNumber', async (req, res) => {
|
|||||||
predictedRating: result.rating,
|
predictedRating: result.rating,
|
||||||
stdDev: result.stdDev,
|
stdDev: result.stdDev,
|
||||||
excludedRoundsCount: result.excludedRoundsCount,
|
excludedRoundsCount: result.excludedRoundsCount,
|
||||||
|
cutoffRating: result.cutoffRating,
|
||||||
totalRounds: roundsForPrediction.length,
|
totalRounds: roundsForPrediction.length,
|
||||||
officialRounds: officialCount,
|
officialRounds: officialCount,
|
||||||
newRounds: newCount,
|
newRounds: newCount,
|
||||||
|
|||||||
@@ -41,11 +41,13 @@ async function getPlayerDataFromDB(pdgaNumber, { includeMonthlyHistory = true }
|
|||||||
let predictedRating = cachedPlayer.predicted_rating;
|
let predictedRating = cachedPlayer.predicted_rating;
|
||||||
let stdDev = cachedPlayer.std_dev;
|
let stdDev = cachedPlayer.std_dev;
|
||||||
let excludedRoundsCount = cachedPlayer.excluded_rounds_count;
|
let excludedRoundsCount = cachedPlayer.excluded_rounds_count;
|
||||||
|
let cutoffRating = cachedPlayer.cutoff_rating;
|
||||||
if (!predictedRating || predictedRating === 0) {
|
if (!predictedRating || predictedRating === 0) {
|
||||||
predictedRating = await getPredictedRatingFromDB(pdgaNumber);
|
predictedRating = await getPredictedRatingFromDB(pdgaNumber);
|
||||||
const updatedPlayer = await getPlayerFromDB(pdgaNumber);
|
const updatedPlayer = await getPlayerFromDB(pdgaNumber);
|
||||||
stdDev = updatedPlayer?.std_dev;
|
stdDev = updatedPlayer?.std_dev;
|
||||||
excludedRoundsCount = updatedPlayer?.excluded_rounds_count;
|
excludedRoundsCount = updatedPlayer?.excluded_rounds_count;
|
||||||
|
cutoffRating = updatedPlayer?.cutoff_rating;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rating = cachedPlayer.current_rating;
|
const rating = cachedPlayer.current_rating;
|
||||||
@@ -68,6 +70,7 @@ async function getPlayerDataFromDB(pdgaNumber, { includeMonthlyHistory = true }
|
|||||||
predictedRating: resolvedPredicted,
|
predictedRating: resolvedPredicted,
|
||||||
stdDev: resolvedStdDev,
|
stdDev: resolvedStdDev,
|
||||||
excludedRoundsCount: (excludedRoundsCount != null && excludedRoundsCount >= 0) ? excludedRoundsCount : null,
|
excludedRoundsCount: (excludedRoundsCount != null && excludedRoundsCount >= 0) ? excludedRoundsCount : null,
|
||||||
|
cutoffRating: (cutoffRating != null && cutoffRating > 0) ? cutoffRating : null,
|
||||||
lastMonthRating,
|
lastMonthRating,
|
||||||
// gap between next predicted update and current rating (null when either is missing)
|
// gap between next predicted update and current rating (null when either is missing)
|
||||||
deltaPredicted: (resolvedPredicted != null && rating != null) ? resolvedPredicted - rating : null,
|
deltaPredicted: (resolvedPredicted != null && rating != null) ? resolvedPredicted - rating : null,
|
||||||
@@ -148,7 +151,7 @@ async function getPredictedRatingFromDB(pdgaNumber) {
|
|||||||
|
|
||||||
const result = calculatePredictedRating(roundRatings);
|
const result = calculatePredictedRating(roundRatings);
|
||||||
|
|
||||||
await savePredictedRatingToDB(pdgaNumber, result.rating, result.stdDev, result.excludedRoundsCount);
|
await savePredictedRatingToDB(pdgaNumber, result.rating, result.stdDev, result.excludedRoundsCount, result.cutoffRating);
|
||||||
|
|
||||||
return result.rating;
|
return result.rating;
|
||||||
}
|
}
|
||||||
@@ -233,6 +236,7 @@ async function getAllRatingsFromDB(progressCallback = null) {
|
|||||||
predictedRating: null,
|
predictedRating: null,
|
||||||
stdDev: null,
|
stdDev: null,
|
||||||
excludedRoundsCount: null,
|
excludedRoundsCount: null,
|
||||||
|
cutoffRating: null,
|
||||||
lastMonthRating: (errorRating != null && errorRatingChange != null) ? errorRating - errorRatingChange : null,
|
lastMonthRating: (errorRating != null && errorRatingChange != null) ? errorRating - errorRatingChange : null,
|
||||||
deltaPredicted: null,
|
deltaPredicted: null,
|
||||||
monthlyHistory: [],
|
monthlyHistory: [],
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ function calculatePredictedRating(roundRatings) {
|
|||||||
|
|
||||||
if (!roundRatings || roundRatings.length === 0) {
|
if (!roundRatings || roundRatings.length === 0) {
|
||||||
debugLog.push('❌ No rounds provided for prediction');
|
debugLog.push('❌ No rounds provided for prediction');
|
||||||
return { rating: 0, debugLog, excludedRoundsCount: null };
|
return { rating: 0, debugLog, excludedRoundsCount: null, cutoffRating: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
debugLog.push(`📊 Starting with ${roundRatings.length} total rounds`);
|
debugLog.push(`📊 Starting with ${roundRatings.length} total rounds`);
|
||||||
@@ -100,7 +100,7 @@ function calculatePredictedRating(roundRatings) {
|
|||||||
|
|
||||||
if (allSortedRounds.length === 0) {
|
if (allSortedRounds.length === 0) {
|
||||||
debugLog.push('❌ No valid rounds after filtering for update date');
|
debugLog.push('❌ No valid rounds after filtering for update date');
|
||||||
return { rating: 0, debugLog, excludedRoundsCount: null };
|
return { rating: 0, debugLog, excludedRoundsCount: null, cutoffRating: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
debugLog.push(`📊 After update date filter: ${allSortedRounds.length} rounds`);
|
debugLog.push(`📊 After update date filter: ${allSortedRounds.length} rounds`);
|
||||||
@@ -127,7 +127,7 @@ function calculatePredictedRating(roundRatings) {
|
|||||||
|
|
||||||
if (eligibleRounds.length === 0) {
|
if (eligibleRounds.length === 0) {
|
||||||
debugLog.push('❌ No eligible rounds found');
|
debugLog.push('❌ No eligible rounds found');
|
||||||
return { rating: 0, debugLog, excludedRoundsCount: null };
|
return { rating: 0, debugLog, excludedRoundsCount: null, cutoffRating: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
debugLog.push(`📈 ELIGIBLE ROUNDS: ${eligibleRounds.length}`);
|
debugLog.push(`📈 ELIGIBLE ROUNDS: ${eligibleRounds.length}`);
|
||||||
@@ -138,6 +138,7 @@ function calculatePredictedRating(roundRatings) {
|
|||||||
let workingRounds = [...eligibleRounds];
|
let workingRounds = [...eligibleRounds];
|
||||||
let workingRatings = workingRounds.map(r => r.rating);
|
let workingRatings = workingRounds.map(r => r.rating);
|
||||||
let excludedRoundsCount = 0;
|
let excludedRoundsCount = 0;
|
||||||
|
let cutoffRating = null;
|
||||||
|
|
||||||
if (workingRatings.length >= 7) {
|
if (workingRatings.length >= 7) {
|
||||||
debugLog.push('🔍 OUTLIER EXCLUSION (≥7 rounds available):');
|
debugLog.push('🔍 OUTLIER EXCLUSION (≥7 rounds available):');
|
||||||
@@ -162,6 +163,7 @@ function calculatePredictedRating(roundRatings) {
|
|||||||
const hundredPointOutliers = workingRatings.filter(rating => rating < hundredPointCutoff && rating >= stdDevCutoff);
|
const hundredPointOutliers = workingRatings.filter(rating => rating < hundredPointCutoff && rating >= stdDevCutoff);
|
||||||
|
|
||||||
excludedRoundsCount = stdDevOutliers.length + hundredPointOutliers.length;
|
excludedRoundsCount = stdDevOutliers.length + hundredPointOutliers.length;
|
||||||
|
cutoffRating = Math.round(Math.max(stdDevCutoff, hundredPointCutoff));
|
||||||
|
|
||||||
if (stdDevOutliers.length > 0) {
|
if (stdDevOutliers.length > 0) {
|
||||||
debugLog.push(` ❌ 2.5σ outliers removed: ${stdDevOutliers.length} rounds`);
|
debugLog.push(` ❌ 2.5σ outliers removed: ${stdDevOutliers.length} rounds`);
|
||||||
@@ -192,6 +194,7 @@ function calculatePredictedRating(roundRatings) {
|
|||||||
} else {
|
} else {
|
||||||
debugLog.push(` ⚠️ Too few rounds after outlier removal (${filteredRatings.length}), keeping all rounds`);
|
debugLog.push(` ⚠️ Too few rounds after outlier removal (${filteredRatings.length}), keeping all rounds`);
|
||||||
excludedRoundsCount = 0;
|
excludedRoundsCount = 0;
|
||||||
|
cutoffRating = null;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
debugLog.push(`⏭️ OUTLIER EXCLUSION SKIPPED (only ${workingRatings.length} rounds, need ≥7)`);
|
debugLog.push(`⏭️ OUTLIER EXCLUSION SKIPPED (only ${workingRatings.length} rounds, need ≥7)`);
|
||||||
@@ -232,7 +235,7 @@ function calculatePredictedRating(roundRatings) {
|
|||||||
debugLog.push(` Final Rating: ${finalRating}`);
|
debugLog.push(` Final Rating: ${finalRating}`);
|
||||||
debugLog.push('=== END PDGA CALCULATION ===');
|
debugLog.push('=== END PDGA CALCULATION ===');
|
||||||
|
|
||||||
return { rating: finalRating, stdDev: Math.round(stdDev), debugLog, excludedRoundsCount };
|
return { rating: finalRating, stdDev: Math.round(stdDev), debugLog, excludedRoundsCount, cutoffRating };
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { parseDate, getNextPDGAUpdateDate, calculatePredictedRating, calculateStandardDeviation };
|
module.exports = { parseDate, getNextPDGAUpdateDate, calculatePredictedRating, calculateStandardDeviation };
|
||||||
|
|||||||
@@ -42,6 +42,12 @@ const chartPdgaNumber = hasPlayer ? player.pdgaNumber : pdgaNumber;
|
|||||||
<dd><%= player.excludedRoundsCount %></dd>
|
<dd><%= player.excludedRoundsCount %></dd>
|
||||||
</div>
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
<% if (player.cutoffRating != null && player.rating) { %>
|
||||||
|
<div>
|
||||||
|
<dt>Cutoff rating</dt>
|
||||||
|
<dd><%= player.cutoffRating %></dd>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|||||||
@@ -130,6 +130,12 @@ function renderSparkline(values, opts) {
|
|||||||
<dd><%= player.excludedRoundsCount %></dd>
|
<dd><%= player.excludedRoundsCount %></dd>
|
||||||
</div>
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
<% if (player.cutoffRating != null && player.rating) { %>
|
||||||
|
<div>
|
||||||
|
<dt>Cutoff rating</dt>
|
||||||
|
<dd><%= player.cutoffRating %></dd>
|
||||||
|
</div>
|
||||||
|
<% } %>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user