<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="/feed.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" /><updated>2026-06-07T21:28:37+00:00</updated><id>/feed.xml</id><title type="html">A Blog</title><subtitle>My GitHub Page</subtitle><entry><title type="html">Estimating Lateral Puck Movement Before Goals</title><link href="/jekyll/update/2026/06/07/estimating_lateral_puck_movement.html" rel="alternate" type="text/html" title="Estimating Lateral Puck Movement Before Goals" /><published>2026-06-07T14:01:24+00:00</published><updated>2026-06-07T14:01:24+00:00</updated><id>/jekyll/update/2026/06/07/estimating_lateral_puck_movement</id><content type="html" xml:base="/jekyll/update/2026/06/07/estimating_lateral_puck_movement.html"><![CDATA[<h1 id="introduction">Introduction</h1>

<p>Moving the puck from one side of the other gets the goalie moving and can make it easier to score.
So I wanted to see how much lateral puck movement there is right before a goal.
To do so, I used both goal tracking data and shot location data from NHL.com’s API.
Combining the two, I was able to isolate the parts of the tracking data right before the shot.</p>

<p>The code for the analysis can be found <a href="https://github.com/danielglin/nhl_goal_tracking_analyses/blob/main/lateral_puck_movement/lateral_puck_movement.py">here</a>.</p>

<h1 id="data">Data</h1>

<p>One part of the data I used was the goal tracking data.
I used this data previously when clustering power play goals and when coming up with criteria for rush goals.
The tracking data comes from NHL.com’s goal visualizations.</p>

<p>The other data I used was the shot location data provided by NHL.com’s API.
Specifically, I used the play-by-play endpoint:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://api-web.nhle.com/v1/gamecenter/&lt;game id&gt;/play-by-play
</code></pre></div></div>
<p>Since this shot data uses a different coordinate system, I had to convert the shot locations to use the same coordinate system the tracking data uses.</p>

<h1 id="pre-processing-the-data">Pre-Processing the Data</h1>

<p>While the tracking data from the goal visualizations uses a coordinate system where the rink is about 2400x1015 units, the shot locations from the API are given in feet.
Also, the y-axis is flipped between the two.
So I converted the shot location coordinates by flipping the y-axis and converting to the tracking data’s coordinate system.</p>

<p>Another part of pre-processing the data is finding the original shot location for tips and deflections.
In the play-by-play API data, tip and deflection goal locations are recorded where the tip or deflection happened.
I wanted to use the location of the original shot when calculating lateral puck movement to avoid counting the shot’s lateral movement.
That meant I had to find the original shot using the tracking data.
For each tip and deflection goal, I went backward through the tracking data, starting at the end, to see if the puck either changed direction above a threshold or changed speed above a separate threshold.
The rationale being a puck shouldn’t change direction or speed too much directly after being shot.</p>

<p>Here is one example of a deflection goal:</p>

<p><img src="/images/lateral_puck_movement/tip_ex_1.png" alt="" /></p>

<p>The green dot is the location of the tip, and the blue dot is the estimated location of the original shot.
The green dot doesn’t line up with the puck tracking data, shown as the black lines, because the play-by-play data uses a coarser coordinate grid.</p>

<p>I checked the original shot location logic by looking at a dozen or so deflection goals and adjusted the direction and speed thresholds as needed.</p>

<p>Since the coordinate system used by the play-by-play API is coarser than the coordinate system used by the tracking data, I had to also pre-process non-deflection goals to find the location of the original shot in the tracking data.</p>

<p>To do so, I looked for the spot in the tracking data closest to the location given by the play-by-play data that fell under a distance threshold.
Using a distance threshold helped filter out goals where the tracking data didn’t match the goal.
For example, <a href="https://www.nhl.com/ppt-replay/goal/2025020007/353">this goal by Brandt Clarke against the Golden Knights on October 8, 2025</a> has tracking data where the last part is incorrect.
I excluded the goals where the tracking data and the play-by-play’s shot location were too far apart.</p>

<h1 id="results">Results</h1>

<p>With the shot locations found in the tracking data, I then calculated the lateral puck movement before the shots for the 2025-2026 regular season goals.
Empty-net goals were omitted.
I tried looking at windows of different lengths of time before the shot.</p>

<p>Below are histograms of the amount of lateral puck movement before goals:</p>

<p><img src="/images/lateral_puck_movement/hist_goals_0-88_secs.png" alt="" /></p>

<p><img src="/images/lateral_puck_movement/hist_goals_1-25_secs.png" alt="" /></p>

<p><img src="/images/lateral_puck_movement/hist_goals_1-62_secs.png" alt="" /></p>

<p>Unsurprisingly, as the time window before the shot increases, the amount of lateral puck movement increases as well.</p>

<p>The median and mean amounts of lateral puck movement:</p>

<table>
  <thead>
    <tr>
      <th>Seconds before Goal</th>
      <th>Median Lateral Puck Movement (Feet)</th>
      <th>Mean Lateral Puck Movement (Feet)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0.88</td>
      <td>9.70</td>
      <td>12.77</td>
    </tr>
    <tr>
      <td>1.25</td>
      <td>15.08</td>
      <td>17.93</td>
    </tr>
    <tr>
      <td>1.62</td>
      <td>20.65</td>
      <td>22.97</td>
    </tr>
  </tbody>
</table>

<h1 id="example-goals">Example Goals</h1>

<p>An example of a goal that has 4 feet of lateral puck movement in the 1.25 seconds before the goal:</p>

<p><a href="https://www.nhl.com/ppt-replay/goal/2025020001/1101">Jesper Boqvist against the Blackhawks on October 7, 2025</a></p>

<p>An example of a goal that has 15 feet of lateral puck movement in the 1.25 seconds before the goal:</p>

<p><a href="https://www.nhl.com/ppt-replay/goal/2025021297/895">Nick Schmaltz against the Jets on April 14, 2026</a></p>

<p>An example of a goal that has 65 feet of lateral puck movement in the 1.25 seconds before the goal:</p>

<p><a href="https://www.nhl.com/ppt-replay/goal/2025020932/1009">Matt Boldy against the Mammoth on February 27, 2026</a></p>

<h1 id="conclusion">Conclusion</h1>

<p>I was expecting goals to have more lateral puck movement than they actually do since I often heard how moving the puck east-to-west is an effective way to score goals.
Perhaps I just have a bad sense of scale.</p>

<p>One follow-up analysis I might do in the future is to see how much vertical puck movement there is before goals.</p>]]></content><author><name></name></author><category term="jekyll" /><category term="update" /><summary type="html"><![CDATA[Introduction]]></summary></entry><entry><title type="html">Defining Rush Goals with Puck Tracking Data</title><link href="/jekyll/update/2026/05/04/defining-rush-goals-puck-tracking-data.html" rel="alternate" type="text/html" title="Defining Rush Goals with Puck Tracking Data" /><published>2026-05-04T12:07:18+00:00</published><updated>2026-05-04T12:07:18+00:00</updated><id>/jekyll/update/2026/05/04/defining-rush-goals-puck-tracking-data</id><content type="html" xml:base="/jekyll/update/2026/05/04/defining-rush-goals-puck-tracking-data.html"><![CDATA[<h1 id="introduction">Introduction</h1>

<p>One of the analyses I wanted to do with the publicly available goal tracking data is to see how many goals each team scores off the rush.
In order to do that, I had to define criteria for rush goals.
That process involved iteratively setting some tentative criteria, reviewing the results, and updating the criteria.
At the end of it, I ended up with three criteria that I used to calculate how many rush goals each team scored in the 2025-2026 regular season.</p>

<p>You can find the code I created for this analysis <a href="https://github.com/danielglin/nhl_goal_tracking_analyses/tree/main/rush_goals">here</a>.</p>

<h1 id="data">Data</h1>

<p>The data I used was the public goal tracking data for the 2025-2026 regular season.
This data is the same data that is used for NHL.com’s goal visualizations.
Here is <a href="https://www.nhl.com/ppt-replay/goal/2025020151/457">one example</a> goal visualization.</p>

<p>For this analysis, I did the same pre-processing steps I had used when looking at <a href="https://github.com/danielglin/nhl_goal_similarity#data">goal similarity</a>.
This includes trimming the starts and ends of the tracking data to account for things like faceoffs.</p>

<p>I filtered out empty net goals from the data using NHL.com’s API to get empty net goal data.</p>

<p>One caveat: some goals are missing data, but this is a very small percentage of goals (&lt; 0.1% of all goals).</p>

<p>To keep it simple, I only used puck tracking data and not the player tracking data.
In the future, I may revisit the criteria to include the player tracking data.</p>

<h1 id="process">Process</h1>

<p>The process I used to come up with criteria to define rush goals was:</p>

<ol>
  <li>Set tentative criteria</li>
  <li>Review a sample of goals that were close to being considered rush or non-rush</li>
  <li>Update the criteria based on that review and repeat the process</li>
</ol>

<p>Throughout this process, I reviewed around 60-80 goals in total.</p>

<h1 id="criteria">Criteria</h1>

<p>The final criteria are:</p>

<ol>
  <li>The puck traveled at least 67 feet along the length of the ice</li>
  <li>The puck spent less than 40 timesteps in the offensive zone
    <ul>
      <li>A timestep is the interval in between data points in the tracking data.</li>
      <li>In a future analysis, I hope to calculate how much time this interval is.</li>
    </ul>
  </li>
  <li>The puck spent less than 9 timesteps beyond the goal line</li>
</ol>

<h1 id="results">Results</h1>

<p>Using the above criteria, about 29% of non-empty net goals were off the rush in the 2025-2026 regular season.</p>

<p>Some example goals defined as rush goals by the criteria:</p>

<ul>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2025021019/115">https://www.nhl.com/ppt-replay/goal/2025021019/115</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2025020129/353">https://www.nhl.com/ppt-replay/goal/2025020129/353</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2025021266/715">https://www.nhl.com/ppt-replay/goal/2025021266/715</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2025020257/828">https://www.nhl.com/ppt-replay/goal/2025020257/828</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2025020499/151">https://www.nhl.com/ppt-replay/goal/2025020499/151</a></li>
</ul>

<p>Here are the number of rush goals by team:</p>

<table>
  <thead>
    <tr>
      <th>Team</th>
      <th>Number of Rush Goals</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>CAR</td>
      <td>89</td>
    </tr>
    <tr>
      <td>PIT</td>
      <td>86</td>
    </tr>
    <tr>
      <td>UTA</td>
      <td>85</td>
    </tr>
    <tr>
      <td>BUF</td>
      <td>84</td>
    </tr>
    <tr>
      <td>EDM</td>
      <td>80</td>
    </tr>
    <tr>
      <td>LAK</td>
      <td>77</td>
    </tr>
    <tr>
      <td>MTL</td>
      <td>75</td>
    </tr>
    <tr>
      <td>ANA</td>
      <td>73</td>
    </tr>
    <tr>
      <td>STL</td>
      <td>73</td>
    </tr>
    <tr>
      <td>TBL</td>
      <td>73</td>
    </tr>
    <tr>
      <td>CBJ</td>
      <td>71</td>
    </tr>
    <tr>
      <td>NYR</td>
      <td>71</td>
    </tr>
    <tr>
      <td>CGY</td>
      <td>70</td>
    </tr>
    <tr>
      <td>MIN</td>
      <td>69</td>
    </tr>
    <tr>
      <td>TOR</td>
      <td>69</td>
    </tr>
    <tr>
      <td>NYI</td>
      <td>68</td>
    </tr>
    <tr>
      <td>PHI</td>
      <td>68</td>
    </tr>
    <tr>
      <td>VGK</td>
      <td>68</td>
    </tr>
    <tr>
      <td>NJD</td>
      <td>66</td>
    </tr>
    <tr>
      <td>WPG</td>
      <td>66</td>
    </tr>
    <tr>
      <td>COL</td>
      <td>65</td>
    </tr>
    <tr>
      <td>SJS</td>
      <td>65</td>
    </tr>
    <tr>
      <td>DET</td>
      <td>64</td>
    </tr>
    <tr>
      <td>FLA</td>
      <td>62</td>
    </tr>
    <tr>
      <td>VAN</td>
      <td>61</td>
    </tr>
    <tr>
      <td>SEA</td>
      <td>59</td>
    </tr>
    <tr>
      <td>OTT</td>
      <td>58</td>
    </tr>
    <tr>
      <td>BOS</td>
      <td>58</td>
    </tr>
    <tr>
      <td>WSH</td>
      <td>58</td>
    </tr>
    <tr>
      <td>CHI</td>
      <td>57</td>
    </tr>
    <tr>
      <td>DAL</td>
      <td>56</td>
    </tr>
    <tr>
      <td>NSH</td>
      <td>45</td>
    </tr>
  </tbody>
</table>

<p>Here are the percentages of goals that were rush goals by team (excluding empty net goals):</p>

<table>
  <thead>
    <tr>
      <th>Team</th>
      <th>% of Goals that are Rush Goals</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>LAK</td>
      <td>37%</td>
    </tr>
    <tr>
      <td>CGY</td>
      <td>36%</td>
    </tr>
    <tr>
      <td>STL</td>
      <td>34%</td>
    </tr>
    <tr>
      <td>UTA</td>
      <td>34%</td>
    </tr>
    <tr>
      <td>BUF</td>
      <td>33%</td>
    </tr>
    <tr>
      <td>NYR</td>
      <td>32%</td>
    </tr>
    <tr>
      <td>NJD</td>
      <td>32%</td>
    </tr>
    <tr>
      <td>CAR</td>
      <td>32%</td>
    </tr>
    <tr>
      <td>PHI</td>
      <td>31%</td>
    </tr>
    <tr>
      <td>CBJ</td>
      <td>31%</td>
    </tr>
    <tr>
      <td>NYI</td>
      <td>31%</td>
    </tr>
    <tr>
      <td>WPG</td>
      <td>31%</td>
    </tr>
    <tr>
      <td>PIT</td>
      <td>31%</td>
    </tr>
    <tr>
      <td>EDM</td>
      <td>30%</td>
    </tr>
    <tr>
      <td>TOR</td>
      <td>30%</td>
    </tr>
    <tr>
      <td>VAN</td>
      <td>30%</td>
    </tr>
    <tr>
      <td>SEA</td>
      <td>29%</td>
    </tr>
    <tr>
      <td>ANA</td>
      <td>29%</td>
    </tr>
    <tr>
      <td>DET</td>
      <td>29%</td>
    </tr>
    <tr>
      <td>CHI</td>
      <td>28%</td>
    </tr>
    <tr>
      <td>MTL</td>
      <td>28%</td>
    </tr>
    <tr>
      <td>TBL</td>
      <td>28%</td>
    </tr>
    <tr>
      <td>MIN</td>
      <td>27%</td>
    </tr>
    <tr>
      <td>FLA</td>
      <td>27%</td>
    </tr>
    <tr>
      <td>SJS</td>
      <td>27%</td>
    </tr>
    <tr>
      <td>VGK</td>
      <td>27%</td>
    </tr>
    <tr>
      <td>WSH</td>
      <td>25%</td>
    </tr>
    <tr>
      <td>COL</td>
      <td>24%</td>
    </tr>
    <tr>
      <td>OTT</td>
      <td>23%</td>
    </tr>
    <tr>
      <td>BOS</td>
      <td>23%</td>
    </tr>
    <tr>
      <td>DAL</td>
      <td>21%</td>
    </tr>
    <tr>
      <td>NSH</td>
      <td>20%</td>
    </tr>
  </tbody>
</table>

<h1 id="conclusion">Conclusion</h1>

<p>While the criteria I developed seem to work well, I may in a future analysis try to refine them by including player tracking data in addition to the puck tracking data.
I would also like to dive deeper into rush goals as well.</p>]]></content><author><name></name></author><category term="jekyll" /><category term="update" /><summary type="html"><![CDATA[Introduction]]></summary></entry><entry><title type="html">Follow-Up to 24-25 Winnipeg Jets Power Play Goals: Finding Goals That Follow the Same Play</title><link href="/jekyll/update/2025/11/19/followup-similar-powerplay-goals.html" rel="alternate" type="text/html" title="Follow-Up to 24-25 Winnipeg Jets Power Play Goals: Finding Goals That Follow the Same Play" /><published>2025-11-19T21:20:18+00:00</published><updated>2025-11-19T21:20:18+00:00</updated><id>/jekyll/update/2025/11/19/followup-similar-powerplay-goals</id><content type="html" xml:base="/jekyll/update/2025/11/19/followup-similar-powerplay-goals.html"><![CDATA[<h1 id="introduction">Introduction</h1>

<p>After writing the <a href="https://danielglin.github.io/jekyll/update/2025/11/15/analysing-winnipeg-24-25-power-play-goals.html">previous post about clustering 24-25 Winnipeg Jets power play goals</a>, I saw a <a href="https://www.nhl.com/ppt-replay/goal/2025020293/993">Dallas power play goal</a> that was similar to the largest cluster of Jets power play goals.</p>

<p>That made me wonder how common this kind of power play goal is, where the puck gets moved from the half-wall to the goal line and then to the slot.  So I did a quick analysis to find those kinds of goals in the 24-25 season.</p>

<h1 id="results">Results</h1>

<p>The Winnipeg power play goals I wanted to find similar goals for are:</p>

<ol>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024021031/576">Mar 11, 25: Gabriel Vilardi against the Rangers</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020587/1063">Dec 30, 24: Gabriel Vilardi against the Predators</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020702/791">Jan 14, 25: Mark Scheifele against the Canucks</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020186/885">Nov 3, 24: Alex Iafallo against the Lightning</a></li>
</ol>

<p>I was able to find 13 power play goals from the past season similar to the Winnipeg goals:</p>

<ol>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020652/459">FLA Jan 8, 25: Sam Reinhart against Utah</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020684/743">DET Jan 12, 25: Dylan Larkin against the Kraken</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024021007/738">TOR Mar 8, 25: John Tavares against the Avalanche</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020009/286">VAN Oct 9, 24: Brock Boeser against the Flames</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024021227/1000">PIT Apr 6, 25: Rickard Rakell against the Blackhawks</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020295/697">PIT Nov 19, 24: Rickard Rakell against the Lightning</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020494/820">DAL Dec 16, 24: Roope Hintz against the Capitals</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020911/1018">NJD Feb 23, 25: Stefan Noesen against the Predators</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020498/869">TBL Dec 17, 24: Mitchell Chaffee against the Blue Jackets</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020035/1114">PHI Oct 12, 24: Travis Konecny against the Flames</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024021272/139">DAL Apr 12, 25: Wyatt Johnston against Utah</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020366/313">WSH Nov 29, 24: Tom Wilson against the Islanders</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024021136/578">DET Mar 25, 25: J.T. Compher against the Avalanche</a></li>
</ol>

<h1 id="approach">Approach</h1>

<p>To find power play goals similar to the Winnipeg ones, I used the same hyperdimensional computing encodings of the goal tracking data used to cluster Winnipeg power play goals.<br />
That let me sort goals by similarity to the Winnipeg goals.
Then I used the NHL API to filter on just the power play goals.
After that, I manually reviewed a handful goals to set a similarity threshold, and those that met that threshold were all reviewed.
In all, I reviewed around 30 or so goals.
You can find a reference for the NHL API <a href="https://github.com/Zmalski/NHL-API-Reference">here</a>.</p>

<p>Example Python code:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">dataclasses</span> <span class="kn">import</span> <span class="n">dataclass</span>
<span class="kn">import</span> <span class="nn">json</span>
<span class="kn">import</span> <span class="nn">polars</span> <span class="k">as</span> <span class="n">pl</span>
<span class="kn">import</span> <span class="nn">numpy</span> <span class="k">as</span> <span class="n">np</span>
<span class="kn">import</span> <span class="nn">requests</span>

<span class="o">@</span><span class="n">dataclass</span>
<span class="k">class</span> <span class="nc">Goal</span><span class="p">:</span>
    <span class="n">game</span><span class="p">:</span> <span class="nb">int</span>
    <span class="n">goal</span><span class="p">:</span> <span class="nb">int</span>
        
<span class="o">@</span><span class="n">dataclass</span>
<span class="k">class</span> <span class="nc">GoalDist</span><span class="p">:</span>
    <span class="n">game_id</span><span class="p">:</span> <span class="nb">int</span>
    <span class="n">goal_id</span><span class="p">:</span> <span class="nb">int</span>
    <span class="n">dist</span><span class="p">:</span> <span class="nb">float</span>


<span class="k">def</span> <span class="nf">dist</span><span class="p">(</span><span class="n">x</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="n">ndarray</span><span class="p">,</span> <span class="n">y</span><span class="p">:</span> <span class="n">np</span><span class="p">.</span><span class="n">ndarray</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
    <span class="s">"""
    Calculates the distance between 2 binary hypervectors
    
    PARAMETERS:
        - x (numpy array): binary hypervector of shape (d,)
        - y (numpy array): binary hypervector of shape (d,)
    
    RETURNS:
        - dist (float): Hamming distance between x and y
    """</span>
    <span class="k">if</span> <span class="n">x</span><span class="p">.</span><span class="n">shape</span> <span class="o">!=</span> <span class="n">y</span><span class="p">.</span><span class="n">shape</span><span class="p">:</span>
        <span class="k">raise</span> <span class="nb">ValueError</span><span class="p">(</span><span class="sa">f</span><span class="s">'x and y have different dimensions: </span><span class="si">{</span><span class="n">x</span><span class="p">.</span><span class="n">shape</span><span class="si">}</span><span class="s"> and </span><span class="si">{</span><span class="n">y</span><span class="p">.</span><span class="n">shape</span><span class="si">}</span><span class="s">'</span><span class="p">)</span>
    
    <span class="n">dim</span> <span class="o">=</span> <span class="n">x</span><span class="p">.</span><span class="n">shape</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
    <span class="k">return</span> <span class="n">np</span><span class="p">.</span><span class="n">count_nonzero</span><span class="p">(</span><span class="n">x</span><span class="o">!=</span><span class="n">y</span><span class="p">)</span><span class="o">/</span><span class="n">dim</span>


<span class="c1"># read in exported goal hypervectors that have been created by trimming
# the tracking data to the last 17 instances
</span><span class="n">df_goal_hvs_last_17</span> <span class="o">=</span> <span class="n">pl</span><span class="p">.</span><span class="n">read_parquet</span><span class="p">(</span><span class="s">'goal_hvs.parquet'</span><span class="p">)</span>

<span class="c1"># sort goals by similarity to this Winnipeg one
</span><span class="n">specified_game_id</span> <span class="o">=</span> <span class="mi">2024020587</span>
<span class="n">specified_goal_id</span> <span class="o">=</span> <span class="mi">1063</span>

<span class="n">specified_goal_hv</span> <span class="o">=</span> <span class="n">df_goal_hvs_last_17</span><span class="p">.</span><span class="nb">filter</span><span class="p">(</span>
    <span class="p">(</span><span class="n">pl</span><span class="p">.</span><span class="n">col</span><span class="p">(</span><span class="s">'game_id'</span><span class="p">)</span><span class="o">==</span><span class="n">specified_game_id</span><span class="p">)</span> <span class="o">&amp;</span>
    <span class="p">(</span><span class="n">pl</span><span class="p">.</span><span class="n">col</span><span class="p">(</span><span class="s">'goal_id'</span><span class="p">)</span><span class="o">==</span><span class="n">specified_goal_id</span><span class="p">)</span>
<span class="p">).</span><span class="n">select</span><span class="p">(</span>
    <span class="n">pl</span><span class="p">.</span><span class="n">col</span><span class="p">(</span><span class="s">'goal_hv'</span><span class="p">)</span>
<span class="p">).</span><span class="n">to_series</span><span class="p">().</span><span class="n">to_list</span><span class="p">()[</span><span class="mi">0</span><span class="p">]</span>
<span class="n">specified_goal_hv</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">(</span><span class="n">specified_goal_hv</span><span class="p">)</span>

<span class="n">goal_dists</span> <span class="o">=</span> <span class="p">[]</span>

<span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">df_goal_hvs_last_17</span><span class="p">.</span><span class="nb">filter</span><span class="p">(</span>
    <span class="p">(</span><span class="n">pl</span><span class="p">.</span><span class="n">col</span><span class="p">(</span><span class="s">'game_id'</span><span class="p">)</span><span class="o">!=</span><span class="n">specified_game_id</span><span class="p">)</span> <span class="o">&amp;</span>
    <span class="p">(</span><span class="n">pl</span><span class="p">.</span><span class="n">col</span><span class="p">(</span><span class="s">'goal_id'</span><span class="p">)</span><span class="o">!=</span><span class="n">specified_goal_id</span><span class="p">)</span>
<span class="p">).</span><span class="n">rows</span><span class="p">(</span><span class="n">named</span><span class="o">=</span><span class="bp">True</span><span class="p">):</span>
    <span class="n">other_game_id</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="s">'game_id'</span><span class="p">]</span>
    <span class="n">other_goal_id</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="s">'goal_id'</span><span class="p">]</span>
    <span class="n">other_goal_hv</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">(</span><span class="n">row</span><span class="p">[</span><span class="s">'goal_hv'</span><span class="p">])</span>
    
    <span class="n">goal_dist</span> <span class="o">=</span> <span class="n">dist</span><span class="p">(</span><span class="n">specified_goal_hv</span><span class="p">,</span> <span class="n">other_goal_hv</span><span class="p">)</span>
    
    <span class="n">goal_dists</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">GoalDist</span><span class="p">(</span><span class="n">game_id</span><span class="o">=</span><span class="n">other_game_id</span><span class="p">,</span> <span class="n">goal_id</span><span class="o">=</span><span class="n">other_goal_id</span><span class="p">,</span> <span class="n">dist</span><span class="o">=</span><span class="n">goal_dist</span><span class="p">))</span>

