<?xml version="1.0" encoding="UTF-8"?>
<rss  xmlns:atom="http://www.w3.org/2005/Atom" 
      xmlns:media="http://search.yahoo.com/mrss/" 
      xmlns:content="http://purl.org/rss/1.0/modules/content/" 
      xmlns:dc="http://purl.org/dc/elements/1.1/" 
      version="2.0">
<channel>
<title>jeremie.spagnolo</title>
<link>https://spazznolo.github.io/</link>
<atom:link href="https://spazznolo.github.io/index.xml" rel="self" type="application/rss+xml"/>
<description>Sports statistics, applied modeling, and research notes by Jeremie Spagnolo.</description>
<generator>quarto-1.10.18</generator>
<lastBuildDate>Wed, 17 May 2023 00:00:00 GMT</lastBuildDate>
<item>
  <title>Goalie Performance</title>
  <link>https://spazznolo.github.io/research/goalie-performance/</link>
  <description><![CDATA[ 





<div class="article-reading-time">
<p>14 min read</p>
</div>
<div class="archive-notice">
<p>This article consolidates five posts published between 2023 and 2024.</p>
</div>
<section id="starting-with-empirical-bayes" class="level2">
<h2 class="anchored" data-anchor-id="starting-with-empirical-bayes">Starting with empirical Bayes</h2>
<p>Recall from the <a href="../../2022/03/28/goalie-consistency-1.html">introductory paragraph</a> of the series on goalie consistency: “Goaltenders make up the least predictable position in hockey. Their behavior confounds analysts and casual fans alike. It isn’t uncommon for a good goalie to have a below replacement level year, or for an unknown goalie to come in and dominate the league for a stretch of time. This may partly explain the relative dearth of analysis on goalies - they’re voodoo, it’s often said.”</p>
<p>This analysis focuses on goalie performance. It aims to enhance the estimation of goalie performance using an empirical Bayes framework. Similar applications were outlined in <a href="https://hockey-graphs.com/2018/06/21/comparing-scoring-talent-with-empirical-bayes/">previous</a> <a href="http://varianceexplained.org/r/empirical_bayes_baseball/">papers</a>. The strategy has several advantages over frequentist methods, chiefly the ability to measure uncertainty, which is crucial in describing goalie performance. The idea is first sketched using raw save percentage as the performance metric, then refined.</p>
<p>The empirical Bayes approach involves two steps. The first step is to use observed data to fit a prior distribution, and the second step is to update this prior using observed data.</p>
<section id="step-one" class="level3">
<h3 class="anchored" data-anchor-id="step-one">Step One</h3>
<p>Imagine a scenario where a new and unknown goalie emerges. What probability can we assign to their career Fenwick* save percentage being .930, .940, or .950?</p>
<p><em>Fenwick save percentage refers to the total percentage of unblocked shots saved, including shots that miss the net. It has been established that goalie skill correlates with the ability to make players miss the net.</em></p>
<p>To represent the possible career Fenwick save percentage for a new goalie, we can utilize the provided histogram of career 5v5 Fenwick save percentages for goalies who have faced 200+ shots (including those facing less adds noise - don’t worry, it’ll be addressed in a future post).</p>
<p><img src="https://spazznolo.github.io/figs/goalie-performance-1-1.png" class="img-fluid" style="width:60.0%" alt="Distribution of career 5v5 Fenwick save percentages for goalies facing 200 or more shots."></p>
<p>While the histogram provides a rough distribution, it contains random bumps throughout. To obtain a more structured representation, we fit a distribution to it.</p>
<p>The beta distribution is a commonly used prior when the variable of interest is a percentage, as in the case of the raw save percentage. Fitting a beta distribution to the career Fenwick save percentage distribution yields the following result.</p>
<p><img src="https://spazznolo.github.io/figs/goalie-performance-1-2.png" class="img-fluid" style="width:60.0%" alt="Fitted beta distribution for career Fenwick save percentages."></p>
<p>The fit is… not really good, and alternative distributions such as gamma or Weibull will be explored in future posts to find the best fit, but it serves its purpose as an introduction to the framework. The beta distribution has two hyperparameters, alpha and beta, which can be interpreted as successes (saves) and failures (goals). The fitted beta distribution in this case has hyperparameters 852 and 55.6, indicating that we attribute 852 saves and 55.6 goals to a goalie before knowing anything about them. This corresponds to a .9388 Fenwick save percentage, or, a little below the median save percentage for goalies facing 200+ shots (.9406).</p>
</section>
<section id="step-two" class="level3">
<h3 class="anchored" data-anchor-id="step-two">Step Two</h3>
<p>The second step involves updating the prior distribution with each goalie’s career results. When a goalie has faced only a few shots, their estimated save percentage shrinks towards the mean of the prior distribution, while the uncertainty (represented by variance in the distribution) remains high. As a goalie faces more shots, the uncertainty of their estimated save percentage decreases.</p>
<p>Updating the prior with observed goalie results is relatively straightforward. It requires adding the observed successes and failures (saves and shots) to the hyperparameters of the beta distribution.</p>
<p>As an example, let’s plot the posterior distributions for two 24-year-old goalies: Jake Oettinger and Jeremy Swayman.</p>
<p><img src="https://spazznolo.github.io/figs/goalie-performance-1-3.png" class="img-fluid" style="width:60.0%" alt="Posterior save percentage distributions for Jake Oettinger and Jeremy Swayman."></p>
<p>These posterior distributions offer interesting insights, like:</p>
<ul>
<li>There’s a 60.28% chance that Swayman’s save percentage is higher than Oettinger’s.</li>
<li>There’s a 94.12% chance that Oettinger’s save percentage is higher than average.</li>
<li>There’s a 93.81% chance that Swayman’s save percentage is higher than average.</li>
<li>Oettinger’s distribution is tighter than Swayman’s because he’s faced more shots.</li>
</ul>
<p>It’s important to list all the assumptions with this method:</p>
<ul>
<li>All shots are assumed to be equal.</li>
<li>Age is assumed to be irrelevant.</li>
<li>The prior distribution is assumed to be beta with hyperparameters 852 and 55.6.</li>
<li>Goalies facing less than 200 shots are ignored.</li>
<li>Goalie careers are equal.</li>
<li>Scoring rates are assumed to be constant.</li>
<li>Team systems are assumed to be identical.</li>
</ul>
<p>These will be challenged and addressed below.</p>
<p>Code available here: <a href="https://github.com/spazznolo/goalie-performance/blob/main/posts/post-1.R">https://github.com/spazznolo/goalie-performance/blob/main/posts/post-1.R</a></p>
</section>
</section>
<section id="accounting-for-shot-quality" class="level2">
<h2 class="anchored" data-anchor-id="accounting-for-shot-quality">Accounting for shot quality</h2>
<p>All shots are not equal, in that they do not have the same probability of becoming a goal. This is established. Many Expected Goals (xG) models have been developed to account for this. Fortunately, Peter Tanner’s website <a href="https://moneypuck.com/index.html">MoneyPuck</a> provides detailed data on each unblocked shot in the NHL, including the probability of the shot being a goal according to his <a href="https://moneypuck.com/about.htm">logistic regression model</a>.</p>
<p>To adjust a goalie’s save percentage for shot quality, we can incorporate these expected goal probabilities from MoneyPuck. Usually, after accounting for shot quality, goalie performance is measured by the number of goals saved above expected (GSAx). This changes the metric from a rate (percentage of shots saved) which is bounded by 0 and 1 to one that can include any real number. Unfortunately, the beta distribution only works with rates. One approach to retain the metric as a rate, and thus the beta distribution as a prior, is as follows:</p>
<ul>
<li>Fenwick Save Percentage (FSV%) = 1 - (Goals Against / Fenwick Shots Against)</li>
<li>Expected Fenwick Save Percentage (xFSV%) = 1 - (Expected Goals Against / Fenwick Shots Against)</li>
<li>Median Save Percentage (MSV%) = Median of Goalie (20+ xG faced) Career Save Percentage</li>
<li><strong>Adjusted Save Percentage (AdjSV%) = MSV% + (FSV% - xFSV%)</strong></li>
</ul>
<p>Let’s plot the distribution of career AdjSV% for goalies who have faced 200+ shots. We will also include a fitted beta distribution in white, and a weibull in red.</p>
<p><img src="https://spazznolo.github.io/figs/goalie-performance-2-1.png" class="img-fluid" style="width:60.0%" alt="Career adjusted save percentage distribution with fitted beta and Weibull distributions."></p>
<p>At first glance, goalies’ career AdjSV% seems to follow a weibull distribution! Cool, but we’re going to sidestep this finding for now, because (hint, hint) <em>there maybe be more than one distribution here</em>. So we’re fitting another beta, which means the remaining steps stay the same. The prior is similar, except this time we add 933 saves and 60 goals (up from 852 and 55.6).</p>
<p>Let’s revisit the Jake Oettinger and Jeremy Swayman comparison.</p>
<p><img src="https://spazznolo.github.io/figs/goalie-performance-2-2.png" class="img-fluid" style="width:60.0%" alt="Posterior adjusted save percentage distributions for Jake Oettinger and Jeremy Swayman."></p>
<p>The posteriors change as follows:</p>
<ul>
<li>There’s a 78.77% (previously 60.28%) chance that Swayman’s AdjSV% is better than Oettinger’s.</li>
<li>There’s a 88.28% (previously 94.12%) chance that Oettinger’s AdjSV% is better than the MSV%.</li>
<li>There’s a 97.30% (previously 93.81%) chance that Swayman’s AdjSV% is better than the MSV%.</li>
</ul>
<p>These changes occur because Swayman faces more difficult shots on average, with an xFSV% of 94.07 compared to Oettinger’s 94.39.</p>
<section id="comparing-the-estimates" class="level3">
<h3 class="anchored" data-anchor-id="comparing-the-estimates">Comparing the estimates</h3>
<p>Below is a collection of plots which compare the save percentage metrics discussed above.</p>
<p>A couple of key points:</p>
<ul>
<li>Goalies who have a poor start to their career tend to play fewer games (surprise, surprise).</li>
<li>The relationship between a goalie’s SV% and their AdjSV% seems to strengthen as they face more shots.</li>
<li>A goalie’s AdjSV% converges with their posterior AdjSV% as they face more shots (indicated by the yellow diagonal line).</li>
<li>Due to the previous points, there is heteroskedasticity in the relationship between a goalie’s SV% and their posterior AdjSV%.</li>
<li>There is likely survivorship bias present.</li>
</ul>
<p><img src="https://spazznolo.github.io/figs/goalie-performance-2-3.png" class="img-fluid" style="width:100.0%" alt="Comparison of save percentage metrics and posterior adjusted save percentage."></p>
<p>Code available here: <a href="https://github.com/spazznolo/goalie-performance/blob/main/posts/post-2.R">https://github.com/spazznolo/goalie-performance/blob/main/posts/post-2.R</a></p>
</section>
</section>
<section id="adjusting-for-age" class="level2">
<h2 class="anchored" data-anchor-id="adjusting-for-age">Adjusting for age</h2>
<p>Like shot quality, the effect of age on performance is a well-researched concept. It has been shown that goalies tend to improve as they age, peak, and then recede for, well, forever. This concept is typically called an <em>age curve</em>. Though goalie age curves are already available elsewhere, they differ slightly, so I’m going to define a custom age curve based on the Moneypuck dataset.</p>
<ul>
<li>Obtain goalie birth dates.</li>
<li>Derive goalie age for each game.</li>
<li>Define new analysis population (we won’t find every goalie’s date of birth).</li>
<li>Explore age.</li>
<li>Adjust for age.</li>
</ul>
<section id="finding-goalie-birth-dates" class="level3">
<h3 class="anchored" data-anchor-id="finding-goalie-birth-dates">Finding goalie birth dates</h3>
<p>I scraped hockey-reference for each goalie’s date of birth (code available <a href="https://github.com/spazznolo/goalie-consistency/blob/main/import/scrape_goalie_data.R">here</a>). By combining the goalies’ birth dates with the dates of each of their games, I determined their exact age for every game played.</p>
<p>The analysis population changes as follows (due to incomplete linkage):</p>
<ul>
<li>Goalie population drops from 315 to 308.</li>
<li>Harmonic mean of shots against rises from 12,690 to 12,721 (mean rises, 4,198 to 4,286).</li>
<li>Harmonic mean of AdjSV% stays at .939 (mean rises, .932 to .933).</li>
</ul>
<p>They barely differ.</p>
</section>
<section id="exploring-age" class="level3">
<h3 class="anchored" data-anchor-id="exploring-age">Exploring age</h3>
<p>Let’s start by simply grouping shots into bins by goalie age (rounded to the first decimal, ex: 26.0, 26.1, etc.), and then calculating the group-wide save percentage. Here’s what that looks like, with points becoming paler for smaller groups sizes:</p>
<p><img src="https://spazznolo.github.io/figs/goalie-performance-3-1.png" class="img-fluid" style="width:60.0%" alt="Save percentage by goalie age, with lighter points for smaller groups."></p>
<p>Some thoughts:</p>
<ul>
<li>Most shots are taken on goalies aged 23-35.</li>
<li>If you squint, you can see a little bit of an age curve here.</li>
<li>This plot is riddled with bias (particularly for goalies with short or long careers).</li>
</ul>
<p>Let’s fix some of the bias above with a few changes. We’ll follow a well-worn strategy <a href="https://hockey-graphs.com/2017/03/23/a-new-look-at-aging-curves-for-nhl-skaters-part-1">seemingly</a> developed by Tango Tiger <a href="http://www.tangotiger.net/aging.html">here</a> called the delta method.</p>
<p>Here are the steps:</p>
<ul>
<li>Take change in save percentage (dSV%) from each goalie’s age to the next.</li>
<li>Take the harmonic mean of dSV% for each age as the the observed change in SV%.</li>
<li>Clip off underrepresented ages (-21, 39+).</li>
<li>Clip off the final year of each goalie’s career.</li>
<li>Take the cumulative sum of dSV% throughout the retained age range.</li>
</ul>
<p>Altogether, this gives us the curve below:</p>
<p><img src="https://spazznolo.github.io/figs/goalie-performance-3-2.png" class="img-fluid" style="width:60.0%" alt="Smoothed goalie age curve based on cumulative changes in save percentage."></p>
<p>Some thoughts:</p>
<ul>
<li>With this method, goalies peak around the age of 25.</li>
<li>This agrees with <a href="https://hockeyviz.com/txt/age22">some</a> past research.</li>
<li>This disagrees with <a href="https://hockey-graphs.com/2014/03/21/how-well-do-goalies-age-a-look-at-a-goalie-aging-curve/">other</a> past research.</li>
</ul>
</section>
<section id="applying-the-adjustment" class="level3">
<h3 class="anchored" data-anchor-id="applying-the-adjustment">Applying the adjustment</h3>
<p>The cleanest way that I can think of adjusting for age is to bake it into the current framework which already adjusts shots by their probability of becoming a goal. This can easily by done by first setting the peak (age 25) as the standard and then adjusting for all other ages, so that, for example, an xFSV% of 0.940 at age 25 is equivalent to an xFSV% of 0.93976 at age 22 (0.94000 - 0.000237) and an xFSV% of 0.93586 at age 38 (0.94000 - 0.00414).</p>
<div class="equation-note">
<ul>
<li>Adjusted (xG) Save Percentage (AdjSV%) = MSV% + (FSV% - xFSV%)</li>
<li>Age Curve Adjustement (acAdj) = f(age), where f is the smoothed curve in the plot above.</li>
<li><strong>Adjusted (Age + xG) Save Percentage (AdjSV%) = MSV% + (FSV% - xFSV% + acAdj)</strong></li>
</ul>
</div>
<p>With this adjustment, Lundqvist’s career save percentages work out like this:</p>
<ul>
<li>SV%: 0.948638</li>
<li>SQ AdjSV%: 0.947320</li>
<li>Age SQ AdjSV%: 0.948619</li>
<li>posterior SQ AdjSV%: 0.947082</li>
<li>posterior Age SQ AdjSV%: 0.948338</li>
</ul>
<p>Code available here: <a href="https://github.com/spazznolo/goalie-performance/blob/main/posts/post-3.R">https://github.com/spazznolo/goalie-performance/blob/main/posts/post-3.R</a></p>
</section>
</section>
<section id="career-length-and-the-prior" class="level2">
<h2 class="anchored" data-anchor-id="career-length-and-the-prior">Career length and the prior</h2>
<section id="the-200-shot-cutoff" class="level3">
<h3 class="anchored" data-anchor-id="the-200-shot-cutoff">The 200-shot cutoff</h3>
<p>The 200 shot cut-off surprisingly filters out 90 out of 314 goalies (or, 28.7%). To illustrate this, here’s the cumulative distribution function of career shots faced for goalies:</p>
<p><img src="https://spazznolo.github.io/figs/goalie-six-one.png" class="img-fluid" style="width:60.0%" alt="Cumulative distribution of career shots faced for goalies."></p>
<p>This is a problem. One possible solution to this was proposed by <a href="http://varianceexplained.org/r/empirical_bayes_baseball/">David Robinson</a> in his Baysian series on baseball. Instead of fitting a beta distribution to batting averages, he fit a negative binomial distribution using 1) at bats and 2) hits. This can easily be applied to goalies using 1) shots faced and 2) adjusted saves. It has <em>its</em> problems, though (which, of course, David addresses in a string of fantastic blog posts, eventually turning into a <a href="https://drob.gumroad.com/l/empirical-bayes">book</a>). The main problem is that it introduces bias - goalies who perform well are likely to get more opportunities to play compared to those who perform poorly. As proof, here’s a plot showing the average AdjSV% by career seasons played:</p>
<p><img src="https://spazznolo.github.io/figs/goalie-six-two.png" class="img-fluid" style="width:60.0%" alt="Average adjusted save percentage by career seasons played."></p>
<p>Because of this bias, we’re faced with a compromise: Do we weigh the observations by shots faced, which will lean the analysis towards better performing goalies, or do we treat all goalie careers equally, losing nearly 30% of our population (albeit less than 1% of shots faced) at the same time? Or… is there an alternative?</p>
<p>David’s alternative was to fit a beta-binomial regression of batting averages on at-bats. Essentially, each at-bat total has its own prior. Though this is a good idea for his use case, it isn’t great for mine. The problem is that I want to build a decision making tool for goalies <em>who are still playing</em>, and we don’t know how many shots they will face in the future! This is the point of departure.</p>
</section>
<section id="combining-priors" class="level3">
<h3 class="anchored" data-anchor-id="combining-priors">Combining Priors</h3>
<p>To address these issues, the Bayesian framework can be expanded to include two priors, glued together by a probability. The goalie career AdjSV% density plots are revisited, this time splitting goalies into two groups: those facing -1,500 shots and those facing 1,500+. From these group distributions, two separate priors can be built, and they can be merged by including a probability of a goalie belonging to each group, which can change as they face more shots.</p>
<p><img src="https://spazznolo.github.io/figs/goalie-six-three.png" class="img-fluid" style="width:60.0%" alt="Career adjusted save percentage distributions split by career shots faced."></p>
<p>Here’s a simple idea for the class probability described above: run a logistic regression using cumulative shots faced and AdjSV% onto the outcome of whether a goalie ended up facing 1,500+ shots. This model would be easy to train and interpret.</p>
<p>And, it turns out, such a model is well-calibrated:</p>
<p><img src="https://spazznolo.github.io/figs/goalie-six-four.png" class="img-fluid" style="width:60.0%" alt="Calibration of the logistic regression predicting a long goalie career."></p>
<p>The new equation to derive the posterior save percentage then becomes:</p>
<div class="equation-note">
<p><code>P(1500+)*(alphaO + AdjSV%)/(alphaO + betaO + S) + P(-1500)*(alphaU + AdjSV%)/(alphaU + betaU + S)</code></p>
</div>
<p>where:</p>
<ul>
<li>P(1500+) = Predicted probability that goalie faces 1500+ shots in career.</li>
<li>alphaO = alpha from fitted beta distribution on goalies facing 1500+ shots.</li>
<li>betaO = beta from fitted beta distribution on goalies facing 1500+ shots.</li>
<li>P(-1500) = Predicted probability that goalie faces -1500 shots in career.</li>
<li>alphaU = alpha from fitted beta distribution on goalies facing -1500 shots.</li>
<li>betaU = beta from fitted beta distribution on goalies facing -1500 shots.</li>
</ul>
<p>Code available here: <a href="https://github.com/spazznolo/goalie-performance/blob/main/posts/post-4.R">https://github.com/spazznolo/goalie-performance/blob/main/posts/post-4.R</a></p>
</section>
</section>
<section id="contextualizing-experience" class="level2">
<h2 class="anchored" data-anchor-id="contextualizing-experience">Contextualizing experience</h2>
<section id="goalies-in-the-analysis" class="level3">
<h3 class="anchored" data-anchor-id="goalies-in-the-analysis">Goalies in the analysis</h3>
<p>Whenever we decide on a range of years for an analysis (in this case, 2007-2022), we are cutting of goalies mid-career. Some, like Carter Hart, are beginning their careers. Others, like Lundqvist, ended them during in the middle of the analysis period. Since we are not yet adjusting for age, we can’t include these goalies in the analysis. Therefore, only goalies who started their careers post-2007 and ended their careers pre-2022 can be included. This is a 15 year range.</p>
<p>The analysis population changes as follows:</p>
<ul>
<li>Goalie population drops from 315 to 140.</li>
<li>Harmonic mean of shots against rises from 12,690 to 14,568 (mean drops, 4,198 to 3,225).</li>
<li>Harmonic mean of AdjSV% stays at .939 (mean drops, .932 to .927).</li>
</ul>
</section>
<section id="career-length" class="level3">
<h3 class="anchored" data-anchor-id="career-length">Career length</h3>
<p>Let’s start simple: How long is the average goalie career? How many goalies face over 30 shots? How many make it past 200?… All of these questions can be answered by plotting the cumulative distribution function (cdf) of career shots faced for goalies.</p>
<p>Some thoughts:</p>
<ul>
<li>25% of goalies faced 33 shots or less (!).</li>
<li>50% of goalies faced 202 shots or less (that’s about 7 games).</li>
<li>75% of goalies faced 2,606 shots or less.</li>
</ul>
<p>We can gain another perspective on goalie experience by repeating the plot above for goalie career seasons played.</p>
<p><img src="https://spazznolo.github.io/figs/goalie-six-two.png" class="img-fluid" style="width:60.0%" alt="Average goalie career seasons and experience distribution."></p>
<p>Some thoughts:</p>
<ul>
<li>42% of goalies played only one season.</li>
<li>74% of goalies played five seasons or less.</li>
<li>90% of goalies played twelve seasons or less.</li>
</ul>
<p>It should now be obvious that most goalies don’t really have a typical NHL career as we imagine them. Let’s confirm the assumption that goalies with more experience perform better than those with less.</p>
<p>It is difficult to understand a goalie’s path by looking at their save percentage or shots faced in isolation. What’s nice about the empirical Bayesian method is that it considers these measures at the same time. Moreover, we can repeatedly re-evaluate a goalie’s pAdjSV% after each shot they face. We can then plot this posterior over each shot of a goalie’s career to get a sense of their path. In order to extract more insight from this, let’s section goalies by their career shots faced, like this:</p>
<p>Goalies facing:</p>
<ul>
<li>less than 300 shots -&gt; -0300.</li>
<li>more than 300 shots, but less than 1,500 -&gt; -1500.</li>
<li>more than 1,500 shots, but less than 6,000 -&gt; -6000.</li>
<li>more than 6000 -&gt; 6000+.</li>
</ul>
<p>As an example, Braden Holtby’s pAdjSV% after facing various shot totals:</p>
<ul>
<li>0 shots: 0.9417 (as is every goalie’s)</li>
<li>1,000 shots: 0.9421</li>
<li>5,000 shots: 0.9425</li>
<li>10,000 shots: 0.9452</li>
<li>15,000 shots: 0.9450</li>
<li>19,555 shots: 0.9433</li>
</ul>
<p>Some thoughts:</p>
<ul>
<li>Nearly every goalie (77.1%) who faces -6000 shots ends his career with a pAdjSV% below expected.</li>
<li>Goalies facing 1500+ but -6000 seem to fade as their career progresses.</li>
<li>These goalies tend to be backups, facing ~600-1000 shots a season.</li>
<li>This fade could partly be due to aging effects.</li>
</ul>
<p>To get a clearer sense of the dynamics described above, let’s take the group average pAdjSV% through each shot faced.</p>
<p><img src="https://spazznolo.github.io/figs/goalie-six-seven.png" class="img-fluid" style="width:60.0%" alt="Average posterior adjusted save percentage through shots faced by career-size group."></p>
<p>Some thoughts:</p>
<ul>
<li>The effects are much clearer here.</li>
<li>There isn’t much to glean from goalies facing -300 shots.</li>
<li>These short NHL careers are almost certainly due to reasons outside their play in the NHL.</li>
<li>Goalies facing -1500 shots fade quickly. They are given a decent look and fail acutely.</li>
<li>Goalies facing -6000 shots start as the best group through the first 1,000 shots, then fade.</li>
<li>This seemingly unintuitive result is likely due to randomness, and, more interestingly, age.</li>
</ul>
<p>Let’s revisit the unintuive plot comparing pAdjSV% over goalie careers, grouped by career length. This time, we’ll plot the average age of goalies in each group as they face shots over their career.</p>
<p><img src="https://spazznolo.github.io/figs/goalie-six-six.png" class="img-fluid" style="width:60.0%" alt="Average goalie age through shots faced by career-size group."></p>
<p>Some thoughts:</p>
<ul>
<li>Goalies facing -6000 shots are ~1.5 years older than 6000+ goalies throughout their career.</li>
<li>This difference obviously includes the span from age 23 to roughly 27.</li>
<li>This is precisely the age range in which goalies seem to be improving in AdjSV%.</li>
<li>We can adjust for this.</li>
</ul>


