2026-03-29 00:00:17 -04:00

210 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html><head><script src="/livereload.js?mindelay=10&amp;v=2&amp;port=1313&amp;path=livereload" data-no-instant defer></script>
<title>Dane Sabo - I Built a Rating System for My Pickleball League (And Definitely Didn&#39;t Cook the Books) </title>
<link rel="stylesheet" type="text/css" href="/css/fonts.css">
<link rel="stylesheet" type="text/css" href="/css/fontawesome.css">
<link rel="stylesheet" type="text/css" href="/css/styles.min.c2acad37d1be0e44722e7f234c116caef79124df36955647824f8b10da479ba3.css">
<link rel="stylesheet" type="text/css" href="/css/dark-theme.css">
<meta charset="UTF-8">
<meta name="author" content="Dane Sabo">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<header class="page-header">
<div class="myname">
<h2><a href="http://localhost:1313/">Dane Sabo</a></h2>
</div>
<nav>
<ul class="navbar">
<li class="">
<a href="/cv/">
<span>CV</span>
</a>
</li>
<li class="">
<a href="/projects/">
<span>Projects</span>
</a>
</li>
<li class="">
<a href="/blog/">
<span>Blog</span>
</a>
</li>
<li class="">
<a href="/contact/">
<span>About Me</span>
</a>
</li>
</ul>
</nav>
</header>
<div id="content">
<main>
<article>
<h1 class="page-title blog">I Built a Rating System for My Pickleball League (And Definitely Didn&#39;t Cook the Books)</h1>
<p class="blog-post-info">Posted: <time>2026-02-26</time>
<span class="blog-taxonomy-info"> &nbsp; | &nbsp; Categories:
<a class="blog-taxonomy-info" href="/categories/projects">Projects</a>, <a class="blog-taxonomy-info" href="/categories/recreation">Recreation</a>
</span>
<span class="blog-taxonomy-info"> &nbsp; | &nbsp; Tags:
<a class="blog-taxonomy-info" href="/tags/elo">ELO</a>, <a class="blog-taxonomy-info" href="/tags/pickleball">pickleball</a>, <a class="blog-taxonomy-info" href="/tags/rating-systems">rating systems</a>, <a class="blog-taxonomy-info" href="/tags/statistics">statistics</a>
</span>
</p>
<div class="blog-post-content">
<p>After running my pickleball league with Glicko-2 for over a month, I realized the system had problems. So I did what any reasonable person would do: I threw it out and rebuilt it from scratch with an ELO system.</p>
<p>And yes, I happen to be the biggest beneficiary of the change. Coincidence? Probably. Let me explain the math, and you can be the judge.</p>
<h2 id="the-problem-glicko-2-was-overkill">The Problem: Glicko-2 Was Overkill</h2>
<p>Glicko-2 is a sophisticated rating system designed for competitive chess. It tracks three values per player:</p>
<ul>
<li><strong>Rating</strong> — Your skill estimate (default: 1500)</li>
<li><strong>Rating Deviation</strong> — How <em>uncertain</em> the system is about your skill</li>
<li><strong>Volatility</strong> — How <em>consistent</em> you are</li>
</ul>
<p>The math involves converting to different scales, computing probabilities with hyperbolic functions, and solving iteratively for new volatility. It&rsquo;s clever, but for a casual league of six players, it&rsquo;s like bringing a sports car to a parking lot.</p>
<p>But the real problem was this: I added a <em>margin bonus</em> to account for wins by different margins (winning 11-9 vs 11-2). The formula?</p>
<pre tabindex="0"><code>weighted_score = base_score + tanh(margin/11 × 0.3) × (base_score - 0.5)
</code></pre><p><strong>Translation:</strong> I took the hyperbolic tangent of a fraction, multiplied by an arbitrary constant (why 0.3? No particular reason), and called it science.</p>
<p>This is what&rsquo;s known as &ldquo;making stuff up.&rdquo; It had no theoretical basis and was impossible to explain to players.</p>
<h2 id="the-doubles-problem">The Doubles Problem</h2>
<p>The old system calculated team ratings by averaging both partners&rsquo; ratings. Sounds reasonable, right?</p>
<p>Until you think about it: If you (1400) play with a strong partner (1700) against two 1550s, the system thinks it&rsquo;s an even match. But <em>you</em> were carried by a stronger player! Winning that match shouldn&rsquo;t boost your rating as much as winning with a weaker partner.</p>
<p>The system didn&rsquo;t account for partner strength, making it unfair for everyone.</p>
<h2 id="enter-pure-elo">Enter: Pure ELO</h2>
<p>ELO is elegantly simple. Every player has <em>one number</em> representing their skill. When two players compete:</p>
<ol>
<li>Calculate the probability that one player beats the other based on rating difference</li>
<li>Compare expected performance to actual performance</li>
<li>Adjust ratings based on the difference</li>
</ol>
<p>The key formula is:</p>
<pre tabindex="0"><code>Expected Win Probability = 1 / (1 + 10^((opponent_rating - your_rating) / 400))
</code></pre><p>If you&rsquo;re 1500 and your opponent is 1500, you should win 50% of the time. If you&rsquo;re 1600 and they&rsquo;re 1500, you should win about 64% of the time. Simple.</p>
<p>After a match:</p>
<pre tabindex="0"><code>Rating Change = K × (Actual Performance - Expected Performance)
</code></pre><p>Where <code>K = 32</code> (how much weight each match carries) and <code>Actual Performance</code> is your <em>per-point performance</em>:</p>
<pre tabindex="0"><code>Actual Performance = Points Scored / Total Points Played
</code></pre><p>Win 11-9? That&rsquo;s 0.55 (55% of points). Win 11-2? That&rsquo;s 0.846 (84.6%). This captures match quality far better than binary win/loss.</p>
<h2 id="the-secret-sauce-the-effective-opponent-formula">The Secret Sauce: The Effective Opponent Formula</h2>
<p>In doubles, we use:</p>
<pre tabindex="0"><code>Effective Opponent Rating = Opponent1 + Opponent2 - Your Teammate
</code></pre><p><strong>Why this works:</strong></p>
<p>If your teammate is strong, the effective opponent rating drops—because your teammate made the match easier. If your teammate is weak, the effective opponent rating rises—because you were undermanned.</p>
<p>Beating 1500-rated opponents with a 1600-rated partner? Effective opponent: 1400. You gain less because your partner carried you.</p>
<p>Beating 1500-rated opponents with a 1400-rated partner? Effective opponent: 1600. You gain more because you did heavy lifting.</p>
<p>This is <em>fair</em>.</p>
<h2 id="the-migration-before-and-after">The Migration: Before and After</h2>
<p>Here&rsquo;s where things get spicy. I replayed all 29 historical matches through the new ELO system:</p>
<table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
<tr style="background-color: #f0f0f0;">
<th style="border: 1px solid #ddd; padding: 10px; text-align: left;">Player</th>
<th style="border: 1px solid #ddd; padding: 10px; text-align: right;">Old Glicko-2</th>
<th style="border: 1px solid #ddd; padding: 10px; text-align: right;">New ELO</th>
<th style="border: 1px solid #ddd; padding: 10px; text-align: right;">Change</th>
<th style="border: 1px solid #ddd; padding: 10px; text-align: right;">Matches</th>
</tr>
<tr>
<td style="border: 1px solid #ddd; padding: 10px;">Andrew Stricklin</td>
<td style="border: 1px solid #ddd; padding: 10px; text-align: right;">1651</td>
<td style="border: 1px solid #ddd; padding: 10px; text-align: right;">1538</td>
<td style="border: 1px solid #ddd; padding: 10px; text-align: right;"><span style="color: #c80000;">113</span></td>
<td style="border: 1px solid #ddd; padding: 10px; text-align: right;">19</td>
</tr>
<tr style="background-color: #fafafa;">
<td style="border: 1px solid #ddd; padding: 10px;">David Pabst</td>
<td style="border: 1px solid #ddd; padding: 10px; text-align: right;">1562</td>
<td style="border: 1px solid #ddd; padding: 10px; text-align: right;">1522</td>
<td style="border: 1px solid #ddd; padding: 10px; text-align: right;"><span style="color: #c80000;">40</span></td>
<td style="border: 1px solid #ddd; padding: 10px; text-align: right;">11</td>
</tr>
<tr>
<td style="border: 1px solid #ddd; padding: 10px;">Jacklyn Wyszynski</td>
<td style="border: 1px solid #ddd; padding: 10px; text-align: right;">1557</td>
<td style="border: 1px solid #ddd; padding: 10px; text-align: right;">1514</td>
<td style="border: 1px solid #ddd; padding: 10px; text-align: right;"><span style="color: #c80000;">43</span></td>
<td style="border: 1px solid #ddd; padding: 10px; text-align: right;">9</td>
</tr>
<tr style="background-color: #fafafa;">
<td style="border: 1px solid #ddd; padding: 10px;">Eliana Crew</td>
<td style="border: 1px solid #ddd; padding: 10px; text-align: right;">1485</td>
<td style="border: 1px solid #ddd; padding: 10px; text-align: right;">1497</td>
<td style="border: 1px solid #ddd; padding: 10px; text-align: right;"><span style="color: #00640a;">+11</span></td>
<td style="border: 1px solid #ddd; padding: 10px; text-align: right;">13</td>
</tr>
<tr>
<td style="border: 1px solid #ddd; padding: 10px;">Krzysztof Radziszeski</td>
<td style="border: 1px solid #ddd; padding: 10px; text-align: right;">1473</td>
<td style="border: 1px solid #ddd; padding: 10px; text-align: right;">1476</td>
<td style="border: 1px solid #ddd; padding: 10px; text-align: right;"><span style="color: #00640a;">+3</span></td>
<td style="border: 1px solid #ddd; padding: 10px; text-align: right;">25</td>
</tr>
<tr style="background-color: #fafafa;">
<td style="border: 1px solid #ddd; padding: 10px;"><strong>Dane Sabo</strong></td>
<td style="border: 1px solid #ddd; padding: 10px; text-align: right;">1290</td>
<td style="border: 1px solid #ddd; padding: 10px; text-align: right;">1449</td>
<td style="border: 1px solid #ddd; padding: 10px; text-align: right;"><strong><span style="color: #00640a;">+159</span></strong></td>
<td style="border: 1px solid #ddd; padding: 10px; text-align: right;">25</td>
</tr>
</table>
<h3 id="observations">Observations</h3>
<p><strong>The Rating Spread Compressed</strong></p>
<p>The old system spread players across 361 rating points. The new system compresses them into 89 points. This makes sense—we&rsquo;re a recreational group, not chess grandmasters. The new system rates us fairly within a tighter band.</p>
<p><strong>The Winners</strong></p>
<ul>
<li><strong>Dane Sabo</strong>: +159 points. The old system penalized him for losses with weaker partners. The effective opponent formula gives credit for &ldquo;carrying.&rdquo; (Purely coincidental that I benefit from my own math.)</li>
<li><strong>Eliana Crew</strong>: +11 points</li>
<li><strong>Krzysztof Radziszeski</strong>: +3 points</li>
</ul>
<p><strong>The Losers</strong></p>
<ul>
<li><strong>Andrew Stricklin</strong>: 113 points. Still ranked #1, but the old system over-credited wins with strong partners.</li>
<li><strong>Jacklyn Wyszynski</strong>: 43 points</li>
<li><strong>David Pabst</strong>: 40 points</li>
</ul>
<h2 id="a-note-on-conflicts-of-interest">A Note on Conflicts of Interest</h2>
<p>You may notice that the system designer (me) is also the biggest beneficiary of the new ratings, gaining a convenient 159 points.</p>
<p>I want to assure you this is <em>purely coincidental</em> and the result of <em>rigorous mathematical analysis</em>, not at all influenced by the fact that I was tired of being ranked last.</p>
<p>The new formulas are based on <em>sound theoretical principles</em> that just <em>happen</em> to conclude I was being unfairly penalized all along.</p>
<p><em>Trust the math.</em> 😉</p>
<h2 id="why-this-system-works">Why This System Works</h2>
<p><strong>For a small league:</strong></p>
<ul>
<li>Simple to understand (one rating per player)</li>
<li>Fair to individual skill (per-point scoring)</li>
<li>Respects partnership (effective opponent formula)</li>
<li>Transparent (you can calculate rating changes yourself)</li>
<li>Fast convergence (5-10 matches to stabilize a rating)</li>
</ul>
<p><strong>The bottom line:</strong> Your rating now reflects your true skill more accurately than before. Even if it means Dane finally looks respectable.</p>
</div>
</article>
</main>
</div><footer>
<span>This website was built using Hugo and the &#39;notrack&#39; theme.</span>
</footer>
</body>
</html>