Skip to content
Permalink
Browse files

Merge pull request #531 from lusbenjamin/confidence-intervals

Deck Winrate Confidence Intervals
  • Loading branch information...
Manuel-777 committed Aug 17, 2019
2 parents d92f111 + f2cfc26 commit 8883ee41df292341e19ba65e8200dae636a4451e
Showing with 32 additions and 13 deletions.
  1. +13 −0 shared/stats-fns.js
  2. +7 −5 window_main/aggregator.js
  3. +12 −8 window_main/decks.js
@@ -131,3 +131,16 @@ function hypergeometricSignificance(
let retVal = math.subtract(1, math.multiply(weightedAverage, 2));
return returnBig ? retVal : math.number(retVal);
}

// Computes the Wald Interval aka Normal Approximation Interval
// Useful for quickly estimating the 95% confidence interval
// Can produce bad results when sample-size < 20
// https://en.wikipedia.org/wiki/Binomial_proportion_confidence_interval#Normal_approximation_interval
// https://www.channelfireball.com/articles/magic-math-how-many-games-do-you-need-for-statistical-significance-in-playtesting/
exports.normalApproximationInterval = normalApproximationInterval;
function normalApproximationInterval(matches, wins) {
if (!matches) return { winrate: 0, interval: 0 };
const winrate = wins / matches;
const interval = 1.96 * Math.sqrt((winrate * (1 - winrate)) / matches);
return { winrate, interval };
}
@@ -20,6 +20,7 @@ const {
getReadableEvent,
getRecentDeckName
} = require("../shared/util");
const { normalApproximationInterval } = require("../shared/stats-fns");

// Default filter values
const DEFAULT_DECK = "All Decks";
@@ -68,11 +69,12 @@ class Aggregator {

static finishStats(stats) {
const { wins, total } = stats;
let winrate = 0;
if (total) {
winrate = Math.round((wins / total) * 100) / 100;
}
stats.winrate = winrate;
const { winrate, interval } = normalApproximationInterval(total, wins);
const roundWinrate = x => Math.round(x * 100) / 100;
stats.winrate = roundWinrate(winrate);
stats.interval = roundWinrate(interval);
stats.winrateLow = roundWinrate(winrate - interval);
stats.winrateHigh = roundWinrate(winrate + interval);
}

static getDefaultColorFilter() {
@@ -211,12 +211,15 @@ function openDecksTab(_filters = {}, scrollTop = 0) {
let colClass = getWinrateClass(dwr.winrate);
deckWinrateDiv.innerHTML = `${dwr.wins}:${
dwr.losses
} <span class="${colClass}_bright">(${formatPercent(
} (<span class="${colClass}_bright">${formatPercent(
dwr.winrate
)})</span>`;
deckWinrateDiv.title = `${dwr.wins} matches won : ${
dwr.losses
} matches lost`;
)}</span> <i style="opacity:0.6;">&plusmn; ${formatPercent(
dwr.interval
)}</i>)`;
deckWinrateDiv.title = `${formatPercent(
dwr.winrateLow
)} to ${formatPercent(dwr.winrateHigh)} with 95% confidence
(estimated actual winrate bounds, assuming a normal distribution)`;
listItem.rightTop.appendChild(deckWinrateDiv);

const deckWinrateLastDiv = createDiv(
@@ -230,9 +233,10 @@ function openDecksTab(_filters = {}, scrollTop = 0) {
deckWinrateLastDiv.innerHTML += `<span class="${colClass}_bright">${formatPercent(
drwr.winrate
)}</span>`;
deckWinrateLastDiv.title = `${drwr.wins} matches won : ${
drwr.losses
} matches lost`;
deckWinrateLastDiv.title = `${formatPercent(
drwr.winrateLow
)} to ${formatPercent(drwr.winrateHigh)} with 95% confidence
(estimated actual winrate bounds, assuming a normal distribution)`;
} else {
deckWinrateLastDiv.innerHTML += "<span>--</span>";
deckWinrateLastDiv.title = "no data yet";

0 comments on commit 8883ee4

Please sign in to comment.
You can’t perform that action at this time.