diff --git a/index.html b/index.html index 89f0888..892f385 100644 --- a/index.html +++ b/index.html @@ -315,7 +315,7 @@ const data = await response.json(); - if (data.predictedRating) { + if (data.predictedRating && data.predictedRating > 0) { predictedCell.textContent = data.predictedRating; const currentRating = parseInt(document.querySelector(`#row-${pdgaNumber} .rating`).textContent); @@ -326,8 +326,8 @@ diffCell.className = `difference ${diffClass}`; diffCell.textContent = diffText; } else { - predictedCell.textContent = 'Error'; - diffCell.textContent = 'Error'; + predictedCell.textContent = 'N/A'; + diffCell.innerHTML = 'Unable to calculate
(Try again later)
'; } } catch (error) { diff --git a/server.js b/server.js index 2eee101..912f1a7 100644 --- a/server.js +++ b/server.js @@ -130,14 +130,31 @@ async function scrapePDGARating(pdgaNumber, retries = 3) { } } -async function getPredictedRating(browser, pdgaNumber) { - try { - const roundRatings = await getPlayerCompetitionRatings(browser, pdgaNumber); - return calculatePredictedRating(roundRatings); - } catch (error) { - console.error(`Error getting predicted rating for ${pdgaNumber}:`, error); - return 0; +async function getPredictedRating(browser, pdgaNumber, retries = 2) { + for (let attempt = 1; attempt <= retries; attempt++) { + try { + console.log(`Predicted rating attempt ${attempt}/${retries} for PDGA ${pdgaNumber}`); + const roundRatings = await getPlayerCompetitionRatings(browser, pdgaNumber); + const predictedRating = calculatePredictedRating(roundRatings); + + if (predictedRating > 0) { + return predictedRating; + } + + if (attempt < retries) { + console.log(`No ratings found, waiting before retry...`); + await new Promise(resolve => setTimeout(resolve, 5000)); + } + } catch (error) { + console.error(`Predicted rating attempt ${attempt}/${retries} failed for ${pdgaNumber}:`, error.message); + if (attempt < retries) { + await new Promise(resolve => setTimeout(resolve, 5000)); + } + } } + + console.log(`All attempts failed for predicted rating of PDGA ${pdgaNumber}`); + return 0; } async function getPlayerCompetitionRatings(browser, pdgaNumber) { @@ -165,10 +182,10 @@ async function getPlayerCompetitionRatings(browser, pdgaNumber) { if (dateMatch) { const dateStr = dateMatch[0]; const date = new Date(dateStr); - const twoYearsAgo = new Date(); - twoYearsAgo.setFullYear(twoYearsAgo.getFullYear() - 2); + const oneYearAgo = new Date(); + oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1); - if (date > twoYearsAgo) { + if (date > oneYearAgo) { const href = tournamentCell.getAttribute('href'); if (href) { urls.push({ @@ -182,15 +199,15 @@ async function getPlayerCompetitionRatings(browser, pdgaNumber) { }); }); - return urls.slice(0, 15); + return urls.slice(0, 8); // Reduce number of tournaments to scrape }); console.log(`Found ${tournamentUrls.length} recent tournaments for PDGA ${pdgaNumber}`); for (const tournamentData of tournamentUrls) { try { - await page.goto(tournamentData.url, { waitUntil: 'networkidle2' }); - await page.waitForTimeout(2000); + await page.goto(tournamentData.url, { waitUntil: 'domcontentloaded', timeout: 45000 }); + await page.waitForTimeout(3000); // Longer delay between requests const roundRatings = await page.evaluate((pdgaNum) => { const rows = document.querySelectorAll('tr'); @@ -241,11 +258,8 @@ async function getPlayerCompetitionRatings(browser, pdgaNumber) { await page.close(); } - const oneYearAgo = new Date(); - oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1); - - const recentRatings = allRatings.filter(r => r.date > oneYearAgo); - return recentRatings.length > 8 ? recentRatings : allRatings; + // Return all ratings from the last year (already filtered above) + return allRatings; } function parseDate(dateStr) { @@ -276,34 +290,52 @@ function parseDate(dateStr) { function calculatePredictedRating(roundRatings) { if (!roundRatings || roundRatings.length === 0) return 0; - const ratings = roundRatings + // Sort by date (most recent first) and extract ratings + const sortedRatings = roundRatings .sort((a, b) => b.date - a.date) - .map(r => r.rating); + .map(r => r.rating) + .filter(r => r > 0); - const weightedRatings = []; - const oneFourth = ratings.length > 9 ? Math.round(ratings.length * 0.25) : -1; + if (sortedRatings.length === 0) return 0; - for (let i = 0; i < ratings.length; i++) { - const rating = ratings[i]; - weightedRatings.push(rating); + // PDGA Rule: Need at least 7 rounds to apply outlier exclusion + if (sortedRatings.length >= 7) { + // Calculate standard deviation for outlier detection + const mean = sortedRatings.reduce((sum, r) => sum + r, 0) / sortedRatings.length; + const stdDev = calculateStandardDeviation(sortedRatings); - if (i < oneFourth) { - weightedRatings.push(rating); + // PDGA Rule: Exclude rounds more than 2.5 standard deviations below average + const filteredRatings = sortedRatings.filter(rating => + rating >= (mean - 2.5 * stdDev) + ); + + // Use filtered ratings if we still have enough data + if (filteredRatings.length >= 4) { + sortedRatings.splice(0, sortedRatings.length, ...filteredRatings); } } - const validRatings = weightedRatings.filter(r => r > 0); - if (validRatings.length === 0) return 0; + // PDGA Rule: Most recent 25% of rounds count double if 9+ rounds + const weightedRatings = []; + if (sortedRatings.length >= 9) { + const recentCount = Math.round(sortedRatings.length * 0.25); + + // Add all ratings once + weightedRatings.push(...sortedRatings); + + // Add the most recent 25% again (double weight) + for (let i = 0; i < recentCount; i++) { + weightedRatings.push(sortedRatings[i]); + } + } else { + // If fewer than 9 rounds, no double weighting + weightedRatings.push(...sortedRatings); + } - const mean = validRatings.reduce((sum, r) => sum + r, 0) / validRatings.length; - const stdDev = calculateStandardDeviation(ratings); - const deviation = Math.min(stdDev * 2.5, 100); + // Calculate final average + const finalRating = weightedRatings.reduce((sum, r) => sum + r, 0) / weightedRatings.length; - const filteredRatings = validRatings.filter(rating => Math.abs(mean - rating) < deviation); - - if (filteredRatings.length === 0) return Math.round(mean); - - return Math.round(filteredRatings.reduce((sum, r) => sum + r, 0) / filteredRatings.length); + return Math.round(finalRating); } function calculateStandardDeviation(ratings) {