From 7defbb5dd204dd6c21238fc9b1dda02fd9828305 Mon Sep 17 00:00:00 2001 From: Mygod Date: Tue, 17 Nov 2020 14:35:35 -0500 Subject: [PATCH 01/12] Make shiny count always appear instead of ID --- src/views/pokemon.mustache | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/views/pokemon.mustache b/src/views/pokemon.mustache index c29d9bf..9a419ab 100644 --- a/src/views/pokemon.mustache +++ b/src/views/pokemon.mustache @@ -98,10 +98,10 @@ - + - + From 085dd56ac585794f1a5bf4378212545a46f6ce22 Mon Sep 17 00:00:00 2001 From: Mygod Date: Tue, 17 Nov 2020 14:58:36 -0500 Subject: [PATCH 02/12] Push shiny rate computation to client --- src/data/map.js | 5 ----- src/views/pokemon.mustache | 12 ++++++++---- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/data/map.js b/src/data/map.js index 2d6839a..fc9b0bd 100644 --- a/src/data/map.js +++ b/src/data/map.js @@ -252,7 +252,6 @@ async function getShinyRates(filter) { const name = Localizer.instance.getPokemonName(pokemonId); const shiny = (row.count || 0); const total = (getTotalCount(pokemonId) || 0); - const rate = shiny === 0 || total === 0 ? 0 : Math.round(total / shiny); const imageUrl = await Localizer.instance.getPokemonIcon(pokemonId); data.push({ id: { @@ -260,10 +259,6 @@ async function getShinyRates(filter) { sort: pokemonId }, pokemon: ` ${name}`, - rate: { - formatted: `1/${rate}`, - sort: -rate, - }, shiny: shiny, total: total, }); diff --git a/src/views/pokemon.mustache b/src/views/pokemon.mustache index 9a419ab..bc3118d 100644 --- a/src/views/pokemon.mustache +++ b/src/views/pokemon.mustache @@ -470,6 +470,13 @@ var shinyDate = $('#shiny-date').val(); var query = `?date=${shinyDate}`; + const computeShinyRate = (row, type, set, meta) => { + return { + _: row.shiny > 0 ? `1/${(row.total / row.shiny).toPrecision(1 + Math.ceil(Math.log10(row.total)))}`, + sort: row.shiny / row.total, + }; + }; + const table = $('#table').DataTable({ "ajax": { "url": "/api/pokemon/shiny" + query, @@ -505,10 +512,7 @@ sort: "id.sort" } }, { "data": "pokemon" }, - { data: { - _: "rate.formatted", - sort: "rate.sort" - } }, + { data: computeShinyRate }, { "data": "shiny" }, { "data": "total" }, ], From 44b5d238f3107912ea8db9ff1bb7f1fee99e19bb Mon Sep 17 00:00:00 2001 From: Mygod Date: Tue, 17 Nov 2020 15:23:44 -0500 Subject: [PATCH 03/12] Add 95% confidence shiny rate --- src/views/pokemon.mustache | 62 ++++++++++++++++++++++++++++++++++++++ static/locales/_de.json | 1 + static/locales/_en.json | 1 + static/locales/_es.json | 1 + 4 files changed, 65 insertions(+) diff --git a/src/views/pokemon.mustache b/src/views/pokemon.mustache index bc3118d..e7ea6ea 100644 --- a/src/views/pokemon.mustache +++ b/src/views/pokemon.mustache @@ -101,6 +101,7 @@ + @@ -477,6 +478,66 @@ }; }; + /** + * Computes exact confidence interval around proportion [lowerEstimate, mostLikely, upperEstimate]. + * The confidence level computed will be (1 - upperTail - lowerTail). + * + * @param numerator An integer indicating the number of events. + * @param denominator An integer indicating the number of trials. + * @param upperTail A real number in [0, 1] indicating % area of upper tail. + * @param lowerTail A real number in [0, 1] indicating % area of lower tail. + * @param accuracy Accuracy of the estimate. Omit to determine automatically based on denominator (.1/denominator). + */ + function computeBinomialConfidenceIntervals(numerator, denominator, upperTail = 0, lowerTail = .05, accuracy = null) { + if (accuracy === null) { + accuracy = .1 / denominator; + } + const computeBinomialLikelihood = (N, p, lower, higher) => { + const q = p / (1 - p); + let sum = 0, total = 0; + for (let k = 0, v = 1; k <= N; ++k, v = v * q * (N + 1 - k) / k) { + total += v; + if (k >= lower && k <= higher) { + sum += v; + } + if (total > 1e30) { + sum /= 1e30; + total /= 1e30; + v /= 1e30; + } + } + return sum / total; + }; + const binarySearch = (lower, higher, predicateIsHigh) => { + let v = (lower + higher) / 2; + while (higher - lower > accuracy) { + if (predicateIsHigh(v)) { + higher = v; + } else { + lower = v; + } + v = (lower + higher) / 2; + } + return v; + }; + let lower = 0, upper = 1; + const likely = numerator / denominator; + if (numerator > 0 && lowerTail > 0) { + lower = binarySearch(0, likely, (p) => computeBinomialLikelihood(denominator, p, numerator, denominator) > lowerTail); + } + if (numerator < denominator && upperTail > 0) { + upper = binarySearch(likely, 1, (p) => computeBinomialLikelihood(denominator, p, numerator, denominator) < upperTail); + } + return [lower, likely, upper]; + } + const computeShinyRateConfidence = (row, type, set, meta) => { + const rate = computeBinomialConfidenceIntervals(row.shiny, row.total)[0]; + return { + _: row.shiny > 0 ? `1/${rate.toPrecision(1 + Math.ceil(Math.log10(row.total)))}`, + sort: rate, + }; + }; + const table = $('#table').DataTable({ "ajax": { "url": "/api/pokemon/shiny" + query, @@ -513,6 +574,7 @@ } }, { "data": "pokemon" }, { data: computeShinyRate }, + { data: computeShinyRateConfidence }, { "data": "shiny" }, { "data": "total" }, ], diff --git a/static/locales/_de.json b/static/locales/_de.json index 6dd6199..7314cda 100644 --- a/static/locales/_de.json +++ b/static/locales/_de.json @@ -67,6 +67,7 @@ "Search by Pokemon name...": "Search by Pokemon name...", "Type in a Pokemon name": "Type in a Pokemon name", "Shiny Rate": "Shiny Rate", + "Shiny Rate 95% Confidence": "Shiny Rate 95% Confidence", "Shiny": "Shiny", "Total": "Total", "Gyms Under Attack!": "Gyms Under Attack!", diff --git a/static/locales/_en.json b/static/locales/_en.json index d9f325d..3a95b74 100644 --- a/static/locales/_en.json +++ b/static/locales/_en.json @@ -67,6 +67,7 @@ "Search by Pokemon name...": "Search by Pokemon name...", "Type in a Pokemon name": "Type in a Pokemon name", "Shiny Rate": "Shiny Rate", + "Shiny Rate 95% Confidence": "Shiny Rate 95% Confidence", "Shiny": "Shiny", "Total": "Total", "Gyms Under Attack!": "Gyms Under Attack!", diff --git a/static/locales/_es.json b/static/locales/_es.json index d9f325d..3a95b74 100644 --- a/static/locales/_es.json +++ b/static/locales/_es.json @@ -67,6 +67,7 @@ "Search by Pokemon name...": "Search by Pokemon name...", "Type in a Pokemon name": "Type in a Pokemon name", "Shiny Rate": "Shiny Rate", + "Shiny Rate 95% Confidence": "Shiny Rate 95% Confidence", "Shiny": "Shiny", "Total": "Total", "Gyms Under Attack!": "Gyms Under Attack!", From dc23a4839ff896740be9247c4d3cfc2e114da625 Mon Sep 17 00:00:00 2001 From: Mygod Date: Tue, 17 Nov 2020 15:24:15 -0500 Subject: [PATCH 04/12] Hide shiny rate in favor of 95% --- src/views/pokemon.mustache | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/pokemon.mustache b/src/views/pokemon.mustache index e7ea6ea..d9b24b1 100644 --- a/src/views/pokemon.mustache +++ b/src/views/pokemon.mustache @@ -100,7 +100,7 @@ - + From a27bbfa7fb830d79588eeae480f9cf11506c8267 Mon Sep 17 00:00:00 2001 From: Mygod Date: Tue, 17 Nov 2020 21:53:14 -0500 Subject: [PATCH 05/12] Fix typo --- src/views/pokemon.mustache | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/pokemon.mustache b/src/views/pokemon.mustache index d9b24b1..b9e5793 100644 --- a/src/views/pokemon.mustache +++ b/src/views/pokemon.mustache @@ -533,7 +533,7 @@ const computeShinyRateConfidence = (row, type, set, meta) => { const rate = computeBinomialConfidenceIntervals(row.shiny, row.total)[0]; return { - _: row.shiny > 0 ? `1/${rate.toPrecision(1 + Math.ceil(Math.log10(row.total)))}`, + _: row.shiny > 0 ? `1/${rate.toPrecision(1 + Math.ceil(Math.log10(row.total)))}` : '0', sort: rate, }; }; From 521d593dcc23863b6e7c362270f6815a5692bc5a Mon Sep 17 00:00:00 2001 From: Mygod Date: Tue, 17 Nov 2020 21:53:34 -0500 Subject: [PATCH 06/12] Fix another typo --- src/views/pokemon.mustache | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/pokemon.mustache b/src/views/pokemon.mustache index b9e5793..0d0856e 100644 --- a/src/views/pokemon.mustache +++ b/src/views/pokemon.mustache @@ -473,7 +473,7 @@ const computeShinyRate = (row, type, set, meta) => { return { - _: row.shiny > 0 ? `1/${(row.total / row.shiny).toPrecision(1 + Math.ceil(Math.log10(row.total)))}`, + _: row.shiny > 0 ? `1/${(row.total / row.shiny).toPrecision(1 + Math.ceil(Math.log10(row.total)))}` : '0', sort: row.shiny / row.total, }; }; From dcd71b354c80835447143f6e76805cf7483c05ad Mon Sep 17 00:00:00 2001 From: Mygod Date: Tue, 17 Nov 2020 22:03:06 -0500 Subject: [PATCH 07/12] Fix usage of DataTable with function --- src/views/pokemon.mustache | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/views/pokemon.mustache b/src/views/pokemon.mustache index 0d0856e..193a3ec 100644 --- a/src/views/pokemon.mustache +++ b/src/views/pokemon.mustache @@ -471,12 +471,9 @@ var shinyDate = $('#shiny-date').val(); var query = `?date=${shinyDate}`; - const computeShinyRate = (row, type, set, meta) => { - return { - _: row.shiny > 0 ? `1/${(row.total / row.shiny).toPrecision(1 + Math.ceil(Math.log10(row.total)))}` : '0', - sort: row.shiny / row.total, - }; - }; + const computeShinyRate = (row, type, set, meta) => type === 'sort' + ? row.shiny / row.total + : row.shiny > 0 ? `1/${(row.total / row.shiny).toPrecision(1 + Math.ceil(Math.log10(row.total)))}` : '0'; /** * Computes exact confidence interval around proportion [lowerEstimate, mostLikely, upperEstimate]. @@ -532,10 +529,8 @@ } const computeShinyRateConfidence = (row, type, set, meta) => { const rate = computeBinomialConfidenceIntervals(row.shiny, row.total)[0]; - return { - _: row.shiny > 0 ? `1/${rate.toPrecision(1 + Math.ceil(Math.log10(row.total)))}` : '0', - sort: rate, - }; + return type === 'sort' ? rate + : row.shiny > 0 ? `1/${rate.toPrecision(1 + Math.ceil(Math.log10(row.total)))}` : '0'; }; const table = $('#table').DataTable({ From 35c275c62df0de0be956db851c95c7bcf80248e5 Mon Sep 17 00:00:00 2001 From: Mygod Date: Tue, 17 Nov 2020 22:04:52 -0500 Subject: [PATCH 08/12] Fix confidence rate calculation --- src/views/pokemon.mustache | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/views/pokemon.mustache b/src/views/pokemon.mustache index 193a3ec..10774f4 100644 --- a/src/views/pokemon.mustache +++ b/src/views/pokemon.mustache @@ -529,8 +529,9 @@ } const computeShinyRateConfidence = (row, type, set, meta) => { const rate = computeBinomialConfidenceIntervals(row.shiny, row.total)[0]; - return type === 'sort' ? rate - : row.shiny > 0 ? `1/${rate.toPrecision(1 + Math.ceil(Math.log10(row.total)))}` : '0'; + return type === 'sort' + ? rate + : row.shiny > 0 ? `1/${(1 / rate).toPrecision(1 + Math.ceil(Math.log10(row.total)))}` : '0'; }; const table = $('#table').DataTable({ From fee06c23847aee4e39bf36ae5f7699ecf5deda9a Mon Sep 17 00:00:00 2001 From: Mygod Date: Tue, 17 Nov 2020 22:11:05 -0500 Subject: [PATCH 09/12] Remove extra digit --- src/views/pokemon.mustache | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/views/pokemon.mustache b/src/views/pokemon.mustache index 10774f4..7fdaa1d 100644 --- a/src/views/pokemon.mustache +++ b/src/views/pokemon.mustache @@ -473,7 +473,7 @@ const computeShinyRate = (row, type, set, meta) => type === 'sort' ? row.shiny / row.total - : row.shiny > 0 ? `1/${(row.total / row.shiny).toPrecision(1 + Math.ceil(Math.log10(row.total)))}` : '0'; + : row.shiny > 0 ? `1/${(row.total / row.shiny).toPrecision(Math.ceil(Math.log10(row.total)))}` : '0'; /** * Computes exact confidence interval around proportion [lowerEstimate, mostLikely, upperEstimate]. @@ -531,7 +531,7 @@ const rate = computeBinomialConfidenceIntervals(row.shiny, row.total)[0]; return type === 'sort' ? rate - : row.shiny > 0 ? `1/${(1 / rate).toPrecision(1 + Math.ceil(Math.log10(row.total)))}` : '0'; + : row.shiny > 0 ? `1/${(1 / rate).toPrecision(Math.ceil(Math.log10(row.total)))}` : '0'; }; const table = $('#table').DataTable({ From 09ac421e62d9b5b823e711ca244b0ebac04c75f1 Mon Sep 17 00:00:00 2001 From: Mygod Date: Tue, 17 Nov 2020 22:23:03 -0500 Subject: [PATCH 10/12] Prevent scientific notation --- src/views/pokemon.mustache | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/views/pokemon.mustache b/src/views/pokemon.mustache index 7fdaa1d..479bd9c 100644 --- a/src/views/pokemon.mustache +++ b/src/views/pokemon.mustache @@ -471,9 +471,10 @@ var shinyDate = $('#shiny-date').val(); var query = `?date=${shinyDate}`; + const withPrecision = (num, denominator) => Number(num.toPrecision(Math.ceil(Math.log10(denominator)))); const computeShinyRate = (row, type, set, meta) => type === 'sort' ? row.shiny / row.total - : row.shiny > 0 ? `1/${(row.total / row.shiny).toPrecision(Math.ceil(Math.log10(row.total)))}` : '0'; + : row.shiny > 0 ? `1/${withPrecision(row.total / row.shiny, row.total)}` : '0'; /** * Computes exact confidence interval around proportion [lowerEstimate, mostLikely, upperEstimate]. @@ -531,7 +532,7 @@ const rate = computeBinomialConfidenceIntervals(row.shiny, row.total)[0]; return type === 'sort' ? rate - : row.shiny > 0 ? `1/${(1 / rate).toPrecision(Math.ceil(Math.log10(row.total)))}` : '0'; + : row.shiny > 0 ? `1/${withPrecision(1 / rate, row.total)}` : '0'; }; const table = $('#table').DataTable({ From 69d244e566fde7a21619f1ce12ca2689b1eeb907 Mon Sep 17 00:00:00 2001 From: Mygod Date: Tue, 17 Nov 2020 22:38:16 -0500 Subject: [PATCH 11/12] Fix computation of upperTail --- src/views/pokemon.mustache | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/views/pokemon.mustache b/src/views/pokemon.mustache index 479bd9c..ad962ee 100644 --- a/src/views/pokemon.mustache +++ b/src/views/pokemon.mustache @@ -482,11 +482,11 @@ * * @param numerator An integer indicating the number of events. * @param denominator An integer indicating the number of trials. - * @param upperTail A real number in [0, 1] indicating % area of upper tail. * @param lowerTail A real number in [0, 1] indicating % area of lower tail. + * @param upperTail A real number in [0, 1] indicating % area of upper tail. * @param accuracy Accuracy of the estimate. Omit to determine automatically based on denominator (.1/denominator). */ - function computeBinomialConfidenceIntervals(numerator, denominator, upperTail = 0, lowerTail = .05, accuracy = null) { + function computeBinomialConfidenceIntervals(numerator, denominator, lowerTail = .05, upperTail = 0, accuracy = null) { if (accuracy === null) { accuracy = .1 / denominator; } @@ -524,7 +524,7 @@ lower = binarySearch(0, likely, (p) => computeBinomialLikelihood(denominator, p, numerator, denominator) > lowerTail); } if (numerator < denominator && upperTail > 0) { - upper = binarySearch(likely, 1, (p) => computeBinomialLikelihood(denominator, p, numerator, denominator) < upperTail); + upper = binarySearch(likely, 1, (p) => computeBinomialLikelihood(denominator, p, 0, numerator) < upperTail); } return [lower, likely, upper]; } From 14eeb780ef1fcca856f67ea74c8b217020c5e7f3 Mon Sep 17 00:00:00 2001 From: Mygod Date: Tue, 17 Nov 2020 22:44:37 -0500 Subject: [PATCH 12/12] Refine withPrecision on edge cases --- src/views/pokemon.mustache | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/pokemon.mustache b/src/views/pokemon.mustache index ad962ee..f95fec0 100644 --- a/src/views/pokemon.mustache +++ b/src/views/pokemon.mustache @@ -471,7 +471,7 @@ var shinyDate = $('#shiny-date').val(); var query = `?date=${shinyDate}`; - const withPrecision = (num, denominator) => Number(num.toPrecision(Math.ceil(Math.log10(denominator)))); + const withPrecision = (num, denominator) => Number(num.toPrecision(Math.ceil(Math.log10(1 + denominator)))); const computeShinyRate = (row, type, set, meta) => type === 'sort' ? row.shiny / row.total : row.shiny > 0 ? `1/${withPrecision(row.total / row.shiny, row.total)}` : '0';
{{ID}}{{ID}} {{Pokemon}} {{Shiny Rate}}{{Shiny}}{{Shiny}} {{Total}}
{{ID}} {{Pokemon}} {{Shiny Rate}}{{Shiny Rate 95% Confidence}} {{Shiny}} {{Total}}
{{ID}} {{Pokemon}}{{Shiny Rate}}{{Shiny Rate}} {{Shiny Rate 95% Confidence}} {{Shiny}} {{Total}}