From a8135ad40cbeb6c0c464d57010032dc033993439 Mon Sep 17 00:00:00 2001 From: schneefux Date: Sat, 16 Jun 2018 20:47:59 +0200 Subject: Calculate and show level scaling --- src/HeroTalentTable.vue | 29 +++++++++---- src/Highlights.vue | 44 ++++++++++--------- src/ReportService.js | 111 ++++++++++++++++++++++++++++++++++++------------ src/ReportTable.vue | 27 +++++++----- src/WinPickScatter.vue | 4 +- src/maps/maps.js | 18 +++++--- 6 files changed, 158 insertions(+), 75 deletions(-) (limited to 'src') diff --git a/src/HeroTalentTable.vue b/src/HeroTalentTable.vue index 0e4509b..5c3545b 100644 --- a/src/HeroTalentTable.vue +++ b/src/HeroTalentTable.vue @@ -8,18 +8,27 @@ :mobile-cards="false"> @@ -38,6 +47,8 @@ export default Vue.component('hero-talent-table', { data: function() { return { getTalentName: maps.getTalentName, + getLevelBuckets: ReportService.getLevelBuckets, + getLevelsPerBucket: (entry) => maps.getMaxLevel(entry) / this.getLevelBuckets(), }; }, computed: { diff --git a/src/Highlights.vue b/src/Highlights.vue index b074b70..0aa8a5f 100644 --- a/src/Highlights.vue +++ b/src/Highlights.vue @@ -5,7 +5,7 @@ title="Overpowered" type="Win Rate" label="%" - :value="Math.round(100 * stats.topWin.Winner)" + :value="Math.round(100 * stats.topWin.TotalWinner)" :entry="stats.topWin" /> @@ -18,29 +18,31 @@ :entry="stats.topPick" /> -
+
+ :value="Math.round(100 * stats.topLowLevel.Winner)" + :entry="stats.topLowLevel" />
-
+
+ title="Spender" + type="Win Rate per Level" + label="%" + :value="'+' + Math.round(100 * stats.topScaling.TalentWinrateLevelScaling)" + :entry="stats.topScaling" />
-
+
+ title="Hidden Gem" + type="Win Rate" + label="%" + :value="Math.round(100 * stats.topUnpopularWin.TotalWinner)" + :entry="stats.topUnpopularWin" />
@@ -48,7 +50,7 @@ title="Best Rare" type="Win Rate" label="%" - :value="Math.round(100 * stats.topRareWin.Winner)" + :value="Math.round(100 * stats.topRareWin.TotalWinner)" :entry="stats.topRareWin" />
@@ -57,7 +59,7 @@ title="Best Epic" type="Win Rate" label="%" - :value="Math.round(100 * stats.topEpicWin.Winner)" + :value="Math.round(100 * stats.topEpicWin.TotalWinner)" :entry="stats.topEpicWin" />
@@ -66,7 +68,7 @@ title="Best Legendary" type="Win Rate" label="%" - :value="Math.round(100 * stats.topLegendaryWin.Winner)" + :value="Math.round(100 * stats.topLegendaryWin.TotalWinner)" :entry="stats.topLegendaryWin" />
@@ -89,11 +91,11 @@ export default Vue.component('highlights', { topUnpopularWin: ReportService.getTopUnpopularWin(this.selectedMode), topPick: ReportService.getTopPick(this.selectedMode), totalPicks: ReportService.getTotalPicks(this.selectedMode), - highestLevelAvg: ReportService.getHighestLevelAvg(this.selectedMode), topRareWin: ReportService.getTopRareWins(this.selectedMode), topEpicWin: ReportService.getTopEpicWins(this.selectedMode), topLegendaryWin: ReportService.getTopLegendaryWins(this.selectedMode), - lowestLevelAvg: ReportService.getLowestLevelAvg(this.selectedMode), + topLowLevel: ReportService.getTopLowLevel(this.selectedMode), + topScaling: ReportService.getTopScaling(this.selectedMode), }; }, playersPerMatch: function() { 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; + }, }; diff --git a/src/ReportTable.vue b/src/ReportTable.vue index 75a1369..4cbc280 100644 --- a/src/ReportTable.vue +++ b/src/ReportTable.vue @@ -1,7 +1,7 @@