diff options
Diffstat (limited to 'src/ReportService.js')
| -rw-r--r-- | src/ReportService.js | 111 |
1 files changed, 83 insertions, 28 deletions
diff --git a/src/ReportService.js b/src/ReportService.js index e0bd299..8bd9514 100644 --- a/src/ReportService.js +++ b/src/ReportService.js @@ -1,9 +1,10 @@ import * as maps from './maps/maps'; -import * as metadata from '../data/78f0edf0/metadata.json'; +import * as metadata from '../data/2d73896c/metadata.json'; const POPULAR_THRESHOLD = 1.0; // percent +const PICKS_THRESHOLD = 200; // picks -const report = require('../data/78f0edf0/report.json') +const report = require('../data/2d73896c/report.json') .filter((entry) => entry.Actor != undefined); // bad data from API downtime const reports = new Map(); @@ -14,45 +15,95 @@ const topWins = new Map(); const topRareWins = new Map(); const topEpicWins = new Map(); const topLegendaryWins = new Map(); -const topLeveledUp = new Map(); -const topLeveledDown = new Map(); +const topLowLevel = new Map(); +const topScaling = new Map(); const topUnpopularWins = new Map(); let totalMatches = 0; let actors = []; const modes = metadata.config.api.modes; for(let mode of modes) { - const relevancy = (entry) => (entry.Count / totalPicks.get(mode)) * entry.Winner; + const relevancy = (entry) => (entry.TotalPicks / totalPicks.get(mode)) * entry.TotalWinner; const playersPerMatch = maps.playersPerMatch(mode); - reports.set(mode, report.filter((entry) => entry.Mode == mode)); - totalPicks.set(mode, report.map((entry) => entry.Count).reduce((agg, cur) => agg + cur, 0)); + const reportFilterMode = report.filter((entry) => entry.Mode == mode); + + const baseLevelForEntry = (entry) => reportFilterMode + .filter((entry2) => entry2.Actor == entry.Actor && + entry2.Talent == entry.Talent) + .map((entry2) => entry2.LevelBucket) + .reduce((agg, cur) => cur < agg? cur : agg); + + const reportFilterModeBaseLevel = reportFilterMode.filter((entry) => entry.LevelBucket == baseLevelForEntry(entry)); + + // using the "base level" (bucket 0) report, add information about diff to 'NoTalent' and scaling + const reportFilterModeRegressed = reportFilterModeBaseLevel.map((entry) => { + const sum = (agg, cur) => agg + cur; + const range = (n) => [...Array(n).keys()]; + + // perform a weighted linear regression. x: level bucket (scaled 0 - 1) y: win rate weights: picks + const actorReport = reportFilterMode + .filter((entry2) => + entry2.Actor == entry.Actor && + entry2.Talent == entry.Talent && + entry2.LevelBucket >= 0); + const buckets = actorReport.map((entry) => entry.LevelBucket).sort(); + const findActorWithBucket = (bucket) => actorReport.find((entry) => entry.LevelBucket == bucket); + const weights = buckets.map((bucket) => findActorWithBucket(bucket).Count); + const sum_weights = weights.reduce(sum, 0); + const xs = [].concat(...buckets.map((bucket, index) => range(weights[index]).map((_) => bucket / metadata.config.self.levelBuckets))); + const ys = [].concat(...buckets.map((bucket, index) => range(weights[index]).map((_) => findActorWithBucket(bucket).Winner))); + const n = xs.length; + const sum_x = xs.reduce(sum, 0); + const sum_y = ys.reduce(sum, 0); + const sum_xy = xs.map((x, index) => x * ys[index]).reduce(sum, 0); + const sum_xx = xs.map((x) => x * x).reduce(sum, 0); + const sum_yy = ys.map((y) => y * y).reduce(sum, 0); + + const slope = (n * sum_xy - sum_x * sum_y) / (n * sum_xx - sum_x * sum_x); + const intercept = (sum_y - slope * sum_x) / n; + + return Object.assign({}, entry, { + TalentWinrateBase: intercept, + TalentWinrateScaling: slope, // to 1 = max level + TalentWinrateLevelScaling: slope / maps.getMaxLevel(entry), + TotalPicks: sum_weights || entry.Count, // NoTalent has no weights + TotalWinner: sum_y / sum_weights || entry.Winner, + SampleTooSmall: sum_weights < PICKS_THRESHOLD, + }); + }); + reports.set(mode, reportFilterModeRegressed); + + totalPicks.set(mode, reports.get(mode) + .map((entry) => entry.TotalPicks) + .reduce((agg, cur) => agg + cur, 0)); totalMatches += totalPicks.get(mode) / maps.playersPerMatch(mode); top10Relevancy.set(mode, reports.get(mode) .sort((entry1, entry2) => relevancy(entry2) - relevancy(entry1)) .slice(0, 10)); - topPicks.set(mode, reports.get(mode).sort((entry1, entry2) => entry2.Count - entry1.Count)[0]); + topPicks.set(mode, reports.get(mode).sort((entry1, entry2) => entry2.TotalPicks - entry1.TotalPicks)[0]); topWins.set(mode, reports.get(mode) - .filter((entry) => 100 * playersPerMatch * entry.Count / totalPicks.get(mode) > POPULAR_THRESHOLD) - .sort((entry1, entry2) => entry2.Winner - entry1.Winner)[0]); + .filter((entry) => 100 * playersPerMatch * entry.TotalPicks / totalPicks.get(mode) > POPULAR_THRESHOLD) + .sort((entry1, entry2) => entry2.TotalWinner - entry1.TotalWinner)[0]); topUnpopularWins.set(mode, reports.get(mode) - .filter((entry) => 100 * playersPerMatch * entry.Count / totalPicks.get(mode) <= POPULAR_THRESHOLD) - .sort((entry1, entry2) => entry2.Winner - entry1.Winner)[0]); + .filter((entry) => 100 * playersPerMatch * entry.TotalPicks / totalPicks.get(mode) <= POPULAR_THRESHOLD && + entry.TotalPicks > PICKS_THRESHOLD) + .sort((entry1, entry2) => entry2.TotalWinner - entry1.TotalWinner)[0]); topRareWins.set(mode, reports.get(mode) - .filter((entry) => maps.getTalentRarity(entry.Talent) == 'Rare' && 100 * playersPerMatch * entry.Count / totalPicks.get(mode)) - .sort((entry1, entry2) => entry2.Winner - entry1.Winner)[0]); + .filter((entry) => maps.getTalentRarity(entry.Talent) == 'Rare' && 100 * playersPerMatch * entry.TotalPicks / totalPicks.get(mode)) + .sort((entry1, entry2) => entry2.TotalWinner - entry1.TotalWinner)[0]); topEpicWins.set(mode, reports.get(mode) - .filter((entry) => maps.getTalentRarity(entry.Talent) == 'Epic' && 100 * playersPerMatch * entry.Count / totalPicks.get(mode)) - .sort((entry1, entry2) => entry2.Winner - entry1.Winner)[0]); + .filter((entry) => maps.getTalentRarity(entry.Talent) == 'Epic' && 100 * playersPerMatch * entry.TotalPicks / totalPicks.get(mode)) + .sort((entry1, entry2) => entry2.TotalWinner - entry1.TotalWinner)[0]); topLegendaryWins.set(mode, reports.get(mode) - .filter((entry) => maps.getTalentRarity(entry.Talent) == 'Legendary' && 100 * playersPerMatch * entry.Count / totalPicks.get(mode)) - .sort((entry1, entry2) => entry2.Winner - entry1.Winner)[0]); - topLeveledUp.set(mode, reports.get(mode) - .filter((entry) => maps.getTalentRarity(entry.Talent) != 'None') - .sort((entry1, entry2) => maps.getScaledLevel(entry2) - maps.getScaledLevel(entry1))[0]); - topLeveledDown.set(mode, reports.get(mode) - .filter((entry) => maps.getTalentRarity(entry.Talent) != 'None') - .sort((entry1, entry2) => maps.getScaledLevel(entry1) - maps.getScaledLevel(entry2))[0]); + .filter((entry) => maps.getTalentRarity(entry.Talent) == 'Legendary' && 100 * playersPerMatch * entry.TotalPicks / totalPicks.get(mode)) + .sort((entry1, entry2) => entry2.TotalWinner - entry1.TotalWinner)[0]); + topLowLevel.set(mode, reports.get(mode) + .filter((entry) => !entry.SampleTooSmall) + .sort((entry1, entry2) => entry2.TalentWinrateBase - entry1.TalentWinrateBase)[0]); + topScaling.set(mode, reports.get(mode) + .filter((entry) => !entry.SampleTooSmall) + .sort((entry1, entry2) => entry2.TalentWinrateLevelScaling - entry1.TalentWinrateLevelScaling)[0]); if (actors.length == 0) { actors = [...new Set(reports.get(mode).map((entry) => entry.Actor))]; @@ -76,12 +127,12 @@ export default { return top10Relevancy.get(mode); }, - getHighestLevelAvg(mode) { - return topLeveledUp.get(mode); + getTopLowLevel(mode) { + return topLowLevel.get(mode); }, - getLowestLevelAvg(mode) { - return topLeveledDown.get(mode); + getTopScaling(mode) { + return topScaling.get(mode); }, getTopWin(mode) { @@ -123,4 +174,8 @@ export default { getLastUpdate() { return metadata.lastUpdate; }, + + getLevelBuckets() { + return metadata.config.self.levelBuckets; + }, }; |
