From 9529d03ade5eef7492a970925064ad4823cbca39 Mon Sep 17 00:00:00 2001 From: "Steinar H. Gunderson" Date: Sat, 20 May 2023 22:49:20 +0200 Subject: [PATCH] Start adding CIs for efficiency. --- ultimate.css | 15 +++++++ ultimate.js | 109 +++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 116 insertions(+), 8 deletions(-) diff --git a/ultimate.css b/ultimate.css index c62f18b..40f5d0f 100644 --- a/ultimate.css +++ b/ultimate.css @@ -60,3 +60,18 @@ td:not(.pad) { td.name { padding-right: 20px; } + +.ci, .invertedci { + vertical-align: middle; + margin-left: 8px; +} + +.ci .marker { stroke: #000; stroke-width: 2px; } +.ci .range.s0 { fill: #fdd; } +.ci .range.s1 { fill: #dfd; } + +.invertedci .marker { stroke: #000; stroke-width: 2px; } +.invertedci .range.s0 { fill: #dfd; } +.invertedci .range.s1 { fill: #fdd; } + +.bar { fill: steelblue; } diff --git a/ultimate.js b/ultimate.js index 97a4787..bb116a2 100644 --- a/ultimate.js +++ b/ultimate.js @@ -52,6 +52,63 @@ function add_3cell(tr, text, cls) { } else { element.classList.add(cls); } + return element; +} + +function add_3cell_ci(tr, ci) { + console.log(ci); + if (isNaN(ci.val)) { + add_3cell(tr, 'N/A'); + return; // FIXME: some SVG padding needed + } + let element = add_3cell(tr, ci.val.toFixed(2)); + let to_x = (val) => { return width * (val - ci.min) / (ci.max - ci.min); }; + + // Container. + const width = 100; + const height = 20; + let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.classList.add('ci'); + svg.setAttribute('width', width); + svg.setAttribute('height', height); + + // The good (green) and red (bad) ranges. + let s0 = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + s0.classList.add('range'); + s0.classList.add('s0'); + s0.setAttribute('width', to_x(ci.desired)); + s0.setAttribute('height', height); + s0.setAttribute('x', '0'); + + let s1 = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + s1.classList.add('range'); + s1.classList.add('s1'); + s1.setAttribute('width', width - to_x(ci.desired)); + s1.setAttribute('height', height); + s1.setAttribute('x', to_x(ci.desired)); + + // Confidence bar. + let bar = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); + bar.classList.add('bar'); + bar.setAttribute('width', to_x(ci.upper_ci) - to_x(ci.lower_ci)); + bar.setAttribute('height', height / 3); + bar.setAttribute('x', to_x(ci.lower_ci)); + bar.setAttribute('y', height / 3); + + // Marker line for average. + let marker = document.createElementNS('http://www.w3.org/2000/svg', 'line'); + marker.classList.add('marker'); + marker.setAttribute('x1', to_x(ci.val)); + marker.setAttribute('x2', to_x(ci.val)); + marker.setAttribute('y1', height / 6); + marker.setAttribute('y2', height * 5 / 6); + + svg.appendChild(s0); + svg.appendChild(s1); + svg.appendChild(bar); + svg.appendChild(marker); + + element.appendChild(svg); } function process_matches(json) { @@ -414,6 +471,42 @@ function write_main_menu(chosen_category) { document.getElementById('mainmenu').replaceChildren(...elems); } +// https://en.wikipedia.org/wiki/1.96#History +const z = 1.959964; + +function make_binomial_ci(val, num, z) { + let avg = val / num; + + // https://en.wikipedia.org/wiki/Binomial_proportion_confidence_interval#Wilson_score_interval + let low = (avg + z*z/(2*num) - z * Math.sqrt(avg * (1.0 - avg) / num + z*z/(4*num*num))) / (1 + z*z/num); + let high = (avg + z*z/(2*num) + z * Math.sqrt(avg * (1.0 - avg) / num + z*z/(4*num*num))) / (1 + z*z/num); + + // Fix the signs so that we don't get -0.00. + low = Math.max(low, 0.0); + return { + 'val': avg, + 'lower_ci': low, + 'upper_ci': high, + 'min': 0.0, + 'max': 1.0, + }; +} + +// These can only happen once per point, but you get -1 and +1 +// instead of 0 and +1. After we rewrite to 0 and 1, it's a binomial, +// and then we can rewrite back. +function make_efficiency_ci(points_won, points_completed, z) +{ + let ci = make_binomial_ci(points_won, points_completed, z); + ci.val = 2.0 * ci.val - 1.0; + ci.lower_ci = 2.0 * ci.lower_ci - 1.0; + ci.upper_ci = 2.0 * ci.upper_ci - 1.0; + ci.min = -1.0; + ci.max = 1.0; + ci.desired = 0.0; // Desired = positive efficiency. + return ci; +} + function make_table_general(players) { let rows = []; { @@ -432,27 +525,27 @@ function make_table_general(players) { let row = document.createElement('tr'); let pm = p.goals + p.assists + p.hockey_assists + p.defenses - p.throwaways - p.drops; let soft_pm = p.offensive_soft_plus + p.defensive_soft_plus - p.offensive_soft_minus - p.defensive_soft_minus; - let o_efficiency = (p.offensive_points_won / p.offensive_points_completed) * 2 - 1; - let d_efficiency = (p.defensive_points_won / p.defensive_points_completed) * 2 - 1; + let o_efficiency = make_efficiency_ci(p.offensive_points_won, p.offensive_points_completed, z); + let d_efficiency = make_efficiency_ci(p.defensive_points_won, p.defensive_points_completed, z); add_3cell(row, p.name, 'name'); // TODO: number? add_3cell(row, pm > 0 ? ('+' + pm) : pm); add_3cell(row, soft_pm > 0 ? ('+' + soft_pm) : soft_pm); - add_3cell(row, p.offensive_points_completed > 0 ? o_efficiency.toFixed(2) : 'N/A'); - add_3cell(row, p.defensive_points_completed > 0 ? d_efficiency.toFixed(2) : 'N/A'); + add_3cell_ci(row, o_efficiency); + add_3cell_ci(row, d_efficiency); add_3cell(row, p.points_played); rows.push(row); } // Globals. let globals = players['globals']; - let o_efficiency = (globals.offensive_points_won / globals.offensive_points_completed) * 2 - 1; - let d_efficiency = (globals.defensive_points_won / globals.defensive_points_completed) * 2 - 1; + let o_efficiency = make_efficiency_ci(globals.offensive_points_won, globals.offensive_points_completed, z); + let d_efficiency = make_efficiency_ci(globals.defensive_points_won, globals.defensive_points_completed, z); let row = document.createElement('tr'); add_3cell(row, ''); add_3cell(row, ''); add_3cell(row, ''); - add_3cell(row, globals.offensive_points_completed > 0 ? o_efficiency.toFixed(2) : 'N/A'); - add_3cell(row, globals.defensive_points_completed > 0 ? d_efficiency.toFixed(2) : 'N/A'); + add_3cell_ci(row, o_efficiency); + add_3cell_ci(row, d_efficiency); add_3cell(row, globals.points_played); rows.push(row); -- 2.39.2