From 9247d1fb5b5f00214899b115b3a021b4498de5c3 Mon Sep 17 00:00:00 2001 From: "Steinar H. Gunderson" Date: Sat, 20 May 2023 23:10:34 +0200 Subject: [PATCH] Add per-point CIs. --- ultimate.js | 60 +++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/ultimate.js b/ultimate.js index 54348cd..7c265c2 100644 --- a/ultimate.js +++ b/ultimate.js @@ -74,7 +74,11 @@ function add_3cell_ci(tr, ci) { const width = 100; const height = 20; let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - svg.classList.add('ci'); + if (ci.inverted === true) { + svg.classList.add('invertedci'); + } else { + svg.classList.add('ci'); + } svg.setAttribute('width', width); svg.setAttribute('height', height); @@ -513,6 +517,32 @@ function make_efficiency_ci(points_won, points_completed, z) return ci; } +// Ds, throwaways and drops can happen multiple times per point, +// so they are Poisson distributed. +// +// Modified Wald (recommended by http://www.ine.pt/revstat/pdf/rs120203.pdf +// since our rates are definitely below 2 per point). +function make_poisson_ci(val, num, z, inverted) +{ + let low = (val == 0) ? 0.0 : ((val - 0.5) - Math.sqrt(val - 0.5)) / num; + let high = (val == 0) ? -Math.log(0.025) / num : ((val + 0.5) + Math.sqrt(val + 0.5)) / num; + + // Fix the signs so that we don't get -0.00. + low = Math.max(low, 0.0); + + // The display range of 0 to 0.25 is fairly arbitrary. So is the desired 0.05 per point. + let avg = val / num; + return { + 'val': avg, + 'lower_ci': low, + 'upper_ci': high, + 'min': 0.0, + 'max': 0.25, + 'desired': 0.05, + 'inverted': inverted, + }; +} + function make_table_general(players) { let rows = []; { @@ -744,24 +774,28 @@ function make_table_per_point(players) { let touches = 0; for (const [q,p] of Object.entries(players)) { if (q === 'globals') continue; + + // Can only happen once per point, so these are binomials. + let ci_goals = make_binomial_ci(p.goals, p.points_played, z); + let ci_assists = make_binomial_ci(p.assists, p.points_played, z); + let ci_hockey_assists = make_binomial_ci(p.hockey_assists, p.points_played, z); + // Arbitrarily desire at least 10% (not everybody can score or assist). + ci_goals.desired = 0.1; + ci_assists.desired = 0.1; + ci_hockey_assists.desired = 0.1; + let row = document.createElement('tr'); add_3cell(row, p.name, 'name'); // TODO: number? + add_3cell_ci(row, ci_goals); + add_3cell_ci(row, ci_assists); + add_3cell_ci(row, ci_hockey_assists); + add_3cell_ci(row, make_poisson_ci(p.defenses, p.points_played, z)); + add_3cell_ci(row, make_poisson_ci(p.throwaways, p.points_played, z, true)); + add_3cell_ci(row, make_poisson_ci(p.drops, p.points_played, z, true)); if (p.points_played > 0) { - add_3cell(row, p.goals == 0 ? 0 : (p.goals / p.points_played).toFixed(2)); - add_3cell(row, p.assists == 0 ? 0 : (p.assists / p.points_played).toFixed(2)); - add_3cell(row, p.hockey_assists == 0 ? 0 : (p.hockey_assists / p.points_played).toFixed(2)); - add_3cell(row, p.defenses == 0 ? 0 : (p.defenses / p.points_played).toFixed(2)); - add_3cell(row, p.throwaways == 0 ? 0 : (p.throwaways / p.points_played).toFixed(2)); - add_3cell(row, p.drops == 0 ? 0 : (p.drops / p.points_played).toFixed(2)); add_3cell(row, p.touches == 0 ? 0 : (p.touches / p.points_played).toFixed(2)); } else { add_3cell(row, 'N/A'); - add_3cell(row, 'N/A'); - add_3cell(row, 'N/A'); - add_3cell(row, 'N/A'); - add_3cell(row, 'N/A'); - add_3cell(row, 'N/A'); - add_3cell(row, 'N/A'); } rows.push(row); -- 2.39.2