</section>
</section>

 ]]></description>
  <category>Bayesian statistics</category>
  <category>Distributions and sampling</category>
  <category>Regression and calibration</category>
  <guid>https://spazznolo.github.io/research/goalie-performance/</guid>
  <pubDate>Wed, 17 May 2023 00:00:00 GMT</pubDate>
</item>
<item>
  <title>NHL Pick Probability</title>
  <link>https://spazznolo.github.io/research/nhl-pick-probability/</link>
  <description><![CDATA[ 





<div class="article-reading-time">
<p>20 min read</p>
</div>
<div class="archive-notice">
<p>This article consolidates six posts published between 2021 and 2024.</p>
</div>
<section id="from-rankings-to-pick-probabilities" class="level2">
<h2 class="anchored" data-anchor-id="from-rankings-to-pick-probabilities">From rankings to pick probabilities</h2>
<p>If you’re picking first at the next NHL draft, you want Lafreniere. If you’re picking second or third, you want Byfield or Stutzle. If you’re picking fourth, or fifth, or sixth, or seventh, you’re picking Rossi or Perffeti, or Raymond, or Drysdale… Notice how the list lengthens as you make your way through the draft? That’s because the uncertainty of a player being better than all other available players increases the deeper you get into the draft. So maybe you really like Rossi, but you’re picking sixth, and you want to be reasonably certain he will still be available. Well, what are the odds Rossi is still available at six? It’s hard to say.</p>
<p>Let’s say we only have our own rankings to go on. Instead of only predicting each player’s draft position, we could create probabilities of each prospect being drafted at each pick. Let’s use Rossi again as an example. Here’s how we might place probabilities on Rossi’s draft result:</p>
<p><img src="https://spazznolo.github.io/figs/first-plot.png" class="img-fluid" style="width:70.0%" alt="Probability distribution of Rossi’s predicted draft result."></p>
<p>This is a probability distribution of Rossi’s predicted draft result. Though this is a step in the right direction, these are only our predictions of where Rossi might go. What if the teams drafting ahead of us don’t rank him as highly? Their probability distribution for Rossi might look like this:</p>
<p><img src="https://spazznolo.github.io/figs/second-plot.png" class="img-fluid" style="width:70.0%" alt="Alternative probability distribution of Rossi’s predicted draft result."></p>
<p>If the teams ahead of us view Rossi closer to the plot above, then he’ll likely slide lower than we predicted he would, and our chances of drafting him are higher than we previously thought. In fact, if we knew what other teams thought of him, we could pretty accurately predict where he’ll still be available in the draft, which allows us to either a) be comfortable we have a strong chance of picking him without trading up, or b) slide down a couple spots, pick up a mid-round pick and still get him. An important thing to remember is that Rossi’s draft position is much less affected by how we think of him than it is by how <a href="http://statsportsconsulting.com/main/wp-content/uploads/Nandakumar_PerfectDraft-1.pdf">everyone else thinks of him</a>.</p>
<p>In practice, no team will ever know exactly how every other team has ranked each prospect. Instead, player-pick probability distributions need to be approximated by other means. Dawson Sprigings outlined one way of doing this for <a href="https://hockey-graphs.com/2016/06/08/nhl-draft-probability-tool/">Hockey-Graphs</a> which used bayesian inference and pro rankings publishers. I’m going to outline another possible way, which uses mock drafts generated by users on <a href="https://www.draftsite.com/">Draft Site</a> to derive probability density functions for each player.</p>
<section id="an-example" class="level3">
<h3 class="anchored" data-anchor-id="an-example">An Example</h3>
<p><a href="https://www.draftsite.com/">Draft Site</a>, gets hundreds of user mock drafts each year. These mock drafts naturally create probability distributions for each player’s potential pick placement. For example, here was Mikko Rantanen’s in 2015 where each user’s draft was weighted equally.</p>
<p><img src="https://spazznolo.github.io/figs/third-plot.png" class="img-fluid" style="width:70.0%" alt="Mikko Rantanen’s 2015 user mock-draft pick distribution."></p>
<p>This distribution should be smoothed, but first, I’d like to address the fact that some mock drafts are more informative than others. A user who can more correctly predict a draft’s order is more valuable than one who cannot. Therefore, larger weights should be given to users who are likely more accurate in their mock draft. Thankfully, there are a couple quality indicators available: a user’s difference to the average user draft, and the number of days before the draft date that a user last updated their mock draft.</p>
</section>
<section id="mock-draft-quality-indicators" class="level3">
<h3 class="anchored" data-anchor-id="mock-draft-quality-indicators">Mock Draft Quality Indicators</h3>
<section id="difference-to-the-average-draft" class="level4">
<h4 class="anchored" data-anchor-id="difference-to-the-average-draft">Difference to the Average Draft</h4>
<p>Mikko Rantanen’s median pick from the raw user data was 9. If a user selected him 25th, they would be 16 spots off. A user’s absolute error rate can be computed for each pick in their mock draft. Below is the relationship of users’ mean absolute pick difference and their mean absolute pick error to the actual draft from 2015-2019. There’s a strong relationship between the two.</p>
<p><img src="https://spazznolo.github.io/figs/fourth-plot.png" class="img-fluid" style="width:70.0%" alt="Relationship between user mock-draft differences and errors."></p>
</section>
<section id="days-to-the-draft" class="level4">
<h4 class="anchored" data-anchor-id="days-to-the-draft">Days to the Draft</h4>
<p>Usually, ranking publications will release a preliminary rankings list about a year before the draft. Then, as the draft approaches and player’s develop – or don’t - the rankings are updated. The plot below demonstrates that user data becomes more accurate as draft day approaches.</p>
<p><img src="https://spazznolo.github.io/figs/fifth-plot.png" class="img-fluid" style="width:70.0%" alt="User mock-draft accuracy by days before the draft."></p>
<p>The variables discussed above were used to (1) filter what are likely low quality drafts and (2) create weights for each user mock draft. Players’ adjusted pick probabilities were then fit and dampened (more information on these decisions is available in the Notes section).</p>
</section>
</section>
<section id="comparing-probability-distributions" class="level3">
<h3 class="anchored" data-anchor-id="comparing-probability-distributions">Comparing Probability Distributions</h3>
<section id="the-effect-of-treatments-on-player-pick-probability-distributions" class="level4">
<h4 class="anchored" data-anchor-id="the-effect-of-treatments-on-player-pick-probability-distributions">The Effect of Treatments on Player-Pick Probability Distributions</h4>
<p>Here’s a visualization of the effects various treatments have on the user rankings.</p>
<p><img src="https://spazznolo.github.io/figs/sixth-plot.png" class="img-fluid" style="width:70.0%" alt="Effects of treatments on player-pick probability distributions."></p>
<p>The downside to user mock drafts is that prospect ranking is likely a hobby for most users. They may mostly rely on second hand information provided by hockey sites, experts, and prospect ranking models. Before going further, it’s important to measure its capacity for prediction. One way of doing this is to build a draft ranking from the data, and then measure its accuracy against professional ranking publishers.</p>
</section>
</section>
<section id="derived-user-rankings-vs-the-pros" class="level3">
<h3 class="anchored" data-anchor-id="derived-user-rankings-vs-the-pros">Derived User Rankings vs the Pros</h3>
<p>Draft rankings are derived from player-pick probability distributions by iterating through each pick of each draft, and drafting the player with the highest probability of being taken. After each pick, the player distributions are re-approximated (more information is available in the Analysis Notes section).</p>
<p>Here is the mean absolute error of derived user draft rankings from 2015-2019 compared to various pro projections. All experts but Bobby Mackenzie, the gold standard of draft projections, have been greyed out. It’s worth noting that the goal of some professional draft analysts is to predict which players will have the best careers, and not necessarily the order they might be picked. Purple is the group average.</p>
<p><img src="https://spazznolo.github.io/figs/seventh-plot.png" class="img-fluid" style="width:70.0%" alt="Mean absolute error of derived user draft rankings versus professional projections."></p>
<p>User data, when properly treated, is competitive with pro ranking publications at predicting draft order. The advantage is that user data has built-in player-pick distributions which can be used to answer important questions about the draft.</p>
</section>
<section id="another-perspective-on-probability-distributions" class="level3">
<h3 class="anchored" data-anchor-id="another-perspective-on-probability-distributions">Another Perspective on Probability Distributions</h3>
<p>Here’s the probability Mikko Rantanen had of being selected at specific picks: 1, 0.1%; 2, 1.0%; 3, 2.6%; 4, 4.6%; 5, 7.9%. Another way to look at this is to say the probability Mikko Rantanen would be selected in the first five picks was 16.3% (the addition of each pick probability for picks 1-5). These cumulative probabilities can be calculated for each pick. Here’s what this looks like on a plot.</p>
<p><img src="https://spazznolo.github.io/figs/eigth-plot.png" class="img-fluid" style="width:70.0%" alt="Cumulative probability of Mikko Rantanen being selected by each pick."></p>
<p>This is called a cumulative distribution (each pick takes the cumulative sum of all previous pick-probabilities). A pick-probability curve like the one visualized above can be derived for each player. Given that these cumulative pick-probabilities are the cornerstone of the analysis, it’s important to measure their accuracy. The fit of these curves can be evaluated by going through each pick and asking the questions: what was the probability of this player being drafted by this pick and was he drafted by this pick?</p>
</section>
<section id="evaluating-the-fit" class="level3">
<h3 class="anchored" data-anchor-id="evaluating-the-fit">Evaluating the Fit</h3>
<p>Either a player was drafted by a certain pick, or they weren’t. This is called a binary event, with values 0 (he wasn’t) and 1 (he was). Mikko Rantanen had a 55.5% probability of being drafted by the eighth pick, and the result was that he wasn’t yet picked (0). The error for the probability attributed to this event can be seen as 0.555-0 = 0.555. Rantanen also had an 86.4% probability of being drafted by the twelfth pick, and the result was that he was picked (1). The error for the probability attributed to this event can be seen as 0.864-1 = -0.136.</p>
<p>Each player has probabilities attached for the first thirty picks of the draft. This is roughly 7,000 events. Errors can be attributed for these events the same way as they were outlined in the previous paragraph. Since the aim is to build well calibrated probability distributions, the brier score will be used to evaluate the fit. The calibration plot is plotted below along with Brier scores.</p>
<p><img src="https://spazznolo.github.io/figs/tenth-plot.png" class="img-fluid" style="width:70.0%" alt="Calibration plot and Brier scores for player-pick probabilities."></p>
<p>The perfect fit is the grey line, where outcomes occur the predicted percentage of time. Whenever a curve slides away underneath the line of perfect fit, like it does with the raw user data (and to a lesser extent the adjusted user data), it means the method tends to be overconfident in its assignment of probabilities. The plot above, along with the Brier scores, suggest the adjusted user data (score ~ 0.0604) is a better fit than raw user data (score ~ 0.198).</p>
<p>Cumulative player-pick probabilities for the 2020 draft are available <a href="https://drive.google.com/file/d/150JF4tPGQ0fRmMsXonIa5CJ_wVVCXBa9/view?usp=drivesdk">here</a>.</p>
</section>
<section id="notes" class="level3">
<h3 class="anchored" data-anchor-id="notes">Notes</h3>
<section id="filtering-users" class="level4">
<h4 class="anchored" data-anchor-id="filtering-users">Filtering Users</h4>
<p>Adjusted to User Average:</p>
<ol type="1">
<li>RMSE to user average &lt; 15</li>
<li>Days to draft &lt; 150</li>
</ol>
<p>Adjusted to Pro Consensus:</p>
<ol type="1">
<li>RMSE to pro consensus &lt; 15</li>
<li>Days to draft &lt; 150</li>
</ol>
</section>
<section id="attributing-weights-to-users" class="level4">
<h4 class="anchored" data-anchor-id="attributing-weights-to-users">Attributing Weights to Users</h4>
<p>A linear regression is fit using: RMSE to user average, days to draft, (and RMSE to pro consensus for data weighted to pro consensus) as predictors and the RMSE to actual draft order as target. User weights are the inverse of the linear model predicted RMSE of user ranking to the actual draft order.</p>
</section>
<section id="fitting-player-distributions" class="level4">
<h4 class="anchored" data-anchor-id="fitting-player-distributions">Fitting Player Distributions</h4>
<p>A gamma distribution is fit to adjusted data.</p>
</section>
<section id="dampening-player-distributions" class="level4">
<h4 class="anchored" data-anchor-id="dampening-player-distributions">Dampening Player Distributions</h4>
<p>Player distributions are dampened by the variance observed in prior years. This is superior to the variance in the raw data because, in this case, it is caused by actual deviations as opposed to what are likely bad user predicitons.</p>
<p>I wrote a similar article for the NBA Draft at <a href="https://fansided.com/2020/09/17/nba-draft-class-controversial-obi-toppin/">Nylon Calculus</a>.</p>
</section>
</section>
</section>
<section id="a-rank-ordered-model" class="level2">
<h2 class="anchored" data-anchor-id="a-rank-ordered-model">A rank-ordered model</h2>
<p>The prospect pick probabilities in the “Draft Pick Probabilities” tab of the <a href="https://piyer97.shinyapps.io/NHLDraft2023/">2023 Draft Stock tool</a> are generated through a process which primarily involves the application of a rank-ordered logit model to draft rankings released throughout the year. The methodology is a simplified version of <a href="https://ecp.ep.liu.se/index.php/linhac/article/view/480">Predicting the NHL Draft with Rank-Ordered Logit Models</a>.</p>
<p>There are three main components - first, partial draft rankings are made complete, then a rank-ordered logit model is fit, finally 100,000 drafts are simulated from model outputs.</p>
<section id="imputation-of-partial-rankings" class="level3">
<h3 class="anchored" data-anchor-id="imputation-of-partial-rankings">Imputation of partial rankings</h3>
<p>Technically, there are thousands of draft prospects. Consequently, draft rankings cannot include every prospect, and so, by definition, they are partial rankings. They need to be made complete to fit into our framework. We do this by first, restricting the population to prospects ranked in the top 100 by at least one publication. Then, through the <a href="https://cran.r-project.org/web/packages/PLMIX/PLMIX.pdf">PLMIX</a> package in R, rankings are made complete using the frequency of their appearance in rankings as weights.</p>
<section id="create-full-rankings" class="level4">
<h4 class="anchored" data-anchor-id="create-full-rankings">Create full rankings</h4>
<p>The following historical code excerpt shows the ranking-imputation step:</p>
<details class="historical-code-disclosure"><summary>Show historical code</summary>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb1-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Get skater appearance counts</span></span>
<span id="cb1-2">top_skater_freq <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span></span>
<span id="cb1-3">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">rank_summaries</span>(</span>
<span id="cb1-4">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">data=</span>ranking_matrix,</span>
<span id="cb1-5">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">format_input=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"ordering"</span>,</span>
<span id="cb1-6">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">mean_rank=</span><span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">TRUE</span>,</span>
<span id="cb1-7">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">pc=</span><span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">FALSE</span>) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">%&gt;%</span></span>
<span id="cb1-8">  .<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>marginals <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">%&gt;%</span></span>
<span id="cb1-9">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">colSums</span>()</span>
<span id="cb1-10"></span>
<span id="cb1-11"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Impute partial rankings to create full ranking matrix</span></span>
<span id="cb1-12">full_ranking_matrix <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span></span>
<span id="cb1-13">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">make_complete</span>(</span>
<span id="cb1-14">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">data=</span>ranking_matrix,</span>
<span id="cb1-15">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">format_input=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"ordering"</span>,</span>
<span id="cb1-16">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">probitems=</span>top_skater_freq) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">%&gt;%</span></span>
<span id="cb1-17">  .<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>completedata</span></code></pre></div></div>
</details>
</section>
</section>
<section id="fitting-of-plackett-luce-models" class="level3">
<h3 class="anchored" data-anchor-id="fitting-of-plackett-luce-models">Fitting of Plackett-Luce models</h3>
<p>As for the rank-ordered logit models, we’re currently operating two. The first, a time-weighted frequentist method from the <a href="https://cran.r-project.org/web/packages/PlackettLuce/PlackettLuce.pdf">PlackettLuce</a> package in R; the other, a Bayesian, <a href="https://github.com/tyrelstokes/Monaco_ranking/blob/main/plackett_luce_opt.stan">tier-weighted implementation written in Stan</a> by <a href="https://twitter.com/TyrelStokes">Tyrel Stokes</a>.</p>
<p>The time-weighted frequentist implementation is a standard application of the Plackett-Luce, except that ranking lists are weighed based on their distance to the draft in days. The ranking weights were determined using my previous work on user mock drafts. The weights are available <a href="https://github.com/spazznolo/draft-rankings/blob/main/data/weights_for_pl.csv">here</a> for those interested. Using the draft day as the index, rankings published a month out are weighted at roughly 90%, two months at 77%, four months at 50%, and a year at 17%.</p>
<section id="build-plackett-luce-model" class="level4">
<h4 class="anchored" data-anchor-id="build-plackett-luce-model">Build Plackett-Luce model</h4>
<p>The following historical code excerpt shows the model-fitting step:</p>
<details class="historical-code-disclosure"><summary>Show historical code</summary>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb2-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Fit the Plackett-Luce model</span></span>
<span id="cb2-2">pl_model <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">PlackettLuce</span>(full_ranking_matrix, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">weights =</span> weights, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">npseudo =</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.1</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">maxit =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5000</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">100</span>))</span>
<span id="cb2-3"></span>
<span id="cb2-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Obtain maximum likelihood estimates from the Plackett-Luce model</span></span>
<span id="cb2-5">mle_estimates <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">coef</span>(pl_model, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">log =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">FALSE</span>)</span></code></pre></div></div>
</details>
<p>The tier-weighted Bayesian implementation is taken wholesale from Tyrel Stokes’ work on <a href="https://github.com/tyrelstokes/Monaco_ranking">track racing</a>. His implementation contains weights, however they are determined by the Bayesian framework, which was not written with time, but tier importance + noise in mind.</p>
</section>
</section>
<section id="simulation-of-drafts" class="level3">
<h3 class="anchored" data-anchor-id="simulation-of-drafts">Simulation of drafts</h3>
<p>These rank-ordered logit models attribute a “strength” score to each player. Drafts are simulated (100k times) by randomly drawing (without replacement) players using their strength score as weights.</p>
<p>The following historical code excerpt shows the simulation step:</p>
<details class="historical-code-disclosure"><summary>Show historical code</summary>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb3-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Simulate draft rankings</span></span>
<span id="cb3-2">draft_simulations <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">replicate</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">100000</span>, <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">sample</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:</span>skaters, skaters, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">replace =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">FALSE</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">prob =</span> mle_estimates))</span></code></pre></div></div>
</details>
</section>
<section id="assumptions" class="level3">
<h3 class="anchored" data-anchor-id="assumptions">Assumptions</h3>
<p>There are three main assumptions which don’t quite fit in this methodology. The first is that the rankings are truly full rankings (they are not). The second is that draft rankings aren’t related over time (they are). The third is that ranking publications are representative of NHL organizations (unsure, could be verified with historical data). We explain each below.</p>
<section id="on-full-rankings" class="level4">
<h4 class="anchored" data-anchor-id="on-full-rankings">On “Full” Rankings</h4>
<p>In an ideal scenario, full rankings would encompass every eligible prospect in each ranking, providing a comprehensive view. However, this is impractical in reality. The challenge lies in incorporating partial rankings into a complete ranking system. One approach is to establish a cut-off point for the number of players included in the rankings. In my opinion, a suitable cut-off would be to limit the rankings to the top 100 prospects. If a publication ranks more than 100 prospects (e.g., 150 or 250), we disregard prospects ranked 101 and beyond. This approach minimizes information loss since only a few publications extend their rankings beyond 100. By focusing on the players within this new set, we are left with approximately 200+ prospects and growing. However, it’s important to note that the current model does not acknowledge the existence of other prospects outside this set. Consequently, the model assigns a 0% probability to a prospect ranked 250 being drafted within the top 100. As a result, the estimated probability of the 200+ prospects being drafted becomes overestimated. This issue becomes increasingly problematic as the draft progresses, and the model’s effectiveness is limited after the first two rounds. One potential way to address this is by adjusting for historical undercoverage, but unfortunately, we lack the necessary data for such adjustments.</p>
</section>
<section id="on-time" class="level4">
<h4 class="anchored" data-anchor-id="on-time">On Time</h4>
<p>To simplify the model, time was included through weights. Essentially, we are flattening time by saying “a ranking published right before the draft is worth about twice as much as a ranking published four months ago”. Even though this works for prediction, it is not actually how the dynamic works. The reason rankings change is <em>not</em> because of time, but because of <em>what prospects/scouts do during this time</em>. This assumption is addressed in <em>Predicting the NHL Draft with Rank-Ordered Logit Models</em>, linked above.</p>
</section>
<section id="on-selection-bias" class="level4">
<h4 class="anchored" data-anchor-id="on-selection-bias">On Selection Bias</h4>
<p>The method employed in this model assumes that prospect ranking publications represent the perspectives of NHL organizations. By “representative,” we mean that these publications assign similar value to players and exhibit comparable variance in their evaluations. For instance, it is plausible that ranking publications lack the same level of resources as professional hockey teams, leading them to rely predominantly on data rather than in-game observations. Consequently, they may place greater emphasis on prospects who excel in ways which are represented in game logs. Any inherent “bias” present in these publication rankings would consequently influence the published pick probabilities in this model.</p>
<p>The code used to generate these probabilities is available on GitHub <a href="https://github.com/spazznolo/draft-rankings">here</a>.</p>
</section>
</section>
</section>
<section id="turning-probabilities-into-pick-value" class="level2">
<h2 class="anchored" data-anchor-id="turning-probabilities-into-pick-value">Turning probabilities into pick value</h2>
<p>The prospect pick probabilities available on the “Draft Pick Probabilities” tab of the <a href="https://piyer97.shinyapps.io/NHLDraft2023/">2023 Draft Tool</a>, created by Pranshanth Iyer and myself, can be used by NHL organizations to inform their decision-making process. Let’s take the Montreal Canadiens as an example.</p>
<p>The Canadiens pick fifth, which, according to consensus, is just outside the top tiers of prospects consisting of Connor Bedard, Adam Fantilli, Matvei Mitchkov, and Leo Carlsson. The next “tier”, contains a handful of players which ranking publications seem to mostly disagree on. One common question is, if you’re the Canadiens, do you trade up to guarantee the drafting of a potential superstar, or do you take the risk and hope one of them slides in the draft? However a more interesting question, in my opinion, is: “How are you making this decision?”</p>
<p>The truth is that either way you’re leaning, you are making a complex calculation with instinct. You’re considering 1) the value of each prospect, 2) the probability each prospect is available at a given pick, and 3) the appropriate cost in assets to move up in the draft. This is an exceedingly difficult calculation to make in the moment, especially in the heat of the draft.</p>
<p>Thankfully, the probabilities in this tool can help.</p>
<section id="prospect-values" class="level3">
<h3 class="anchored" data-anchor-id="prospect-values">Prospect Values</h3>
<p>First, we need the value of the top 5 prospects according to the Canadiens. For this example, let’s assume they evaluate prospects based on their predicted Wins Above Replacement (WAR) while their rights are held by the team (7 years), and the predicted WAR of their top five ranked prospects is as follows (feel free to use the metric and values of your choosing):</p>
<ul>
<li>Connor Bedard: 24.0</li>
<li>Adam Fantilli: 19.0</li>
<li>Matvei Mitchkov: 17.5</li>
<li>Leo Carlsson: 14.0</li>
<li>William Smith: 10.0</li>
</ul>
</section>
<section id="prospect-pick-probabilities" class="level3">
<h3 class="anchored" data-anchor-id="prospect-pick-probabilities">Prospect Pick Probabilities</h3>
<p>Next, we can use our tool to get the probability that each player is still available at each pick. Here they are:</p>
<p><img src="https://spazznolo.github.io/figs/draft-probabilities-3-1.png" class="img-fluid" style="width:85.0%" alt="Prospect pick probabilities by draft pick."></p>
</section>
<section id="pick-values" class="level3">
<h3 class="anchored" data-anchor-id="pick-values">Pick Values</h3>
<p>As the Canadiens, we derive our very own pick values by multiplying the probability a prospect is available at a certain pick by their predicted WAR. Obviously, we’re going to take the highest-value player available, so the pick values are calculated like so:</p>
<div class="equation-note">
<pre class="text equation-expression"><code>1st pick - (1.000*24) = 24.000
2nd pick - (0.002*24) + (0.998*19) = 19.010
3rd pick - (0.000*24) + (0.210*19) + (0.790*17.5) = 17.815
4th pick - (0.000*24) + (0.028*19) + (0.420*17.5) + (0.552*14.0) = 15.610
5th pick - (0.000*24) + (0.002*19) + (0.111*17.5) + (0.269*14.0) + (0.618*10.0) = 11.927</code></pre>
</div>
<p>Using the third pick as an example:</p>
<ul>
<li>There is a 0% chance Bedard, the Canadiens’ highest ranked prospect, is available, so his value is multiplied by 0.</li>
<li>There’s a 21.0% chance Fantilli, the Canadiens’ second-ranked prospect, is available, so his value is multiplied by 0.210.</li>
<li>If neither Bedard nor Fantilli is available (79.0% chance), the Canadiens would select Mitchkov, whose value is multiplied by 0.790.</li>
<li>In total, the pick is valued at 17.815 WAR.</li>
</ul>
</section>
<section id="previous-work" class="level3">
<h3 class="anchored" data-anchor-id="previous-work">Previous Work</h3>
<p>Note that this is a departure from the usual draft pick value chart which has long been established in the hockey analytics community (below is an example of the Athletic’s) which uses <em>average</em> pick values:</p>
<p><img src="https://spazznolo.github.io/figs/draft-probability-3-2.png" class="img-fluid" style="width:70.0%" alt="Historical average draft pick value chart."></p>
<p>The reason for this proposed departure is that, in practice, there are weak drafts (like last year), and strong drafts (like this year). Even within a draft class, there can be pockets of heterogeneity where talented players are clustered together, as well as instances of significant drops in value. Consequently, for our purposes, it is more appropriate to derive pick values based on the prospects <em>eligible for this year’s draft</em>. Moreover, since the historical value chart is well-established, the Canadiens should be trying to leverage it to find value gains.</p>
</section>
<section id="decisions-decisions" class="level3">
<h3 class="anchored" data-anchor-id="decisions-decisions">Decisions, Decisions</h3>
<p>With pick probabilities and prospect values, the Canadiens now have a quantitive framework to determine the value of a pick in the upcoming draft. This allows them to assess the value difference between picks and consider trade scenarios. Which brings us back to the initial question: should the Canadiens consider trading up?</p>
<p>Let’s explore this with a hypothetical scenario. Imagine they have the opportunity to trade up to the fourth pick. To gain organizational value, they would need to give up less than the value of the fourth pick (15.610) minus the value of their fifth pick (11.927), which equals 3.683 WAR. If they can trade their fifth plus a piece which is worth less than 3.564 WAR, they’ve gained value. With this, a complex decision becomes relatively simple, at least in theory.</p>
<p>By combining prospect pick probabilities with internal evaluations, the Canadiens can make more informed decisions about their draft strategies and potential trades.</p>
<p>For more information, visit the <a href="https://piyer97.shinyapps.io/NHLDraft2023/">2023 Draft Tool</a>. You can reach Prashanth and I on twitter (<a href="https://twitter.com/iyer_prashanth"><span class="citation" data-cites="iyer_prashanth">@iyer_prashanth</span></a>, <a href="https://twitter.com/spazznolo"><span class="citation" data-cites="spazznolo">@spazznolo</span></a>).</p>
</section>
</section>
<section id="adding-uncertainty" class="level2">
<h2 class="anchored" data-anchor-id="adding-uncertainty">Adding uncertainty</h2>
<p>The framework can also include prospect value uncertainty.</p>
<section id="the-michkov-problem" class="level3">
<h3 class="anchored" data-anchor-id="the-michkov-problem">The Michkov problem</h3>
<p>One of the main considerations for teams drafting in the 2-7 spot this year was whether they should risk their extremely valuable lottery pick on Matvei Michkov. He signed a 3-year contract in Russia. He only met with a few teams. He might be the most talented player in the draft. He may never come to North America… etc. Essentially, the question these teams were asking is this: is it worth drafting Michkov’s uncertainty when more reliable, though perhaps less skilled, players are available? Once again, teams have to make a complex calculation. Once again, they’re left with mostly instinct.</p>
<p>Unless!</p>
<p>Unless, prospect value uncertainty is quantified and included in the decision process.</p>
</section>
<section id="assigning-uncertainty" class="level3">
<h3 class="anchored" data-anchor-id="assigning-uncertainty">Assigning Uncertainty</h3>
<p>Adapting the framework to include uncertainty is actually easy. Instead of assigning prospect values through point estimates (Bedard was at 24 WAR, Michkov at 17.5), prospects are assigned <a href="https://www.scribbr.com/statistics/probability-distributions/#:~:text=A%20probability%20distribution%20is%20a,using%20graphs%20or%20probability%20tables.">probability distributions</a> which reflect the uncertainty of their value.</p>
<p>As an example: instead of Bedard’s value being 24 WAR, it will be normally distributed with mean 24 and standard deviation 3. Here’s what that looks like:</p>
<p><img src="https://spazznolo.github.io/figs/draft-probabilities-4-1.png" class="img-fluid" style="width:60.0%" alt="Illustrative distribution for Bedard's prospect value."></p>
<p>In this example:</p>
<ul>
<li>Bedard has a 50% chance of providing 22-26 WAR.</li>
<li>Bedard has a 25% chance of providing 26+ WAR.</li>
<li>Bedard has a 25% chance of providing -22 WAR.</li>
</ul>
<p>Like the point estimates above, the actual distribution doesn’t matter, it’s just an example to illustrate the framework I’m introducing.</p>
<p>Let’s assign these value distributions to each player, making sure Michkov’s value has higher uncertainty by increasing the standard deviation of his distribution. Prospect values now look like this:</p>
<p><img src="https://spazznolo.github.io/figs/draft-probabilities-4-2.png" class="img-fluid" style="width:60.0%" alt="Prospect value distributions with varying uncertainty."></p>
<p>Then, these probability distributions are multiplied by the probability a prospect is available at each pick. Pick values now look like this:</p>
<p><img src="https://spazznolo.github.io/figs/draft-probabilities-4-3.png" class="img-fluid" style="width:60.0%" alt="Pick value distributions after accounting for prospect availability."></p>
<p>With these pick value distributions you get the same comparisons as above, plus:</p>
<ul>
<li>the probability that a given pick will be more valuable than another.</li>
<li>WAR for whichever “outcome” you’re interested in (like top 10% scenario, bottom 10%, etc.)</li>
</ul>
<p>Let’s re-visit the Montreal example where they should only trade up if the value of the fourth pick (15.87) exceeded that of their package (fifth pick (13.15) plus a piece). After including uncertainty, these point estimates become probability distributions and the difference between the two picks looks like this:</p>
<p><img src="https://spazznolo.github.io/figs/draft-probabilities-4-4.png" class="img-fluid" style="width:60.0%" alt="Difference between the fourth and fifth pick value distributions."></p>
<p>Code available here: <a href="https://github.com/spazznolo/draft-rankings/blob/main/scripts/post_4.R">https://github.com/spazznolo/draft-rankings/blob/main/scripts/post_4.R</a></p>
</section>
</section>
<section id="a-drafting-strategy" class="level2">
<h2 class="anchored" data-anchor-id="a-drafting-strategy">A drafting strategy</h2>
<p>The pick value framework captures the unique dynamics of a given draft and the uncertainty of prospect values. It also suggests a drafting strategy which aims to maximize draft value. But first, some background.</p>
<section id="background-on-optimization" class="level3">
<h3 class="anchored" data-anchor-id="background-on-optimization">Background on Optimization</h3>
<p>The optimization of draft value has been explored specifically for the NHL by now Kraken analyst <a href="https://twitter.com/nnstats"><span class="citation" data-cites="nnstats">@nnstats</span></a> in this 2017 <a href="https://www.statsportsconsulting.com/wp-content/uploads/Nandakumar_PerfectDraft-1.pdf">paper</a>. It was a counter to the prevalent discourse at the time, which assumed the optimal draft was achieved by simply picking the best players as they were available. Instead, she argued that a team should only draft the best player available (BPA) if he won’t be available for their next pick. For instance, Jamie Benn, a sixth round pick, should have been drafted in the fifth round to maximize total draft value.</p>
<p>The key outstanding question from this work was: “How can we determine if the risk of deferring on the BPA and potentially losing them is worth the reward of drafting another quality prospect before landing the BPA later?” The prospect pick probabilities from our draft tool, when combined with prospect values, allow for a probablistic framework to make such a decision.</p>
</section>
<section id="the-strategy" class="level3">
<h3 class="anchored" data-anchor-id="the-strategy">The strategy</h3>
<p>The core objective of this strategy is to determine a team’s maximum conditional draft value at each pick. The maximum value is selected from a list of conditional draft values associated with each available prospect. Using a team’s first two picks as an example, the strategy can be written (somewhat) formally like this:</p>
<div class="equation-note">
<pre class="text equation-expression"><code>for n remaining prospects, ranked 1 to n
max draft value = max(v1, v2, ..., vn)
where,
vi = estimated draft value when choosing prospect i with the next pick, specifically for i ≠ 1
vi = v(pi) + (Pj(p1) x v(p1)) + ... + ((1 - Pj(p1) - ... - Pj(p(x-1))) x v(px))
where,
v(pi) = value of prospect i,
Pj(pi) = probability that propsect i is available at pick j, and
Pj(p1) + ... + Pj(p(x-1)) &lt; 1 and Pj(p1) + ... + Pj(p(x-1)) + Pj(p(x))) &gt;= 1</code></pre>
</div>
<p>To further illustrate, let’s consider the first round of this year’s draft up to pick 17.</p>
</section>
<section id="an-example-using-the-red-wings-first-round-picks" class="level3">
<h3 class="anchored" data-anchor-id="an-example-using-the-red-wings-first-round-picks">An example using the Red Wings’ first round picks</h3>
<p>The Red Wings held the 9th and 17th picks in this year’s draft. I’m going to calculate the optimal drafting strategy for the Red Wings using a set of <a href="https://twitter.com/spazznolo/status/1674392375018307585">well-performing</a> prospect rankings from Bob McKenzie, Cam Robinson, and Chris Peters. Prospect values are assigned using their respective rankings in the historical pick value chart.</p>
<p>At the 9th pick of the draft, the remaining prospects were ranked as follows:</p>
<p><img src="https://spazznolo.github.io/figs/draft-probabilities-5-4.png" class="img-fluid" style="width:80.0%" alt="Remaining prospect rankings at pick 9."></p>
<p>Let’s determine who each analyst should draft at the 9th pick to maximize their expected draft value for picks 9 and 17. To answer, we use the equation shown above, which requires: 1) the value of the prospect taken at 9, 2) the conditional probabilities of the remaining prospects being available at 17, and 3) the values of these remaining prospects.</p>
<p><img src="https://spazznolo.github.io/figs/draft-probabilities-5-5.png" class="img-fluid" style="width:80.0%" alt="Conditional draft values for the Red Wings' pick 9 options."></p>
<p>Here, McKenzie and Robinson should take the BPA to maximize conditional draft value, but Peters should defer and take his second BPA, Zach Benson.</p>
</section>
</section>
<section id="after-the-draft" class="level2">
<h2 class="anchored" data-anchor-id="after-the-draft">After the draft</h2>
<p>In general the probabilities were pretty well calibrated. However, the model was too certain in the top 10 picks. As a result, the sportsbooks cooked us.</p>


</section>

 ]]></description>
  <category>Probability and simulation</category>
  <category>Regression and calibration</category>
  <category>Distributions and sampling</category>
  <guid>https://spazznolo.github.io/research/nhl-pick-probability/</guid>
  <pubDate>Sun, 28 Nov 2021 00:00:00 GMT</pubDate>
</item>
</channel>
</rss>