<span class="n">l_sim_goals</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">goal_dists</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">item</span><span class="p">:</span> <span class="n">item</span><span class="p">.</span><span class="n">dist</span><span class="p">)</span>

<span class="c1"># print out info for the most similar ones
</span><span class="n">NUM_PDS</span> <span class="o">=</span> <span class="mi">3</span>

<span class="n">threshold</span> <span class="o">=</span> <span class="mf">0.141</span>

<span class="k">for</span> <span class="n">goal_dist</span> <span class="ow">in</span> <span class="n">l_sim_goals</span><span class="p">:</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">goal_dist</span><span class="p">.</span><span class="n">dist</span> <span class="o">&lt;=</span> <span class="n">threshold</span><span class="p">):</span>
        <span class="n">r</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="sa">f</span><span class="s">'https://api-web.nhle.com/v1/gamecenter/</span><span class="si">{</span><span class="n">goal_dist</span><span class="p">.</span><span class="n">game_id</span><span class="si">}</span><span class="s">/landing'</span><span class="p">)</span>
        <span class="n">landing_str</span> <span class="o">=</span> <span class="n">r</span><span class="p">.</span><span class="n">content</span><span class="p">.</span><span class="n">decode</span><span class="p">()</span>
        <span class="n">dict_landing</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="n">loads</span><span class="p">(</span><span class="n">landing_str</span><span class="p">)</span>

        <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">NUM_PDS</span><span class="p">):</span> <span class="c1"># ignoring OT pp goals   
