]> git.sesse.net Git - pkanalytics/commitdiff
Start adding CIs for efficiency.
authorSteinar H. Gunderson <sgunderson@bigfoot.com>
Sat, 20 May 2023 20:49:20 +0000 (22:49 +0200)
committerSteinar H. Gunderson <sgunderson@bigfoot.com>
Sat, 20 May 2023 20:50:01 +0000 (22:50 +0200)
ultimate.css
ultimate.js

index c62f18bc281adaf9f8456a35984cff138b70eb81..40f5d0f4d26d69ef3520a1462f85dc1165f5e15b 100644 (file)
@@ -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; }
index 97a4787f636c899d2066afd1305ba5d2be739a44..bb116a2f29aa3f79e479e1a68a225b7b02979d67 100644 (file)
@@ -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);