</span>            <span class="n">l_dict_goals</span> <span class="o">=</span> <span class="n">dict_landing</span><span class="p">[</span><span class="s">'summary'</span><span class="p">][</span><span class="s">'scoring'</span><span class="p">][</span><span class="n">i</span><span class="p">][</span><span class="s">'goals'</span><span class="p">]</span>

            <span class="c1"># go through each goal in the game to find the similar one
</span>            <span class="k">for</span> <span class="n">dict_goal</span> <span class="ow">in</span> <span class="n">l_dict_goals</span><span class="p">:</span>
                <span class="n">event_id</span> <span class="o">=</span> <span class="n">dict_goal</span><span class="p">[</span><span class="s">'eventId'</span><span class="p">]</span>
                <span class="n">strength</span> <span class="o">=</span> <span class="n">dict_goal</span><span class="p">[</span><span class="s">'strength'</span><span class="p">]</span>
                <span class="n">scoring_team</span> <span class="o">=</span> <span class="n">dict_goal</span><span class="p">[</span><span class="s">'teamAbbrev'</span><span class="p">][</span><span class="s">'default'</span><span class="p">]</span>

                <span class="k">if</span> <span class="p">(</span><span class="n">strength</span> <span class="o">==</span> <span class="s">'pp'</span><span class="p">)</span> <span class="ow">and</span> <span class="p">(</span><span class="n">event_id</span> <span class="o">==</span> <span class="n">goal_dist</span><span class="p">.</span><span class="n">goal_id</span><span class="p">):</span>
                    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">'Team: </span><span class="si">{</span><span class="n">scoring_team</span><span class="si">}</span><span class="s">'</span><span class="p">)</span>                    
                    <span class="n">url</span> <span class="o">=</span> <span class="sa">f</span><span class="s">'https://www.nhl.com/ppt-replay/goal/</span><span class="si">{</span><span class="n">goal_dist</span><span class="p">.</span><span class="n">game_id</span><span class="si">}</span><span class="s">/</span><span class="si">{</span><span class="n">goal_dist</span><span class="p">.</span><span class="n">goal_id</span><span class="si">}</span><span class="s">'</span>
                    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">'Game: </span><span class="si">{</span><span class="n">goal_dist</span><span class="p">.</span><span class="n">game_id</span><span class="si">}</span><span class="s">, goal: </span><span class="si">{</span><span class="n">goal_dist</span><span class="p">.</span><span class="n">goal_id</span><span class="si">}</span><span class="s">, dist: </span><span class="si">{</span><span class="n">goal_dist</span><span class="p">.</span><span class="n">dist</span><span class="si">}</span><span class="s">, url: </span><span class="si">{</span><span class="n">url</span><span class="si">}</span><span class="s">'</span><span class="p">)</span>
</code></pre></div></div>

<p>Here is a visualization of the paths the puck took in the similar goals:</p>

<p><img src="/images/wpg_24_25_ppg_clusters/followup_sim_cluster_1.jpg" alt="" /></p>

<p>Each separate line represents a different goal.
The arrows show the direction the puck moves in.
The large overlap between the different goals highlights the similarity between the goals.</p>

<h1 id="conclusion">Conclusion</h1>

<p>When the question, “How common are power play goals like <a href="https://www.nhl.com/ppt-replay/goal/2025020293/993">this one</a>?” popped into my head, the goal encodings I created using the tracking data made it simple to find out.
They helped narrow down the goals I had to review to just a few dozen goals.</p>]]></content><author><name></name></author><category term="jekyll" /><category term="update" /><summary type="html"><![CDATA[Introduction]]></summary></entry><entry><title type="html">Clustering Winnipeg 24-25 Power Play Goals</title><link href="/jekyll/update/2025/11/15/analyzing-winnipeg-24-25-power-play-goals.html" rel="alternate" type="text/html" title="Clustering Winnipeg 24-25 Power Play Goals" /><published>2025-11-15T20:48:18+00:00</published><updated>2025-11-15T20:48:18+00:00</updated><id>/jekyll/update/2025/11/15/analyzing-winnipeg-24-25-power-play-goals</id><content type="html" xml:base="/jekyll/update/2025/11/15/analyzing-winnipeg-24-25-power-play-goals.html"><![CDATA[<h1 id="introduction">Introduction</h1>

<p>I was interested in seeing if I could cluster NHL goals using goal tracking data.
Clustering is a technique to group similar items together.
For a first experiment, I decided to look at a team’s power play goals since that is a smaller number of goals to work with and because I thought there were be more similar plays on the power play.</p>

<p>So I took a look at Winnipeg’s 2024-2025 power play goals since they had the number one power play last regular season.</p>

<h1 id="data">Data</h1>

<p>Last season, the Winnipeg Jets had 62 regulation power play goals and one in overtime.
I focused on just the regulation power play goals.
To get the game and goal id’s of their power play goals, I used the NHL’s API as documented by <a href="https://github.com/Zmalski/NHL-API-Reference?tab=readme-ov-file">Zmalski</a>.  Specifically, I used the club schedule season and game landing endpoints.</p>

<p>I already had the 24-25 regular season goal tracking data downloaded, so I could just use the game and goal id’s of the Jets power play goals to filter the tracking data to just those goals.</p>

<p>Then I encoded the goal tracking data into vectors using the approach I took to find similar goals.
If you’re interested in the details, check out <a href="https://github.com/danielglin/nhl_goal_similarity/">the repo</a>.
This encoding only uses the puck tracking data, not the player tracking data.</p>

<h1 id="results">Results</h1>

<p>The resulting clusters frequently have mostly similar goals.
However, there are some clusters where the goals are pretty different from each other.
In all, 22 clusters were found. 
Below is a graph of the cluster sizes:</p>

<p><img src="/images/wpg_24_25_ppg_clusters/wpg_24_25_ppg_cluster_sizes.png" alt="Cluster Sizes" /></p>

<p>Each bar represents one cluster.</p>

<p>Here is a visualization of the biggest cluster found, which had seven goals:</p>

<p><img src="/images/wpg_24_25_ppg_clusters/wpg_24_25_ppg_grp_1.jpg" alt="Goals" /></p>

<p>The lines are the paths the puck took, and the arrows show the direction the puck headed in.
Arrows that are farther apart show the puck moving faster.</p>

<p>With many goals shown, it’s a little cluttered, but it’s possible to see the puck moving from the dot to near the goal line.
In some goals, a Winnipeg player passes to the slot, while in others, the Jet player attacks the net from the goal line.
One goal that differs from the others is the goal shown in blue, which is Scheifele’s goal against the Sharks.</p>

<p><strong>Update:</strong> I made a <a href="https://danielglin.github.io/jekyll/update/2025/11/19/followup-similar-powerplay-goals.html">follow-up post</a> that searched for power play goals similar to the first four goals in this cluster.</p>

<p>Links to replays of the goals in this cluster:</p>

<ol>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024021031/576">Mar 11, 25: Gabriel Vilardi against the Rangers</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020587/1063">Dec 20, 24: Gabriel Vilardi against the Predators</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020702/791">Jan 14, 25: Mark Scheifele against the Canucks</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020186/885">Nov 3, 24: Alex Iafallo against the Lightning</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020978/1018">Mar 4, 25: Nikolaj Ehlers against the Islanders</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024021119/524">Mar 23, 25: Nino Niederreiter against the Sabres</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020506/657">Dec 17, 24: Mark Scheifele against the Sharks</a></li>
</ol>

<p>Here is a visualization for the second biggest cluster, which had six goals:</p>

<p><img src="/images/wpg_24_25_ppg_clusters/wpg_24_25_ppg_grp_18.jpg" alt="Goals" /></p>

<p>Most of the goals in this group come off a rebound.
However, Vilardi’s goal against the Flames and Ehlers’s against the Sharks aren’t.
The Ehlers goal, in blue, especially stands out as being different against the others.</p>

<p>Links to replays of the goals in this cluster:</p>

<ol>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020359/82">Nov 27, 24: Gabriel Vilardi against the Kings</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020831/315">Feb 1, 25: Cole Perfetti against the Capitals</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024021219/864">Apr 5, 25: Mark Scheifele against Utah</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024021282/584">Apr 13, 25: Alex Iafallo against the Oilers</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020791/903">Jan 26: 25: Gabriel Vilardi against the Flames</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020073/320">Oct 18, 24: Nikolaj Ehlers against the Sharks</a></li>
</ol>

<p>Below is one example of a smaller cluster that has dissimilar goals:</p>

<p><img src="/images/wpg_24_25_ppg_clusters/wpg_24_25_ppg_grp_16.jpg" alt="Goals" /></p>

<p>None of the goals in this cluster are particularly similar to each other, showing that the clustering doesn’t always produce great results.</p>

<p>Links to replays of the goals in this cluster:</p>

<ol>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020452/402">Dec 10, 24: Vladislav Namestnikov against the Bruins</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020812/81">Jan 30, 25: Vladislav Namestnikov against the Bruins</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020247/727">Nov 12, 24: Gabriel Vilardi against the Rangers</a></li>
</ol>

<p>To check out the goals in the other clusters, please take a look at the appendix.</p>

<h1 id="approach">Approach</h1>

<p>In order to cluster the power play goals, I had to first create vector representations for them.
To do so, I used hyperdimensional computing, which uses high-dimensional random vectors to represent data.
You can find more details on this vector representation of goals <a href="https://github.com/danielglin/nhl_goal_similarity/">here</a>.
The repo also has the code to create those representations.</p>

<p>One slight tweak I had to make here is to use just the last 17 instances of each goal’s tracking data.
During some early experiments, I noticed that the earlier instances in the tracking data were throwing off the results.
Most goals had around 90-100 instances in their tracking data after trimming some instances to account for things like data before faceoffs or data after the puck enters the net.</p>

<p>I tried several different numbers of instances to take from the end of the data: 10, 15, 17, and 20, with 17 working the best.</p>

<p>For the clustering algorithm itself, I chose to use agglomerative clustering.
One main reason is because I didn’t want to force goals that were dissimilar from all the other goals into a cluster.
Agglomerative clustering starts with each goal in its own cluster.
Similar clusters then get combined into bigger ones.</p>

<p>Since I used agglomerative clustering, I had to choose the linkage used.
The linkage determines how clusters get combined into bigger ones.
I tried single, complete, and average linkages, with complete ending up doing the best.
Complete linkage looks at two clusters and then takes all the possible pairs where one goal is from one cluster and one is from the other.
The distance between these two clusters is defined as the largest distance between two goals in those pairs.
Then the two clusters with the smallest distance between them gets merged if that distance falls under a threshold.
After trying several different thresholds, I found that a threshold of 0.3 worked the best.
For details on the other linkages, refer to <a href="https://online.stat.psu.edu/stat505/lesson/14/14.4">this page</a>.</p>

<p>To perform clustering, I used <a href="https://scikit-learn.org/stable/modules/generated/sklearn.cluster.AgglomerativeClustering.html">Scikit-Learn’s implementation</a>.
For those unfamiliar, Scikit-Learn is a Python machine learning library.
You can find the Python script I wrote for clustering <a href="https://github.com/danielglin/nhl_goal_similarity/blob/main/analyses/power_plays/winnipeg_24_25_ppg_analysis.py">here</a>.
If you run the clustering, you may end up with slightly different results due to the randomness used when encoding goals as vectors.</p>

<h1 id="appendix">Appendix</h1>

<p>Here are all the clusters found by running clustering on Winnipeg’s 2024-2025 regular season regulation power play goals.
Note that the cluster labels are arbitrary.</p>

<p><strong>Cluster 0</strong></p>

<p><img src="/images/wpg_24_25_ppg_clusters/wpg_24_25_ppg_grp_0.jpg" alt="Goals" /></p>

<ol>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020143/696">https://www.nhl.com/ppt-replay/goal/2024020143/696</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020073/971">https://www.nhl.com/ppt-replay/goal/2024020073/971</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020482/658">https://www.nhl.com/ppt-replay/goal/2024020482/658</a></li>
</ol>

<p><strong>Cluster 1</strong></p>

<p><img src="/images/wpg_24_25_ppg_clusters/wpg_24_25_ppg_grp_1.jpg" alt="Goals" /></p>

<ol>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024021031/576">https://www.nhl.com/ppt-replay/goal/2024021031/576</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020978/1018">https://www.nhl.com/ppt-replay/goal/2024020978/1018</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020506/657">https://www.nhl.com/ppt-replay/goal/2024020506/657</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024021119/524">https://www.nhl.com/ppt-replay/goal/2024021119/524</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020587/1063">https://www.nhl.com/ppt-replay/goal/2024020587/1063</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020702/791">https://www.nhl.com/ppt-replay/goal/2024020702/791</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020186/885">https://www.nhl.com/ppt-replay/goal/2024020186/885</a></li>
</ol>

<p><strong>Cluster 2</strong></p>

<p><img src="/images/wpg_24_25_ppg_clusters/wpg_24_25_ppg_grp_2.jpg" alt="Goals" /></p>

<ol>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020073/941">https://www.nhl.com/ppt-replay/goal/2024020073/941</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020342/1178">https://www.nhl.com/ppt-replay/goal/2024020342/1178</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020978/678">https://www.nhl.com/ppt-replay/goal/2024020978/678</a></li>
</ol>

<p><strong>Cluster 3</strong></p>

<p><img src="/images/wpg_24_25_ppg_clusters/wpg_24_25_ppg_grp_3.jpg" alt="Goals" /></p>

<ol>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020991/67">https://www.nhl.com/ppt-replay/goal/2024020991/67</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020716/670">https://www.nhl.com/ppt-replay/goal/2024020716/670</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020997/529">https://www.nhl.com/ppt-replay/goal/2024020997/529</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024021156/698">https://www.nhl.com/ppt-replay/goal/2024021156/698</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020991/214">https://www.nhl.com/ppt-replay/goal/2024020991/214</a></li>
</ol>

<p><strong>Cluster 4</strong></p>

<p><img src="/images/wpg_24_25_ppg_clusters/wpg_24_25_ppg_grp_4.jpg" alt="Goals" /></p>

<ol>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020156/946">https://www.nhl.com/ppt-replay/goal/2024020156/946</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020201/812">https://www.nhl.com/ppt-replay/goal/2024020201/812</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020008/1090">https://www.nhl.com/ppt-replay/goal/2024020008/1090</a></li>
</ol>

<p><strong>Cluster 5</strong></p>

<p><img src="/images/wpg_24_25_ppg_clusters/wpg_24_25_ppg_grp_5.jpg" alt="Goals" /></p>

<ol>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020439/679">https://www.nhl.com/ppt-replay/goal/2024020439/679</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020297/792">https://www.nhl.com/ppt-replay/goal/2024020297/792</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020506/939">https://www.nhl.com/ppt-replay/goal/2024020506/939</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020536/188">https://www.nhl.com/ppt-replay/goal/2024020536/188</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020452/652">https://www.nhl.com/ppt-replay/goal/2024020452/652</a></li>
</ol>

<p><strong>Cluster 6</strong></p>

<p><img src="/images/wpg_24_25_ppg_clusters/wpg_24_25_ppg_grp_6.jpg" alt="Goals" /></p>

<ol>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020511/614">https://www.nhl.com/ppt-replay/goal/2024020511/614</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020667/981">https://www.nhl.com/ppt-replay/goal/2024020667/981</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020587/1076">https://www.nhl.com/ppt-replay/goal/2024020587/1076</a></li>
</ol>

<p><strong>Cluster 7</strong></p>

<p><img src="/images/wpg_24_25_ppg_clusters/wpg_24_25_ppg_grp_7.jpg" alt="Goals" /></p>

<ol>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020852/464">https://www.nhl.com/ppt-replay/goal/2024020852/464</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020812/795">https://www.nhl.com/ppt-replay/goal/2024020812/795</a></li>
</ol>

<p><strong>Cluster 8</strong></p>

<p><img src="/images/wpg_24_25_ppg_clusters/wpg_24_25_ppg_grp_8.jpg" alt="Goals" /></p>

<ol>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020775/453">https://www.nhl.com/ppt-replay/goal/2024020775/453</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020410/601">https://www.nhl.com/ppt-replay/goal/2024020410/601</a></li>
</ol>

<p><strong>Cluster 9</strong></p>

<p><img src="/images/wpg_24_25_ppg_clusters/wpg_24_25_ppg_grp_9.jpg" alt="Goals" /></p>

<ol>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020536/558">https://www.nhl.com/ppt-replay/goal/2024020536/558</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024021204/711">https://www.nhl.com/ppt-replay/goal/2024021204/711</a></li>
</ol>

<p><strong>Cluster 10</strong></p>

<p><img src="/images/wpg_24_25_ppg_clusters/wpg_24_25_ppg_grp_10.jpg" alt="Goals" /></p>

<ol>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020316/828">https://www.nhl.com/ppt-replay/goal/2024020316/828</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020008/651">https://www.nhl.com/ppt-replay/goal/2024020008/651</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020791/950">https://www.nhl.com/ppt-replay/goal/2024020791/950</a></li>
</ol>

<p><strong>Cluster 11</strong></p>

<p><img src="/images/wpg_24_25_ppg_clusters/wpg_24_25_ppg_grp_11.jpg" alt="Goals" /></p>

<ol>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020230/205">https://www.nhl.com/ppt-replay/goal/2024020230/205</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020482/403">https://www.nhl.com/ppt-replay/goal/2024020482/403</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020129/132">https://www.nhl.com/ppt-replay/goal/2024020129/132</a></li>
</ol>

<p><strong>Cluster 12</strong></p>

<p><img src="/images/wpg_24_25_ppg_clusters/wpg_24_25_ppg_grp_12.jpg" alt="Goals" /></p>

<ol>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020143/619">https://www.nhl.com/ppt-replay/goal/2024020143/619</a></li>
</ol>

<p><strong>Cluster 13</strong></p>

<p><img src="/images/wpg_24_25_ppg_clusters/wpg_24_25_ppg_grp_13.jpg" alt="Goals" /></p>

<ol>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024021271/596">https://www.nhl.com/ppt-replay/goal/2024021271/596</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020073/423">https://www.nhl.com/ppt-replay/goal/2024020073/423</a></li>
</ol>

<p><strong>Cluster 14</strong></p>

<p><img src="/images/wpg_24_25_ppg_clusters/wpg_24_25_ppg_grp_14.jpg" alt="Goals" /></p>

<ol>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020230/646">https://www.nhl.com/ppt-replay/goal/2024020230/646</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020171/1006">https://www.nhl.com/ppt-replay/goal/2024020171/1006</a></li>
</ol>

<p><strong>Cluster 15</strong></p>

<p><img src="/images/wpg_24_25_ppg_clusters/wpg_24_25_ppg_grp_15.jpg" alt="Goals" /></p>

<ol>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020546/373">https://www.nhl.com/ppt-replay/goal/2024020546/373</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020129/1088">https://www.nhl.com/ppt-replay/goal/2024020129/1088</a></li>
</ol>

<p><strong>Cluster 16</strong></p>

<p><img src="/images/wpg_24_25_ppg_clusters/wpg_24_25_ppg_grp_16.jpg" alt="Goals" /></p>

<ol>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020452/402">https://www.nhl.com/ppt-replay/goal/2024020452/402</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020812/81">https://www.nhl.com/ppt-replay/goal/2024020812/81</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020247/727">https://www.nhl.com/ppt-replay/goal/2024020247/727</a></li>
</ol>

<p><strong>Cluster 17</strong></p>

<p><img src="/images/wpg_24_25_ppg_clusters/wpg_24_25_ppg_grp_17.jpg" alt="Goals" /></p>

<ol>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020647/953">https://www.nhl.com/ppt-replay/goal/2024020647/953</a></li>
</ol>

<p><strong>Cluster 18</strong></p>

<p><img src="/images/wpg_24_25_ppg_clusters/wpg_24_25_ppg_grp_18.jpg" alt="Goals" /></p>

<ol>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020359/82">https://www.nhl.com/ppt-replay/goal/2024020359/82</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020831/315">https://www.nhl.com/ppt-replay/goal/2024020831/315</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020073/320">https://www.nhl.com/ppt-replay/goal/2024020073/320</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024021219/864">https://www.nhl.com/ppt-replay/goal/2024021219/864</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024021282/584">https://www.nhl.com/ppt-replay/goal/2024021282/584</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020791/903">https://www.nhl.com/ppt-replay/goal/2024020791/903</a></li>
</ol>

<p><strong>Cluster 19</strong></p>

<p><img src="/images/wpg_24_25_ppg_clusters/wpg_24_25_ppg_grp_19.jpg" alt="Goals" /></p>

<ol>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020099/683">https://www.nhl.com/ppt-replay/goal/2024020099/683</a></li>
</ol>

<p><strong>Cluster 20</strong></p>

<p><img src="/images/wpg_24_25_ppg_clusters/wpg_24_25_ppg_grp_20.jpg" alt="Goals" /></p>

<ol>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020852/569">https://www.nhl.com/ppt-replay/goal/2024020852/569</a></li>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020574/908">https://www.nhl.com/ppt-replay/goal/2024020574/908</a></li>
</ol>

<p><strong>Cluster 21</strong></p>

<p><img src="/images/wpg_24_25_ppg_clusters/wpg_24_25_ppg_grp_21.jpg" alt="Goals" /></p>

<ol>
  <li><a href="https://www.nhl.com/ppt-replay/goal/2024020452/967">https://www.nhl.com/ppt-replay/goal/2024020452/967</a></li>
</ol>]]></content><author><name></name></author><category term="jekyll" /><category term="update" /><summary type="html"><![CDATA[Introduction]]></summary></entry></feed>