<?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>Pass The Puck</title>
<link>https://your-website-url.example.com/</link>
<atom:link href="https://your-website-url.example.com/index.xml" rel="self" type="application/rss+xml"/>
<description>A player tracking blog built with Quarto</description>
<generator>quarto-1.8.24</generator>
<lastBuildDate>Thu, 18 Jun 2026 04:00:00 GMT</lastBuildDate>
<item>
  <title>Feature-Based Expected Goals (xG)</title>
  <dc:creator>Howard Baik</dc:creator>
  <link>https://your-website-url.example.com/posts/feature-based-xg/</link>
  <description><![CDATA[ 





<section id="introduction" class="level2">
<h2 class="anchored" data-anchor-id="introduction">Introduction</h2>
<p>This post is an implementation of the Feature-Based Expected Goals (xG) Model from Section 2.4 of <a href="https://www.linkedin.com/in/jonathan-arsenault-a0b414114/">Jonathan Arsenault</a>’s PhD thesis, <a href="https://mcgill.scholaris.ca/items/da91938f-30c7-4f75-9ca3-2402a9e5422f"><em>Quantitative Analysis of Hockey Using Spatiotemporal Tracking Data</em></a>. As the thesis puts it, “xG models use a supervised learning algorithm to establish a mapping between a set of features that describe a shot and the probability of the shot resulting in a goal.” Here, those features come from event and tracking data and capture shooter location, shooter motion, pressure from defenders, pre-shot puck movement, traffic in the shooting lane, and goaltender positioning. We will create each of these features, then train an XGBoost model to predict each shot’s probability of becoming a goal.</p>
<p>We will use data from the <a href="https://github.com/bigdatacup/Big-Data-Cup-2026">Big Data Cup 2026</a>, which spans 10 games and combines Stathletes-tracked event data with player tracking data generated from broadcast video.</p>
<p>All code is written using Claude Opus 4.8 and verified by the author.</p>
<section id="data-import" class="level3">
<h3 class="anchored" data-anchor-id="data-import">Data Import</h3>
<p>We import the data from the <a href="https://github.com/bigdatacup/Big-Data-Cup-2026/releases/tag/Data">Big Data Cup 2026 GitHub repository</a> and tag each events and tracking data with a Game ID. Then, we convert the clock strings (“MM:SS” / “M:SS”) to total seconds for easier comparison and joining between the events and tracking datasets. The tracking data is separated by period (P1/P2/P3/OT), so we combine all the tracking files into one dataset of every tracked player-frame across all games and periods.</p>
<div id="945cd7c6" class="cell" data-execution_count="1">
<details class="code-fold">
<summary>Show code</summary>
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb1-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> polars <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> pl</span>
<span id="cb1-2"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> great_tables <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> GT</span>
<span id="cb1-3"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> math</span>
<span id="cb1-4"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> json</span>
<span id="cb1-5"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> urllib.request, io</span>
<span id="cb1-6"></span>
<span id="cb1-7"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Convert clock strings ("MM:SS" / "M:SS") to total seconds.</span></span>
<span id="cb1-8"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># events.Clock uses zero-padded minutes ("09:30"); tracking "Game Clock" is unpadded ("9:30").</span></span>
<span id="cb1-9"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Casting both to integer seconds makes them directly comparable for joins.</span></span>
<span id="cb1-10"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> clock_to_seconds(col):</span>
<span id="cb1-11">    parts <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> pl.col(col).<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">str</span>.split(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">":"</span>)</span>
<span id="cb1-12">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> (</span>
<span id="cb1-13">        parts.<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">list</span>.get(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>).cast(pl.Int64) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">60</span></span>
<span id="cb1-14">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> parts.<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">list</span>.get(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>).cast(pl.Int64)</span>
<span id="cb1-15">    )</span>
<span id="cb1-16"></span>
<span id="cb1-17"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Add the seconds column and slot it right after its source column.</span></span>
<span id="cb1-18"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> add_seconds_after(df, src, new):</span>
<span id="cb1-19">    df <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> df.with_columns(clock_to_seconds(src).alias(new))</span>
<span id="cb1-20">    cols <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [c <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> c <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> df.columns <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> c <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">!=</span> new]</span>
<span id="cb1-21">    cols.insert(df.get_column_index(src) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, new)</span>
<span id="cb1-22">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> df.select(cols)</span>
<span id="cb1-23"></span>
<span id="cb1-24"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Every game is read directly from the Big Data Cup 2026 release (no local download).</span></span>
<span id="cb1-25"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Each row is tagged with a "Game" id (the filename stem, e.g. "2025-10-11.Team.A.@.Team.D"),</span></span>
<span id="cb1-26"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># identical for a game's Events and Tracking files. That key is required downstream: the</span></span>
<span id="cb1-27"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># shots&lt;-&gt;tracking joins match on Period + Frame, but Frame is only a per-game counter, so</span></span>
<span id="cb1-28"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># without "Game" they would cross-match rows between different games.</span></span>
<span id="cb1-29">_RELEASE_API <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"https://api.github.com/repos/bigdatacup/Big-Data-Cup-2026/releases/tags/Data"</span></span>
<span id="cb1-30">_assets <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> {a[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"name"</span>]: a[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"browser_download_url"</span>]</span>
<span id="cb1-31">           <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> a <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> json.load(urllib.request.urlopen(_RELEASE_API))[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"assets"</span>]}</span>
<span id="cb1-32"></span>
<span id="cb1-33"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> _read_url(url, <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span>kwargs):</span>
<span id="cb1-34">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> pl.read_csv(io.BytesIO(urllib.request.urlopen(url).read()), <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span>kwargs)</span>
<span id="cb1-35"></span>
<span id="cb1-36"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Events: one file per game.</span></span>
<span id="cb1-37">event_frames <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> []</span>
<span id="cb1-38"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> name, url <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> _assets.items():</span>
<span id="cb1-39">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">not</span> name.endswith(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">".Events.csv"</span>):</span>
<span id="cb1-40">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">continue</span></span>
<span id="cb1-41">    game <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> name[: <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">".Events.csv"</span>)]</span>
<span id="cb1-42">    df <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> _read_url(url, schema_overrides<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>{<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Player_Id"</span>: pl.String, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Player_Id_2"</span>: pl.String})</span>
<span id="cb1-43">    df <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> add_seconds_after(df, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Clock"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Clock_Seconds"</span>)</span>
<span id="cb1-44">    df <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> df.with_columns(pl.lit(game).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game"</span>))</span>
<span id="cb1-45">    event_frames.append(df)</span>
<span id="cb1-46">events <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> pl.concat(event_frames)</span>
<span id="cb1-47"></span>
<span id="cb1-48"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Tracking: P1/P2/P3 (+ POT overtime). ".Tracking_P" excludes Shifts and camera_orientations.</span></span>
<span id="cb1-49">track_frames <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> []</span>
<span id="cb1-50"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> name, url <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> _assets.items():</span>
<span id="cb1-51">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">".Tracking_P"</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">not</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> name:</span>
<span id="cb1-52">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">continue</span></span>
<span id="cb1-53">    game <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> name.split(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">".Tracking_"</span>)[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>]</span>
<span id="cb1-54">    df <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> _read_url(url, schema_overrides<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>{<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location Z (Feet)"</span>: pl.Float64, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>: pl.String}).drop(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Player Id"</span>)</span>
<span id="cb1-55">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Overtime tracking labels Period as "OT"; events use 4 for OT. Normalize to 4 (Int64)</span></span>
<span id="cb1-56">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># so the dtype is consistent across files and OT shots can join their tracking frames.</span></span>
<span id="cb1-57">    df <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> df.with_columns(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>).replace(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"OT"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"4"</span>).cast(pl.Int64))</span>
<span id="cb1-58">    df <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> add_seconds_after(df, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game Clock"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game_Clock_Seconds"</span>)</span>
<span id="cb1-59">    df <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> df.with_columns(pl.lit(game).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game"</span>))</span>
<span id="cb1-60">    track_frames.append(df)</span>
<span id="cb1-61"></span>
<span id="cb1-62"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># One table of every tracked player-frame across all games and periods.</span></span>
<span id="cb1-63">tracking <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> pl.concat(track_frames)</span></code></pre></div></div>
</details>
</div>
<p><img src="https://your-website-url.example.com/posts/feature-based-xg/events_data_table.png" class="img-fluid"></p>
<p><img src="https://your-website-url.example.com/posts/feature-based-xg/tracking_data_table.png" class="img-fluid"></p>
</section>
<section id="distribution-of-shot-types" class="level3">
<h3 class="anchored" data-anchor-id="distribution-of-shot-types">Distribution of Shot Types</h3>
<p>We look at the distribution of shot types in the events dataset. We filter for <code>Shot</code> and <code>Goal</code> events and count the number of unique occurences of the <code>Detail 1</code> column, which indicates the type of shot taken. We then visualize this distribution using a bar chart.</p>
<p><img src="https://your-website-url.example.com/posts/feature-based-xg/shot_types_distribution.png" class="img-fluid"></p>
<div id="95a21532" class="cell" data-execution_count="2">
<details class="code-fold">
<summary>Show code</summary>
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb2-1">shot_types <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (</span>
<span id="cb2-2">    events.<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">filter</span>(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Event"</span>).is_in([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Goal"</span>]))</span>
<span id="cb2-3">    .group_by(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Detail_1"</span>)</span>
<span id="cb2-4">    .agg(pl.<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>().alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"count"</span>))</span>
<span id="cb2-5">    .with_columns((pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"count"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"count"</span>).<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">sum</span>()).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"pct"</span>))</span>
<span id="cb2-6">    .sort(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"count"</span>, descending<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>)</span>
<span id="cb2-7">)</span>
<span id="cb2-8"></span>
<span id="cb2-9"></span>
<span id="cb2-10"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> matplotlib.pyplot <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> plt</span>
<span id="cb2-11"></span>
<span id="cb2-12">fig, ax <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> plt.subplots(figsize<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">10</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">6</span>))</span>
<span id="cb2-13"></span>
<span id="cb2-14"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Create bar chart with percentages</span></span>
<span id="cb2-15">ax.bar(shot_types[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Detail_1"</span>], shot_types[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"pct"</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">100</span>, color<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"#1f77b4"</span>, edgecolor<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"black"</span>, alpha<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.7</span>)</span>
<span id="cb2-16"></span>
<span id="cb2-17"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Customize the plot</span></span>
<span id="cb2-18">ax.set_xlabel(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot Type"</span>, fontsize<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">12</span>, fontweight<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bold"</span>)</span>
<span id="cb2-19">ax.set_ylabel(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Percentage (%)"</span>, fontsize<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">12</span>, fontweight<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bold"</span>)</span>
<span id="cb2-20">ax.set_title(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Distribution of Shot Types (Percentage)"</span>, fontsize<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">14</span>, fontweight<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bold"</span>)</span>
<span id="cb2-21"></span>
<span id="cb2-22"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Rotate x-axis labels for better readability</span></span>
<span id="cb2-23">plt.xticks(rotation<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">45</span>, ha<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"right"</span>)</span>
<span id="cb2-24"></span>
<span id="cb2-25"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Add percentage labels on top of bars</span></span>
<span id="cb2-26"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> i, v <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">enumerate</span>(shot_types[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"pct"</span>]):</span>
<span id="cb2-27">    ax.text(i, v <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">100</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>v<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">100</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">:.1f}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">%"</span>, ha<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"center"</span>, va<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bottom"</span>, fontsize<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">8</span>)</span>
<span id="cb2-28"></span>
<span id="cb2-29">plt.tight_layout()</span>
<span id="cb2-30">plt.show()</span></code></pre></div></div>
</details>
</div>
<p>The shot type distribution in our dataset is very similar to the thesis’s dataset. Table 2.1 from the thesis reveals that Wrist Shots (47.8%) and Snap Shots (25.5%) are the most common, followed by Slap Shots (15.6%) and Deflections (4.5%).</p>
</section>
<section id="distribution-of-shot-outcomes" class="level3">
<h3 class="anchored" data-anchor-id="distribution-of-shot-outcomes">Distribution of Shot Outcomes</h3>
<p>We look at the shot outcomes in the events dataset. We filter for <code>Shot</code> and <code>Goal</code> events and count the number of unique occurences of the <code>Detail 2</code> column, which indicates the outcome of the shot (e.g., On Net, Missed, Blocked).</p>
<div id="67a61e32" class="cell" data-execution_count="3">
<details class="code-fold">
<summary>Show code</summary>
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb3-1">descriptions <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> {</span>
<span id="cb3-2">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Saved"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"The shot is on target and saved by the goaltender."</span>,</span>
<span id="cb3-3">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Goal"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"The shot is on target and not saved by the goaltender, resulting in a goal."</span>,</span>
<span id="cb3-4">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Blocked"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"The shot is blocked by a skater from the defending team."</span>,</span>
<span id="cb3-5">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Missed"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"The shot is not on target."</span>,</span>
<span id="cb3-6">}</span>
<span id="cb3-7"></span>
<span id="cb3-8">shot_outcomes <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (</span>
<span id="cb3-9">    events.<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">filter</span>(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Event"</span>).is_in([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Goal"</span>]))</span>
<span id="cb3-10">    .with_columns(</span>
<span id="cb3-11">        pl.when(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Event"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Goal"</span>)</span>
<span id="cb3-12">        .then(pl.lit(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Goal"</span>))</span>
<span id="cb3-13">        .when((pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Event"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span> (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Detail_2"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"On Net"</span>))</span>
<span id="cb3-14">        .then(pl.lit(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Saved"</span>))</span>
<span id="cb3-15">        .when((pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Event"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span> (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Detail_2"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Blocked"</span>))</span>
<span id="cb3-16">        .then(pl.lit(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Blocked"</span>))</span>
<span id="cb3-17">        .when((pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Event"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span> (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Detail_2"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Missed"</span>))</span>
<span id="cb3-18">        .then(pl.lit(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Missed"</span>))</span>
<span id="cb3-19">        .alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Outcome"</span>)</span>
<span id="cb3-20">    )</span>
<span id="cb3-21">    .group_by(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Outcome"</span>)</span>
<span id="cb3-22">    .agg(pl.<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>().alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"count"</span>))</span>
<span id="cb3-23">    .with_columns((pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"count"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"count"</span>).<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">sum</span>()).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"pct"</span>))</span>
<span id="cb3-24">    .with_columns(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Outcome"</span>).replace(descriptions).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Description"</span>))</span>
<span id="cb3-25">    .select(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Outcome"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Description"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"pct"</span>)</span>
<span id="cb3-26">    .sort(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"pct"</span>, descending<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>)</span>
<span id="cb3-27">)</span>
<span id="cb3-28"></span>
<span id="cb3-29">(</span>
<span id="cb3-30">    GT(shot_outcomes)</span>
<span id="cb3-31">    .tab_header(title<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Distribution of shot outcomes in the dataset"</span>)</span>
<span id="cb3-32">    .cols_label(</span>
<span id="cb3-33">        Outcome<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot Outcome"</span>,</span>
<span id="cb3-34">        Description<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Description"</span>,</span>
<span id="cb3-35">        pct<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">% o</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">f Shots"</span>,</span>
<span id="cb3-36">    )</span>
<span id="cb3-37">    .fmt_percent(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"pct"</span>, decimals<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>)</span>
<span id="cb3-38">    .cols_width({<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Outcome"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"130px"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Description"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"420px"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"pct"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"110px"</span>})</span>
<span id="cb3-39">)</span></code></pre></div></div>
</details>
</div>
<p><img src="https://your-website-url.example.com/posts/feature-based-xg/shot_outcomes_table.png" class="img-fluid"></p>
<p>Again, the shot outcome distribution in our dataset is very similar to the thesis’s dataset. Table 2.2 from the thesis shows that the Saved outcome is the most common (48.3%), followed by Blocked (26.0%), Missed (21.4%), and Goal (4.21%). It is perfectly normal for the Goal outcome to be the least common, as goals are rare in hockey.</p>
</section>
<section id="distribution-of-shot-location" class="level3">
<h3 class="anchored" data-anchor-id="distribution-of-shot-location">Distribution of Shot Location</h3>
<p>We look at the distribution of shot locations in the events dataset. We filter for <code>Shot</code> and <code>Goal</code> events and visualize the distribution of the <code>X_Coordinate</code> and <code>Y_Coordinate</code> columns on a half-sized ice rink, along with the shot angle.</p>
<p><br></p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://your-website-url.example.com/posts/feature-based-xg/goal_locations_ice_rink.png" class="img-fluid quarto-figure quarto-figure-center figure-img" style="width:55.0%"></p>
</figure>
</div>
<p>There are 58 goals in our 10-game dataset and most of the goals are from the high-danger areas of the rink: in front of the goal and near the goal line. Low shot angles mean the shot was taken from the area in front of the goaltender, a high-danger area of the rink. High shot angles mean that the shot was taken from the area near the boards, a low-danger area of the rink.</p>
</section>
</section>
<section id="feature-engineering" class="level2">
<h2 class="anchored" data-anchor-id="feature-engineering">Feature Engineering</h2>
<p>We extract features from the <em>events and tracking</em> data for the Features-Based xG Model in the thesis. Since the thesis certainly draws on different data than the 2026 Big Data Cup, we make a few assumptions along the way to match its features as closely as possible.</p>
<section id="shot-location" class="level3">
<h3 class="anchored" data-anchor-id="shot-location">Shot Location</h3>
<p>Section 2.4.1.1 of the thesis defines three shot-location variables: the shot angle, the shot distance, and the meridian distance.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://your-website-url.example.com/posts/feature-based-xg/shot-location.png" class="img-fluid quarto-figure quarto-figure-center figure-img" style="width:40.0%"></p>
</figure>
</div>
<ol type="1">
<li>The shot angle <img src="https://latex.codecogs.com/png.latex?%5Ctheta"> in degrees is defined as:</li>
</ol>
<p><img src="https://latex.codecogs.com/png.latex?%0A%5Ctheta%20=%5Cfrac%7B180%7D%7B%5Cpi%7D%20%5Cleft%7C%20%5Ctext%7Batan2%7D(y,%20x%20-%2089)%20%5Cright%7C.%0A"></p>
<ol start="2" type="1">
<li><p>The meridian distance <img src="https://latex.codecogs.com/png.latex?d_m"> is the perpendicular distance from the shot location to the meridian (the “royal road” running down the center of the ice).</p></li>
<li><p>The shot distance <img src="https://latex.codecogs.com/png.latex?d"> is the distance between the shot location and the goal.</p></li>
</ol>
<div id="068fc30a" class="cell" data-execution_count="4">
<details class="code-fold">
<summary>Show code</summary>
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb4-1">(</span>
<span id="cb4-2">    events</span>
<span id="cb4-3">    .<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">filter</span>(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Event"</span>).is_in([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Goal"</span>]))</span>
<span id="cb4-4">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Normalize to right side</span></span>
<span id="cb4-5">    .with_columns(</span>
<span id="cb4-6">        pl.when(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"X_Coordinate"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>)</span>
<span id="cb4-7">        .then(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"X_Coordinate"</span>))</span>
<span id="cb4-8">        .otherwise(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"X_Coordinate"</span>))</span>
<span id="cb4-9">        .alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"X_Coordinate"</span>),</span>
<span id="cb4-10"></span>
<span id="cb4-11">        pl.when(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"X_Coordinate"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>)</span>
<span id="cb4-12">        .then(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Y_Coordinate"</span>))</span>
<span id="cb4-13">        .otherwise(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Y_Coordinate"</span>))</span>
<span id="cb4-14">        .alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Y_Coordinate"</span>),</span>
<span id="cb4-15">    )</span>
<span id="cb4-16">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Shot angle (degrees) and meridian distance</span></span>
<span id="cb4-17">    .with_columns(</span>
<span id="cb4-18">        (</span>
<span id="cb4-19">            pl.arctan2(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Y_Coordinate"</span>), <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">89</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"X_Coordinate"</span>))</span>
<span id="cb4-20">            .<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">abs</span>()</span>
<span id="cb4-21">            <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> (<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">180</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> math.pi)</span>
<span id="cb4-22">        ).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot_Angle"</span>),</span>
<span id="cb4-23">        pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Y_Coordinate"</span>).<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">abs</span>().alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"d_m"</span>),</span>
<span id="cb4-24">    )</span>
<span id="cb4-25">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Distance via trigonometry: d = d_m / sin(θ), fallback to (89 - x) when Y = 0</span></span>
<span id="cb4-26">    .with_columns(</span>
<span id="cb4-27">        pl.when(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"d_m"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>)</span>
<span id="cb4-28">        .then(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">89</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"X_Coordinate"</span>))</span>
<span id="cb4-29">        .otherwise(</span>
<span id="cb4-30">            pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"d_m"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot_Angle"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> math.pi <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">180</span>).sin()</span>
<span id="cb4-31">        )</span>
<span id="cb4-32">        .alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"d_trig"</span>),</span>
<span id="cb4-33">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Euclidean distance as sanity check</span></span>
<span id="cb4-34">        ((<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">89</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"X_Coordinate"</span>)).<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">pow</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Y_Coordinate"</span>).<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">pow</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>))</span>
<span id="cb4-35">        .sqrt()</span>
<span id="cb4-36">        .alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"d_euclidean"</span>),</span>
<span id="cb4-37">    )</span>
<span id="cb4-38">)</span></code></pre></div></div>
</details>
</div>
<p><img src="https://your-website-url.example.com/posts/feature-based-xg/shot_location_table.png" class="img-fluid"></p>
</section>
<section id="shooter-motion" class="level3">
<h3 class="anchored" data-anchor-id="shooter-motion">Shooter Motion</h3>
<p>Section 2.4.1.2 of the thesis describes two additional features: shooter’s speed and direction of travel. For convenience, our model only uses shooter’s speed, which is the magnitude of the shooter’s velocity at the instant of their shot.</p>
<p>There are a few parameters we set before the feature engineering process:</p>
<ol type="1">
<li><code>FPS = 30</code> is the tracking frame rate (frames per second). The tracking dataset contains the <code>Image ID</code> column, whose suffix is a frame counter that ticks ~30 times per game-clock second (e.g.&nbsp;<code>_066486</code> -&gt; <code>_066487</code> -&gt; <code>_066488</code>). On average, there are 30 frames per game-clock second, and we store this information in <code>FPS</code> to convert frames to seconds using this formula:</li>
</ol>
<p><img src="https://latex.codecogs.com/png.latex?%0A%5Ctext%7Btime%20(seconds)%7D%20=%20%5Cfrac%7B%5Ctext%7Bframe%20count%7D%7D%7B%5Ctext%7BFPS%7D%7D%0A"></p>
<p>  &nbsp; Later, we use this time interval to calculate speed:</p>
<p><img src="https://latex.codecogs.com/png.latex?%0A%5Ctext%7Bspeed%20(ft/s)%7D%20=%20%5Cfrac%7B%5Ctext%7Bdistance%20(ft)%7D%7D%7B%5Ctext%7Btime%20(seconds)%7D%7D%0A"></p>
<ol start="2" type="1">
<li><code>SMOOTH_K = 3</code> is the window (in frames) used to estimate speed by central difference. Rather than measuring how far a player moved between two <em>adjacent</em> frames, which is noisy, we compare the player’s position <code>SMOOTH_K</code> frames ahead with their position <code>SMOOTH_K</code> frames behind, then divide by the time that elapsed between those two samples:</li>
</ol>
<p><img src="https://latex.codecogs.com/png.latex?%0A%5Ctext%7Bspeed%7D%20%5Capprox%20%5Cfrac%7B%5Csqrt%7B(x_%7Bi+k%7D%20-%20x_%7Bi-k%7D)%5E2%20+%20(y_%7Bi+k%7D%20-%20y_%7Bi-k%7D)%5E2%7D%7D%7B(%5Ctext%7Bframe%7D_%7Bi+k%7D%20-%20%5Ctext%7Bframe%7D_%7Bi-k%7D)%20/%20%5Ctext%7BFPS%7D%7D,%20%5Cquad%20k%20=%20%5Ctexttt%7BSMOOTH%5C_K%7D.%0A"></p>
<p>  &nbsp; This method averages out per-frame jitter and gives a more stable speed estimate.</p>
<ol start="3" type="1">
<li><p><code>FT_PER_S_TO_MPH = 0.6818</code> is the conversion factor from feet per second (fps) to miles per hour (mph).</p></li>
<li><p><code>MAX_SPEED_MPH = 30.0</code> is the maximum speed (in mph) we consider valid for our model. In the 25/26 NHL season, the maximum skating speed for a player was just under 25 mph, so a speed above 30 mph is determined a tracking glitch.</p></li>
</ol>
<p>To compute player speed, we first restrict the tracking data to player rows, excluding the puck. We read the frame number from the suffix of the <code>Image Id</code> column, so that an <code>Image Id</code> of <code>2025-10-11 Team A @ Team D_065468</code> gives frame <code>065468</code>. After sorting each player’s frames in chronological order, we take the change in <code>x</code> and <code>y</code> across the <code>SMOOTH_K</code>-frame central-difference window and divide the resulting Euclidean displacement by the elapsed time, converting frames to seconds with the <code>FPS = 30</code> factor introduced above. We call this new dataset “players”.</p>
<div id="65c43972" class="cell" data-execution_count="5">
<details class="code-fold">
<summary>Show code</summary>
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb5-1">FPS <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">30</span>                  </span>
<span id="cb5-2">SMOOTH_K <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>              </span>
<span id="cb5-3">FT_PER_S_TO_MPH <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.6818</span>  </span>
<span id="cb5-4">MAX_SPEED_MPH <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">30.0</span>      </span>
<span id="cb5-5">MAX_SPEED_FPS <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> MAX_SPEED_MPH <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> FT_PER_S_TO_MPH</span>
<span id="cb5-6"></span>
<span id="cb5-7">players <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (</span>
<span id="cb5-8">    tracking</span>
<span id="cb5-9">    .<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">filter</span>(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Player or Puck"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Player"</span>)</span>
<span id="cb5-10">    .with_columns(</span>
<span id="cb5-11">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Numeric frame index from the Image Id suffix, and jersey as a string for matching.</span></span>
<span id="cb5-12">        pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Image Id"</span>).<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">str</span>.split(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"_"</span>).<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">list</span>.last().cast(pl.Int64).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>),</span>
<span id="cb5-13">        pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Player Jersey Number"</span>).cast(pl.String).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Jersey"</span>),</span>
<span id="cb5-14">    )</span>
<span id="cb5-15">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Drop rows whose jersey couldn't be read: they'd all collapse into one bogus "null"</span></span>
<span id="cb5-16">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># track, and differencing positions of *different* players invents huge fake speeds.</span></span>
<span id="cb5-17">    .<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">filter</span>(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Jersey"</span>).is_not_null())</span>
<span id="cb5-18">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Order each player's frames in time so shift() walks consecutive samples.</span></span>
<span id="cb5-19">    .sort([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Team"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Jersey"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>])</span>
<span id="cb5-20">    .with_columns(</span>
<span id="cb5-21">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Central difference over +/- SMOOTH_K frames, partitioned per player-period.</span></span>
<span id="cb5-22">        (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location X (Feet)"</span>).shift(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>SMOOTH_K) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location X (Feet)"</span>).shift(SMOOTH_K))</span>
<span id="cb5-23">            .over([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Team"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Jersey"</span>]).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"dx"</span>),</span>
<span id="cb5-24">        (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location Y (Feet)"</span>).shift(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>SMOOTH_K) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location Y (Feet)"</span>).shift(SMOOTH_K))</span>
<span id="cb5-25">            .over([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Team"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Jersey"</span>]).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"dy"</span>),</span>
<span id="cb5-26">        (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>).shift(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>SMOOTH_K) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>).shift(SMOOTH_K))</span>
<span id="cb5-27">            .over([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Team"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Jersey"</span>]).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"dframe"</span>),</span>
<span id="cb5-28">    )</span>
<span id="cb5-29">    .with_columns(</span>
<span id="cb5-30">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># dt = frames / fps (seconds); speed = displacement / dt, in feet per second.</span></span>
<span id="cb5-31">        ((pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"dx"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"dy"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>).sqrt() <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"dframe"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> FPS)).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"speed_fps"</span>)</span>
<span id="cb5-32">    )</span>
<span id="cb5-33">    .with_columns(</span>
<span id="cb5-34">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Null out physically impossible speeds (tracking ID-swaps / position jumps) so</span></span>
<span id="cb5-35">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># they don't poison the per-shot pick or its fallback median.</span></span>
<span id="cb5-36">        pl.when(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"speed_fps"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;=</span> MAX_SPEED_FPS).then(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"speed_fps"</span>)).otherwise(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"speed_fps"</span>)</span>
<span id="cb5-37">    )</span>
<span id="cb5-38">    .with_columns(</span>
<span id="cb5-39">        (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"speed_fps"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> FT_PER_S_TO_MPH).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"speed_mph"</span>)</span>
<span id="cb5-40">    )</span>
<span id="cb5-41">)</span></code></pre></div></div>
</details>
</div>
<p>Now that we have player speed, we filter the events data for shots and goals, calling this dataset “shots”. We left join “players” onto “shots” to create shooter tracking data, then keep only the shooter’s frames within one second of the shot. Among those, we pick the frame whose tracked position is closest (by Euclidean distance) to where the event records the shot — the moment the tracking best lines up with the recorded release point — and read off the shooter’s speed there. The result is the shots dataset with one shooter-speed column per shot, which we label with <code>shot_id</code>.</p>
<div id="3bac087b" class="cell" data-execution_count="6">
<details class="code-fold">
<summary>Show code</summary>
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb6-1">shots <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (</span>
<span id="cb6-2">    events.<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">filter</span>(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Event"</span>).is_in([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Goal"</span>]))</span>
<span id="cb6-3">    .with_columns(</span>
<span id="cb6-4">        pl.when(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Team"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Home_Team"</span>)).then(pl.lit(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Home"</span>))</span>
<span id="cb6-5">          .otherwise(pl.lit(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Away"</span>)).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Track_Team"</span>)</span>
<span id="cb6-6">    )</span>
<span id="cb6-7">    .with_row_index(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>)</span>
<span id="cb6-8">)</span>
<span id="cb6-9"></span>
<span id="cb6-10"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Candidate shooter frames: same player, within the event second (+/-1s).</span></span>
<span id="cb6-11">cand <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (</span>
<span id="cb6-12">    shots.join(</span>
<span id="cb6-13">        players.select([</span>
<span id="cb6-14">            <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Team"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Jersey"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game_Clock_Seconds"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>,</span>
<span id="cb6-15">            <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location X (Feet)"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location Y (Feet)"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"speed_fps"</span>,</span>
<span id="cb6-16">        ]),</span>
<span id="cb6-17">        left_on<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Track_Team"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Player_Id"</span>],</span>
<span id="cb6-18">        right_on<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Team"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Jersey"</span>], how<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"left"</span>,</span>
<span id="cb6-19">    )</span>
<span id="cb6-20">    .<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">filter</span>((pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game_Clock_Seconds"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Clock_Seconds"</span>)).<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">abs</span>() <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>)</span>
<span id="cb6-21">    .<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">filter</span>(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location X (Feet)"</span>).is_not_null()</span>
<span id="cb6-22">            <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location Y (Feet)"</span>).is_not_null())</span>
<span id="cb6-23">    .with_columns(</span>
<span id="cb6-24">        ((pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location X (Feet)"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"X_Coordinate"</span>)) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span></span>
<span id="cb6-25">         <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location Y (Feet)"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Y_Coordinate"</span>)) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>).sqrt().alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"d_direct"</span>),</span>
<span id="cb6-26">        ((pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location X (Feet)"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"X_Coordinate"</span>)) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span></span>
<span id="cb6-27">         <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location Y (Feet)"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Y_Coordinate"</span>)) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>).sqrt().alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"d_flip"</span>),</span>
<span id="cb6-28">    )</span>
<span id="cb6-29">    .with_columns(</span>
<span id="cb6-30">        pl.min_horizontal(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"d_direct"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"d_flip"</span>).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"match_dist"</span>),</span>
<span id="cb6-31">        (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"d_flip"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"d_direct"</span>)).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"used_flip"</span>),</span>
<span id="cb6-32">    )</span>
<span id="cb6-33">)</span>
<span id="cb6-34"></span>
<span id="cb6-35"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Release frame = the candidate closest to the release point (nulls_last so a real</span></span>
<span id="cb6-36"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># match is never beaten by a missing distance).</span></span>
<span id="cb6-37">release <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (</span>
<span id="cb6-38">    cand.sort(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"match_dist"</span>, nulls_last<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>)</span>
<span id="cb6-39">    .group_by(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, maintain_order<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>)</span>
<span id="cb6-40">    .first()</span>
<span id="cb6-41">    .select([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"match_dist"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"used_flip"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"speed_fps"</span>])</span>
<span id="cb6-42">)</span>
<span id="cb6-43"></span>
<span id="cb6-44"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Fallback: representative speed over the whole event-second window.</span></span>
<span id="cb6-45">window_med <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> cand.group_by(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>).agg(</span>
<span id="cb6-46">    pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"speed_fps"</span>).median().alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"speed_fps_window_median"</span>),</span>
<span id="cb6-47">    pl.<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>().alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"n_candidates"</span>),</span>
<span id="cb6-48">)</span>
<span id="cb6-49"></span>
<span id="cb6-50"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># A match is trusted when the closest frame is within ~10 ft and has a defined speed;</span></span>
<span id="cb6-51"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># otherwise fall back to the window median (flagged), or mark no tracking at all.</span></span>
<span id="cb6-52">MAX_MATCH_DIST <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">10.0</span></span>
<span id="cb6-53">shot_speeds <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (</span>
<span id="cb6-54">    shots.join(release, on<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, how<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"left"</span>)</span>
<span id="cb6-55">    .join(window_med, on<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, how<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"left"</span>)</span>
<span id="cb6-56">    .with_columns(</span>
<span id="cb6-57">        pl.when((pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"match_dist"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;=</span> MAX_MATCH_DIST) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"speed_fps"</span>).is_not_null())</span>
<span id="cb6-58">          .then(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"speed_fps"</span>))</span>
<span id="cb6-59">          .otherwise(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"speed_fps_window_median"</span>))</span>
<span id="cb6-60">          .alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shooter_speed_fps"</span>),</span>
<span id="cb6-61">        pl.when((pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"match_dist"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;=</span> MAX_MATCH_DIST) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"speed_fps"</span>).is_not_null())</span>
<span id="cb6-62">          .then(pl.lit(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"matched"</span>))</span>
<span id="cb6-63">          .when(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"speed_fps_window_median"</span>).is_not_null())</span>
<span id="cb6-64">          .then(pl.lit(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"fallback_window_median"</span>))</span>
<span id="cb6-65">          .otherwise(pl.lit(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"no_tracking"</span>))</span>
<span id="cb6-66">          .alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"match_quality"</span>),</span>
<span id="cb6-67">    )</span>
<span id="cb6-68">    .with_columns((pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shooter_speed_fps"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.6818</span>).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shooter_speed_mph"</span>))</span>
<span id="cb6-69">    .sort([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Clock_Seconds"</span>], descending<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>[<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">False</span>, <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>])</span>
<span id="cb6-70">)</span></code></pre></div></div>
</details>
</div>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://your-website-url.example.com/posts/feature-based-xg/shot_speeds_table.png" class="img-fluid quarto-figure quarto-figure-center figure-img"></p>
</figure>
</div>
</section>
<section id="pressure" class="level3">
<h3 class="anchored" data-anchor-id="pressure">Pressure</h3>
<p>In Section 2.4.1.3 of the thesis, pressure is defined as the distance from the shot location to the closest and second-closest defending skater. A continuous distance is more descriptive than counting defenders within 6 ft, and adding the second-closest defender captures pressure from multiple defenders.</p>
<p>To compute pressure, we remove goaltenders from the players dataset and left join the remaining defenders onto the shots dataset, computing the distance from each defender to the shot. For each shot, we then keep the two smallest distances, giving the distance from the shot location to the closest and second-closest defenders.</p>
<div id="45a80805" class="cell" data-execution_count="7">
<details class="code-fold">
<summary>Show code</summary>
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb7" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb7-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Defending skaters only: opposite tracking team, real jersey numbers, no goalies.</span></span>
<span id="cb7-2">defenders <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (</span>
<span id="cb7-3">    players</span>
<span id="cb7-4">    .<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">filter</span>(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Jersey"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">!=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Go"</span>)</span>
<span id="cb7-5">    .select([</span>
<span id="cb7-6">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Team"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>,</span>
<span id="cb7-7">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location X (Feet)"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location Y (Feet)"</span>,</span>
<span id="cb7-8">    ])</span>
<span id="cb7-9">)</span>
<span id="cb7-10"></span>
<span id="cb7-11"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Per shot: the defending team, the release frame, and the shot location in tracking</span></span>
<span id="cb7-12"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># coordinates (negated when the release match used the flip).</span></span>
<span id="cb7-13">shot_frames <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (</span>
<span id="cb7-14">    shots</span>
<span id="cb7-15">    .join(release.select([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"used_flip"</span>]), on<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, how<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"left"</span>)</span>
<span id="cb7-16">    .with_columns(</span>
<span id="cb7-17">        pl.when(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Track_Team"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Home"</span>).then(pl.lit(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Away"</span>))</span>
<span id="cb7-18">          .otherwise(pl.lit(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Home"</span>)).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Def_Team"</span>),</span>
<span id="cb7-19">        pl.when(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"used_flip"</span>)).then(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"X_Coordinate"</span>))</span>
<span id="cb7-20">          .otherwise(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"X_Coordinate"</span>)).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot_X"</span>),</span>
<span id="cb7-21">        pl.when(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"used_flip"</span>)).then(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Y_Coordinate"</span>))</span>
<span id="cb7-22">          .otherwise(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Y_Coordinate"</span>)).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot_Y"</span>),</span>
<span id="cb7-23">    )</span>
<span id="cb7-24">    .select([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Def_Team"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot_X"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot_Y"</span>])</span>
<span id="cb7-25">)</span>
<span id="cb7-26"></span>
<span id="cb7-27"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Join each shot to every defending skater in its release frame; measure distance.</span></span>
<span id="cb7-28">pressure <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (</span>
<span id="cb7-29">    shot_frames</span>
<span id="cb7-30">    .join(</span>
<span id="cb7-31">        defenders,</span>
<span id="cb7-32">        left_on<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Def_Team"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>],</span>
<span id="cb7-33">        right_on<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Team"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>],</span>
<span id="cb7-34">        how<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"left"</span>,</span>
<span id="cb7-35">    )</span>
<span id="cb7-36">    .with_columns(</span>
<span id="cb7-37">        ((pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location X (Feet)"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot_X"</span>)) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span></span>
<span id="cb7-38">         <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location Y (Feet)"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot_Y"</span>)) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>)</span>
<span id="cb7-39">        .sqrt().alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"def_dist"</span>)</span>
<span id="cb7-40">    )</span>
<span id="cb7-41">    .group_by(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, maintain_order<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>)</span>
<span id="cb7-42">    .agg(</span>
<span id="cb7-43">        pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"def_dist"</span>).sort(nulls_last<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"def_dists"</span>),</span>
<span id="cb7-44">        pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"def_dist"</span>).is_not_null().<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">sum</span>().alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"n_defenders"</span>),</span>
<span id="cb7-45">    )</span>
<span id="cb7-46">    .with_columns(</span>
<span id="cb7-47">        pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"def_dists"</span>).<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">list</span>.get(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, null_on_oob<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"closest_def_dist"</span>),</span>
<span id="cb7-48">        pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"def_dists"</span>).<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">list</span>.get(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, null_on_oob<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"second_closest_def_dist"</span>),</span>
<span id="cb7-49">    )</span>
<span id="cb7-50">    .drop(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"def_dists"</span>)</span>
<span id="cb7-51">)</span>
<span id="cb7-52"></span>
<span id="cb7-53"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Attach to the shots table for use as expected-goals model features.</span></span>
<span id="cb7-54">shot_pressure <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> shots.join(pressure, on<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, how<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"left"</span>).select(</span>
<span id="cb7-55">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Clock"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Team"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Player_Id"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Event"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Detail_1"</span>,</span>
<span id="cb7-56">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"n_defenders"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"closest_def_dist"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"second_closest_def_dist"</span>,</span>
<span id="cb7-57">)</span></code></pre></div></div>
</details>
</div>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://your-website-url.example.com/posts/feature-based-xg/pressure_table.png" class="img-fluid quarto-figure quarto-figure-center figure-img"></p>
</figure>
</div>
</section>
<section id="pre-shot-movement" class="level3">
<h3 class="anchored" data-anchor-id="pre-shot-movement">Pre-Shot Movement</h3>
<p>Section 2.4.1.4 of the thesis discusses pre-shot movement: how the puck moved in the seconds leading up to the shot. It is a key predictor because lateral puck movement forces the goaltender to move side to side, which can leave them out of position when the shot is released.</p>
<p>This section focuses on the meridian line, which is visualized below:</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://your-website-url.example.com/posts/feature-based-xg/rink_coords_meridian.png" class="img-fluid quarto-figure quarto-figure-center figure-img" style="width:85.0%"></p>
</figure>
</div>
<p>Pre-shot movement features are focused on lateral puck movement (east-west):</p>
<ol type="1">
<li>Time since the puck last crossed the meridian: Seconds since the puck last crossed the meridian.</li>
<li>Location where the puck crossed the meridian: Measured as the distance to the goal line</li>
<li>Average puck speed: Average puck speed in the 1-second before the shot.</li>
</ol>
<p>Features 1 and 2 are only defined when the crossing happened within 5-seconds before the shot.</p>
<p>To compute the pre-shot movement features, we first filter for the puck in the tracking dataset. Then, we take the change in <code>x</code> and <code>y</code> across the <code>SMOOTH_K</code>-frame central-difference window and divide the resulting Euclidean displacement by the elapsed time, converting frames to seconds with the <code>FPS = 30</code> factor introduced above. We call this new dataset “puck”.</p>
<p>Then, we left-join this puck dataset onto the shots dataset and keep only the puck frames within 5 seconds (150 frames) before each shot. The resulting dataset is called the “window” dataset. In the window dataset, a meridian crossing shows up as a sign change in the puck’s y-position between consecutive frames (the puck passed through the y=0 line), so we keep those rows to locate the most recent crossing before the shot. With the resulting rows, we get the first two features: time since meridian crossing and the location where the puck crossed the meridian.</p>
<p>To compute the average puck speed, we filter for rows in the window dataset where there is a 1-second (30 frames) difference in the frame when the shot was taken and the puck tracking frame. Then, we get the average of all the puck speeds recorded for a single shot id to find the average puck speed 1-second before the shot. The resulting average puck speed captures how fast the puck was moving into the shot.</p>
<div id="0a1b84a8" class="cell" data-execution_count="8">
<details class="code-fold">
<summary>Show code</summary>
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb8" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb8-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># The puck moves far faster than skaters, so the 30 mph skater cap doesn't apply; we</span></span>
<span id="cb8-2"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># only drop physically impossible jumps (tracking glitches) above ~110 mph.</span></span>
<span id="cb8-3">MAX_PUCK_SPEED_MPH <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">110.0</span></span>
<span id="cb8-4">MAX_PUCK_SPEED_FPS <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> MAX_PUCK_SPEED_MPH <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> FT_PER_S_TO_MPH</span>
<span id="cb8-5"></span>
<span id="cb8-6">puck <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (</span>
<span id="cb8-7">    tracking</span>
<span id="cb8-8">    .<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">filter</span>(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Player or Puck"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Puck"</span>)</span>
<span id="cb8-9">    .<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">filter</span>(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location X (Feet)"</span>).is_not_null())</span>
<span id="cb8-10">    .with_columns(</span>
<span id="cb8-11">        pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Image Id"</span>).<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">str</span>.split(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"_"</span>).<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">list</span>.last().cast(pl.Int64).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>)</span>
<span id="cb8-12">    )</span>
<span id="cb8-13">    .sort([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>])</span>
<span id="cb8-14">    .with_columns(</span>
<span id="cb8-15">        (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location X (Feet)"</span>).shift(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>SMOOTH_K) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location X (Feet)"</span>).shift(SMOOTH_K)).over([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>]).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"dx"</span>),</span>
<span id="cb8-16">        (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location Y (Feet)"</span>).shift(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>SMOOTH_K) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location Y (Feet)"</span>).shift(SMOOTH_K)).over([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>]).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"dy"</span>),</span>
<span id="cb8-17">        (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>).shift(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>SMOOTH_K) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>).shift(SMOOTH_K)).over([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>]).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"dframe"</span>),</span>
<span id="cb8-18">    )</span>
<span id="cb8-19">    .with_columns(</span>
<span id="cb8-20">        ((pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"dx"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"dy"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>).sqrt() <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"dframe"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> FPS)).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"puck_speed_fps"</span>)</span>
<span id="cb8-21">    )</span>
<span id="cb8-22">    .with_columns(</span>
<span id="cb8-23">        pl.when(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"puck_speed_fps"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;=</span> MAX_PUCK_SPEED_FPS).then(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"puck_speed_fps"</span>)).otherwise(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"puck_speed_fps"</span>)</span>
<span id="cb8-24">    )</span>
<span id="cb8-25">    .select([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location X (Feet)"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location Y (Feet)"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"puck_speed_fps"</span>])</span>
<span id="cb8-26">    .rename({<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location X (Feet)"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"puck_x"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location Y (Feet)"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"puck_y"</span>})</span>
<span id="cb8-27">)</span>
<span id="cb8-28"></span>
<span id="cb8-29"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Per-shot orientation: the shot location in tracking coords (negated when the release</span></span>
<span id="cb8-30"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># match used the flip) tells us which goal line the team attacks (+89 if X &gt; 0).</span></span>
<span id="cb8-31">shot_orient <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (</span>
<span id="cb8-32">    shots.join(release.select([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"used_flip"</span>]), on<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, how<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"left"</span>)</span>
<span id="cb8-33">    .with_columns(</span>
<span id="cb8-34">        pl.when(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"used_flip"</span>)).then(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"X_Coordinate"</span>)).otherwise(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"X_Coordinate"</span>)).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot_X_track"</span>)</span>
<span id="cb8-35">    )</span>
<span id="cb8-36">    .with_columns((pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot_X_track"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"attack_right"</span>))</span>
<span id="cb8-37">    .select([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"attack_right"</span>])</span>
<span id="cb8-38">    .rename({<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_frame"</span>})</span>
<span id="cb8-39">)</span>
<span id="cb8-40"></span>
<span id="cb8-41"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Puck samples in the same period within 5 s (150 frames) before each shot, with</span></span>
<span id="cb8-42"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># coordinates normalized to attack-right. (The meridian sign-change is invariant to the</span></span>
<span id="cb8-43"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># flip; normalizing X only matters for the goal-line distance.)</span></span>
<span id="cb8-44">window <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (</span>
<span id="cb8-45">    shot_orient.join(puck, on<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>], how<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"left"</span>)</span>
<span id="cb8-46">    .<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">filter</span>((pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_frame"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>)).is_between(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> FPS))</span>
<span id="cb8-47">    .with_columns(</span>
<span id="cb8-48">        pl.when(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"attack_right"</span>)).then(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"puck_x"</span>)).otherwise(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"puck_x"</span>)).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"px"</span>),</span>
<span id="cb8-49">        pl.when(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"attack_right"</span>)).then(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"puck_y"</span>)).otherwise(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"puck_y"</span>)).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"py"</span>),</span>
<span id="cb8-50">    )</span>
<span id="cb8-51">    .sort([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>])</span>
<span id="cb8-52">)</span>
<span id="cb8-53"></span>
<span id="cb8-54"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Feature 3: average puck speed over the 1 s (30 frames) before the shot.</span></span>
<span id="cb8-55">avg_speed <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (</span>
<span id="cb8-56">    window.<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">filter</span>((pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_frame"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>)).is_between(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> FPS))</span>
<span id="cb8-57">    .group_by(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>)</span>
<span id="cb8-58">    .agg(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"puck_speed_fps"</span>).mean().alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"avg_puck_speed_fps"</span>))</span>
<span id="cb8-59">    .with_columns((pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"avg_puck_speed_fps"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> FT_PER_S_TO_MPH).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"avg_puck_speed_mph"</span>))</span>
<span id="cb8-60">)</span>
<span id="cb8-61"></span>
<span id="cb8-62"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Features 1 &amp; 2: the most recent meridian crossing within the 5 s window. A crossing is</span></span>
<span id="cb8-63"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># a sign change in py between consecutive frames; we linearly interpolate the X where</span></span>
<span id="cb8-64"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># py = 0, then keep the latest crossing (largest Frame) before the shot.</span></span>
<span id="cb8-65">crossings <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (</span>
<span id="cb8-66">    window.with_columns(</span>
<span id="cb8-67">        pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"px"</span>).shift(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>).over(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"px_prev"</span>),</span>
<span id="cb8-68">        pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"py"</span>).shift(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>).over(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"py_prev"</span>),</span>
<span id="cb8-69">    )</span>
<span id="cb8-70">    .<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">filter</span>(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"py_prev"</span>).is_not_null() <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span> (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"py_prev"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"py"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>))</span>
<span id="cb8-71">    .with_columns(</span>
<span id="cb8-72">        (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"px_prev"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"px"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"px_prev"</span>)) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> (<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"py_prev"</span>)) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"py"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"py_prev"</span>))).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"x_cross"</span>)</span>
<span id="cb8-73">    )</span>
<span id="cb8-74">    .sort([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>])</span>
<span id="cb8-75">    .group_by(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, maintain_order<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>)</span>
<span id="cb8-76">    .last()</span>
<span id="cb8-77">    .with_columns(</span>
<span id="cb8-78">        ((pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_frame"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>)) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> FPS).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"time_since_meridian"</span>),</span>
<span id="cb8-79">        (<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">89</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"x_cross"</span>)).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"meridian_cross_dist"</span>),  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># negative if crossed behind the net</span></span>
<span id="cb8-80">    )</span>
<span id="cb8-81">    .select([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"time_since_meridian"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"meridian_cross_dist"</span>])</span>
<span id="cb8-82">)</span>
<span id="cb8-83"></span>
<span id="cb8-84"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Attach to the shots table; nulls mark shots with no meridian crossing in the prior 5 s.</span></span>
<span id="cb8-85">pre_shot_movement <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (</span>
<span id="cb8-86">    shots.join(crossings, on<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, how<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"left"</span>)</span>
<span id="cb8-87">    .join(avg_speed, on<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, how<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"left"</span>)</span>
<span id="cb8-88">    .sort([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Clock_Seconds"</span>], descending<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>[<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">False</span>, <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>])</span>
<span id="cb8-89">    .select(</span>
<span id="cb8-90">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Clock"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Team"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Player_Id"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Event"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Detail_1"</span>,</span>
<span id="cb8-91">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"time_since_meridian"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"meridian_cross_dist"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"avg_puck_speed_fps"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"avg_puck_speed_mph"</span>,</span>
<span id="cb8-92">    )</span>
<span id="cb8-93">)</span></code></pre></div></div>
</details>
</div>
<p><img src="https://your-website-url.example.com/posts/feature-based-xg/pre_shot_movement_table.png" class="img-fluid"></p>
</section>
<section id="traffic" class="level3">
<h3 class="anchored" data-anchor-id="traffic">Traffic</h3>
<p>Section 2.4.1.5 of the thesis models traffic with goal-face occlusion. For each skater on the ice, draw a straight line from the shooter through that skater and extend it to the goal line. Where that line crosses the goal line is, in effect, the “shadow” that skater casts onto the net from the shooter’s viewpoint. Center a Gaussian (normal distribution) at that shadow point, with a standard deviation of 1.5 ft (a tunable parameter). Then integrate that Gaussian over the interval from −3 ft to +3 ft, which is the 6-ft-wide opening of the goal. The end result is goal-face occlusion.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://your-website-url.example.com/posts/feature-based-xg/occlusion_density.png" class="img-fluid quarto-figure quarto-figure-center figure-img" style="width:55.0%"></p>
</figure>
</div>
<p>To calculate occlusion, we take the player tracking data with goaltenders removed and left-join it onto the shots dataset, pairing each shot with every skater on the ice at its release frame, then drop the shooter row. For each remaining skater, we use the linear interpolation equation below to find where the line from the shooter through that skater crosses the goal line, i.e.&nbsp;its y-intercept on the goal line.</p>
<p><img src="https://latex.codecogs.com/png.latex?%0Ay_%7B%5Ctext%7Bgoal%20line%7D%7D=%20%5Ctext%7BShotY%7D%20+%20(%5Ctext%7BSkaterY%7D%20-%20%5Ctext%7BShotY%7D)%20%5Ccdot%20%5Cfrac%7B%5Ctext%7BGoalX%7D%20-%20%5Ctext%7BShotX%7D%7D%7B%5Ctext%7BSkaterX%7D%20-%20%5Ctext%7BShotX%7D%7D%0A"></p>
<p>Then, we find the total occlusion density between the two goal posts using the formula below to compute the probability mass of a normal bell curve:</p>
<p><img src="https://latex.codecogs.com/png.latex?%0A%5Ctext%7BOcclusion%20Density%7D%20=%20%5CPhi%5Cleft(%5Cfrac%7B%5Ctext%7BGOALPOST%7D%20-%20y_%7B%5Ctext%7Bgoal%7D%7D%7D%7B%5Csigma%7D%5Cright)%20-%20%5CPhi%5Cleft(%5Cfrac%7B-%5Ctext%7BGOALPOST%7D%20-%20y_%7B%5Ctext%7Bgoal%7D%7D%7D%7B%5Csigma%7D%5Cright)%0A"></p>
<div id="c69ec97d" class="cell" data-execution_count="9">
<details class="code-fold">
<summary>Show code</summary>
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb9" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb9-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> numpy <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> np</span>
<span id="cb9-2"></span>
<span id="cb9-3">SIGMA <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1.5</span>  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Gaussian standard deviation (feet)</span></span>
<span id="cb9-4">GOAL_HALF <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">3.0</span>  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># half-width of goal face (feet)</span></span>
<span id="cb9-5"></span>
<span id="cb9-6"></span>
<span id="cb9-7"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> _normal_cdf_batched(x: np.ndarray) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> np.ndarray:</span>
<span id="cb9-8">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">"""Vectorised normal CDF using math.erf identity: Φ(z) = 0.5*(1+erf(z/√2))."""</span></span>
<span id="cb9-9">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.5</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> (<span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1.0</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> np.vectorize(math.erf)(x <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> math.sqrt(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>)))</span>
<span id="cb9-10"></span>
<span id="cb9-11"></span>
<span id="cb9-12"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># --- Per-shot info in tracking coordinates (reuses release match) ---</span></span>
<span id="cb9-13">shot_info <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (</span>
<span id="cb9-14">    shots.join(release.select([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"used_flip"</span>]), on<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, how<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"left"</span>)</span>
<span id="cb9-15">    .with_columns(</span>
<span id="cb9-16">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Shot location in tracking coords</span></span>
<span id="cb9-17">        pl.when(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"used_flip"</span>)).then(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"X_Coordinate"</span>)).otherwise(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"X_Coordinate"</span>)).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot_X"</span>),</span>
<span id="cb9-18">        pl.when(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"used_flip"</span>)).then(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Y_Coordinate"</span>)).otherwise(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Y_Coordinate"</span>)).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot_Y"</span>),</span>
<span id="cb9-19">    )</span>
<span id="cb9-20">    .with_columns(</span>
<span id="cb9-21">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Goal line: +89 if attacking right, −89 if attacking left</span></span>
<span id="cb9-22">        pl.when(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot_X"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>).then(pl.lit(<span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">89.0</span>)).otherwise(pl.lit(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">89.0</span>)).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"goal_x"</span>),</span>
<span id="cb9-23">    )</span>
<span id="cb9-24">    .select([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Track_Team"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Player_Id"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot_X"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot_Y"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"goal_x"</span>])</span>
<span id="cb9-25">)</span>
<span id="cb9-26"></span>
<span id="cb9-27"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># --- All non-goalie skaters at each shot's release frame ---</span></span>
<span id="cb9-28">skaters_at_frame <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (</span>
<span id="cb9-29">    players</span>
<span id="cb9-30">    .<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">filter</span>(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Jersey"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">!=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Go"</span>)</span>
<span id="cb9-31">    .select([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Team"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Jersey"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>,</span>
<span id="cb9-32">             <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location X (Feet)"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location Y (Feet)"</span>])</span>
<span id="cb9-33">    .rename({<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location X (Feet)"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"sk_x"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location Y (Feet)"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"sk_y"</span>})</span>
<span id="cb9-34">)</span>
<span id="cb9-35"></span>
<span id="cb9-36"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Join each shot with every skater present at the release frame, then drop the shooter.</span></span>
<span id="cb9-37">occlusion_pairs <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (</span>
<span id="cb9-38">    shot_info.join(</span>
<span id="cb9-39">        skaters_at_frame,</span>
<span id="cb9-40">        left_on<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>],</span>
<span id="cb9-41">        right_on<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>],</span>
<span id="cb9-42">        how<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"left"</span>,</span>
<span id="cb9-43">    )</span>
<span id="cb9-44">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Exclude the shooter (same team in tracking namespace + same jersey)</span></span>
<span id="cb9-45">    .<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">filter</span>(</span>
<span id="cb9-46">        <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">~</span>((pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Track_Team"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Team"</span>)) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span> (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Player_Id"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Jersey"</span>)))</span>
<span id="cb9-47">    )</span>
<span id="cb9-48">    .<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">filter</span>(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"sk_x"</span>).is_not_null() <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"sk_y"</span>).is_not_null())</span>
<span id="cb9-49">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Compute y-intercept on the goal line</span></span>
<span id="cb9-50">    .with_columns(</span>
<span id="cb9-51">        (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot_Y"</span>)</span>
<span id="cb9-52">         <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"sk_y"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot_Y"</span>))</span>
<span id="cb9-53">           <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"goal_x"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot_X"</span>))</span>
<span id="cb9-54">           <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"sk_x"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot_X"</span>))</span>
<span id="cb9-55">        ).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"y_goal"</span>)</span>
<span id="cb9-56">    )</span>
<span id="cb9-57">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Drop degenerate cases: skater at same x as shooter, or intercept is undefined</span></span>
<span id="cb9-58">    .<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">filter</span>(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"y_goal"</span>).is_not_null() <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"y_goal"</span>).is_finite())</span>
<span id="cb9-59">)</span>
<span id="cb9-60"></span>
<span id="cb9-61"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Compute Gaussian contribution per skater via map_batches</span></span>
<span id="cb9-62">occlusion_pairs <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> occlusion_pairs.with_columns(</span>
<span id="cb9-63">    pl.struct([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"y_goal"</span>]).map_batches(</span>
<span id="cb9-64">        <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">lambda</span> s: pl.Series(</span>
<span id="cb9-65">            _normal_cdf_batched((GOAL_HALF <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> s.struct.field(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"y_goal"</span>).to_numpy()) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> SIGMA)</span>
<span id="cb9-66">            <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> _normal_cdf_batched((<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>GOAL_HALF <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> s.struct.field(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"y_goal"</span>).to_numpy()) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> SIGMA)</span>
<span id="cb9-67">        ),</span>
<span id="cb9-68">        return_dtype<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>pl.Float64,</span>
<span id="cb9-69">    ).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"occlusion_contribution"</span>)</span>
<span id="cb9-70">)</span>
<span id="cb9-71"></span>
<span id="cb9-72"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Sum per shot</span></span>
<span id="cb9-73">total_occlusion <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (</span>
<span id="cb9-74">    occlusion_pairs</span>
<span id="cb9-75">    .group_by(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, maintain_order<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>)</span>
<span id="cb9-76">    .agg(</span>
<span id="cb9-77">        (<span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1.0</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> (<span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1.0</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"occlusion_contribution"</span>)).product())</span>
<span id="cb9-78">        .alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"total_occlusion"</span>)</span>
<span id="cb9-79">    )</span>
<span id="cb9-80">)</span>
<span id="cb9-81"></span>
<span id="cb9-82">shot_occlusion <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (</span>
<span id="cb9-83">    shots.join(total_occlusion, on<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, how<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"left"</span>)</span>
<span id="cb9-84">    .select(</span>
<span id="cb9-85">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Clock"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Team"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Player_Id"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Event"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Detail_1"</span>,</span>
<span id="cb9-86">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"X_Coordinate"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Y_Coordinate"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"total_occlusion"</span>,</span>
<span id="cb9-87">    )</span>
<span id="cb9-88">)</span></code></pre></div></div>
</details>
</div>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://your-website-url.example.com/posts/feature-based-xg/shot_occlusion_table.png" class="img-fluid quarto-figure quarto-figure-center figure-img"></p>
</figure>
</div>
</section>
<section id="goaltender-positioning" class="level3">
<h3 class="anchored" data-anchor-id="goaltender-positioning">Goaltender Positioning</h3>
<p>Section 2.4.1.6 of the thesis deals with features related to goaltender positioning, goaltender angle and depth relative to the goal center. Figure 2.9 illustrates goaltender angle <img src="https://latex.codecogs.com/png.latex?%5Czeta"> and depth <img src="https://latex.codecogs.com/png.latex?d_g">:</p>
<p><img src="https://your-website-url.example.com/posts/feature-based-xg/goaltender-positioning.png" class="img-fluid"></p>
<p>The figure shows that an angle near 0° means the goalie is squared up to the shooter, while a larger angle means they are off-angle, leaving more of the net exposed. The depth, meanwhile, captures how far the goalie has ventured out of the crease.</p>
<p>To compute these two features, we filter the player tracking data for goaltenders and left-join it onto the shots dataset. For each shot, we form two vectors from the goal center — (89, 0) or (−89, 0) — one pointing to the shooter and the other to the goaltender. The angle between these vectors gives the goaltender angle, and the length of the goaltender vector gives the goaltender depth.</p>
<div id="c0f7a7f3" class="cell" data-execution_count="10">
<details class="code-fold">
<summary>Show code</summary>
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb10" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb10-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Per shot: defending team, release frame, shot location in tracking coords, goal center.</span></span>
<span id="cb10-2">shot_goalie_ctx <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (</span>
<span id="cb10-3">    shots.join(release.select([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"used_flip"</span>]), on<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, how<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"left"</span>)</span>
<span id="cb10-4">    .with_columns(</span>
<span id="cb10-5">        pl.when(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Track_Team"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Home"</span>).then(pl.lit(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Away"</span>))</span>
<span id="cb10-6">          .otherwise(pl.lit(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Home"</span>)).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Def_Team"</span>),</span>
<span id="cb10-7">        pl.when(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"used_flip"</span>)).then(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"X_Coordinate"</span>))</span>
<span id="cb10-8">          .otherwise(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"X_Coordinate"</span>)).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot_X"</span>),</span>
<span id="cb10-9">        pl.when(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"used_flip"</span>)).then(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Y_Coordinate"</span>))</span>
<span id="cb10-10">          .otherwise(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Y_Coordinate"</span>)).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot_Y"</span>),</span>
<span id="cb10-11">    )</span>
<span id="cb10-12">    .with_columns(</span>
<span id="cb10-13">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Goal line: +89 if attacking right, −89 if attacking left.</span></span>
<span id="cb10-14">        pl.when(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot_X"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>).then(pl.lit(<span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">89.0</span>)).otherwise(pl.lit(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">89.0</span>)).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"goal_x"</span>),</span>
<span id="cb10-15">    )</span>
<span id="cb10-16">    .select([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Def_Team"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot_X"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot_Y"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"goal_x"</span>])</span>
<span id="cb10-17">)</span>
<span id="cb10-18"></span>
<span id="cb10-19"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Defending goaltender's tracked position at each shot's release frame.</span></span>
<span id="cb10-20">goalies <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (</span>
<span id="cb10-21">    players</span>
<span id="cb10-22">    .<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">filter</span>(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Jersey"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Go"</span>)</span>
<span id="cb10-23">    .select([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Team"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>,</span>
<span id="cb10-24">             <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location X (Feet)"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location Y (Feet)"</span>])</span>
<span id="cb10-25">    .rename({<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location X (Feet)"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"goalie_x"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rink Location Y (Feet)"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"goalie_y"</span>})</span>
<span id="cb10-26">)</span>
<span id="cb10-27"></span>
<span id="cb10-28">goaltender_positioning <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (</span>
<span id="cb10-29">    shot_goalie_ctx</span>
<span id="cb10-30">    .join(</span>
<span id="cb10-31">        goalies,</span>
<span id="cb10-32">        left_on<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Def_Team"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>],</span>
<span id="cb10-33">        right_on<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Team"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Frame"</span>],</span>
<span id="cb10-34">        how<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"left"</span>,</span>
<span id="cb10-35">    )</span>
<span id="cb10-36">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># One goalie per team-frame is expected; guard against duplicate tracks.</span></span>
<span id="cb10-37">    .group_by(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, maintain_order<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>)</span>
<span id="cb10-38">    .first()</span>
<span id="cb10-39">    .with_columns(</span>
<span id="cb10-40">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Vectors from the goal center to the shooter and to the goalie.</span></span>
<span id="cb10-41">        (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot_X"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"goal_x"</span>)).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"s_x"</span>),</span>
<span id="cb10-42">        pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot_Y"</span>).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"s_y"</span>),</span>
<span id="cb10-43">        (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"goalie_x"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"goal_x"</span>)).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"g_x"</span>),</span>
<span id="cb10-44">        pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"goalie_y"</span>).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"g_y"</span>),</span>
<span id="cb10-45">    )</span>
<span id="cb10-46">    .with_columns(</span>
<span id="cb10-47">        (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"s_x"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"s_y"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>).sqrt().alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"s_mag"</span>),</span>
<span id="cb10-48">        (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"g_x"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"g_y"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>).sqrt().alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"g_mag"</span>),</span>
<span id="cb10-49">        (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"s_x"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"g_x"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"s_y"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"g_y"</span>)).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"dot"</span>),</span>
<span id="cb10-50">    )</span>
<span id="cb10-51">    .with_columns(</span>
<span id="cb10-52">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># d_g: distance from the goaltender to the goal center.</span></span>
<span id="cb10-53">        pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"g_mag"</span>).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"goaltender_depth"</span>),</span>
<span id="cb10-54">        <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># ζ (degrees): undefined when shooter or goalie sits exactly at the goal center.</span></span>
<span id="cb10-55">        pl.when((pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"s_mag"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&amp;</span> (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"g_mag"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>))</span>
<span id="cb10-56">          .then(</span>
<span id="cb10-57">              (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"dot"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> (pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"s_mag"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"g_mag"</span>)))</span>
<span id="cb10-58">              .clip(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1.0</span>, <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">1.0</span>).arccos() <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> (<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">180</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> math.pi)</span>
<span id="cb10-59">          )</span>
<span id="cb10-60">          .otherwise(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>)</span>
<span id="cb10-61">          .alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"angle_discrepancy"</span>),</span>
<span id="cb10-62">    )</span>
<span id="cb10-63">    .select([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"angle_discrepancy"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"goaltender_depth"</span>])</span>
<span id="cb10-64">)</span>
<span id="cb10-65"></span>
<span id="cb10-66"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Attach to the shots table for use as expected-goals model features.</span></span>
<span id="cb10-67">shot_goaltender <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (</span>
<span id="cb10-68">    shots.join(goaltender_positioning, on<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, how<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"left"</span>)</span>
<span id="cb10-69">    .sort([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Clock_Seconds"</span>], descending<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>[<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">False</span>, <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>])</span>
<span id="cb10-70">    .select(</span>
<span id="cb10-71">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Clock"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Team"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Player_Id"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Event"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Detail_1"</span>,</span>
<span id="cb10-72">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"angle_discrepancy"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"goaltender_depth"</span>,</span>
<span id="cb10-73">    )</span>
<span id="cb10-74">)</span></code></pre></div></div>
</details>
</div>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://your-website-url.example.com/posts/feature-based-xg/shot_goaltender_table.png" class="img-fluid quarto-figure quarto-figure-center figure-img"></p>
</figure>
</div>
</section>
</section>
<section id="feature-based-expected-goals-xg-model" class="level2">
<h2 class="anchored" data-anchor-id="feature-based-expected-goals-xg-model">Feature-Based Expected Goals (xG) Model</h2>
<p>With feature engineering complete, we join all of the features above on <code>shot_id</code> to assemble a single modeling dataset. The result spans 10 games and holds 1,190 shots across 21 columns, 58 of which are goals.</p>
<div id="521bc174" class="cell" data-execution_count="11">
<details class="code-fold">
<summary>Show code</summary>
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb11" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb11-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Shot Location was computed display-only above, so its features are recomputed here</span></span>
<span id="cb11-2"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># keyed by shot_id (same logic: normalize to the right side, then angle/distances).</span></span>
<span id="cb11-3">shot_location <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (</span>
<span id="cb11-4">    shots</span>
<span id="cb11-5">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Normalize to right side</span></span>
<span id="cb11-6">    .with_columns(</span>
<span id="cb11-7">        pl.when(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"X_Coordinate"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>)</span>
<span id="cb11-8">        .then(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"X_Coordinate"</span>))</span>
<span id="cb11-9">        .otherwise(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"X_Coordinate"</span>))</span>
<span id="cb11-10">        .alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"X_Coordinate"</span>),</span>
<span id="cb11-11"></span>
<span id="cb11-12">        pl.when(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"X_Coordinate"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>)</span>
<span id="cb11-13">        .then(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span>pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Y_Coordinate"</span>))</span>
<span id="cb11-14">        .otherwise(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Y_Coordinate"</span>))</span>
<span id="cb11-15">        .alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Y_Coordinate"</span>),</span>
<span id="cb11-16">    )</span>
<span id="cb11-17">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Shot angle (degrees), meridian distance, and distance to goal</span></span>
<span id="cb11-18">    .with_columns(</span>
<span id="cb11-19">        (</span>
<span id="cb11-20">            pl.arctan2(pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Y_Coordinate"</span>), <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">89</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"X_Coordinate"</span>))</span>
<span id="cb11-21">            .<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">abs</span>()</span>
<span id="cb11-22">            <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> (<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">180</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> math.pi)</span>
<span id="cb11-23">        ).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot_Angle"</span>),</span>
<span id="cb11-24">        pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Y_Coordinate"</span>).<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">abs</span>().alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"d_m"</span>),</span>
<span id="cb11-25">    )</span>
<span id="cb11-26">    .with_columns(</span>
<span id="cb11-27">        ((<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">89</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"X_Coordinate"</span>)).<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">pow</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Y_Coordinate"</span>).<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">pow</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>))</span>
<span id="cb11-28">        .sqrt()</span>
<span id="cb11-29">        .alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_distance"</span>),</span>
<span id="cb11-30">    )</span>
<span id="cb11-31">    .select(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot_Angle"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"d_m"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_distance"</span>)</span>
<span id="cb11-32">)</span>
<span id="cb11-33"></span>
<span id="cb11-34"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Unify all expected-goals features into one per-shot dataset by joining on shot_id.</span></span>
<span id="cb11-35">shot_features <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (</span>
<span id="cb11-36">    shots.select(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Clock"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Clock_Seconds"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Team"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Player_Id"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Event"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Detail_1"</span>)</span>
<span id="cb11-37">    .with_columns((pl.col(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Event"</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Goal"</span>).cast(pl.Int8).alias(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"is_goal"</span>))</span>
<span id="cb11-38">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Shot Location</span></span>
<span id="cb11-39">    .join(shot_location, on<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, how<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"left"</span>)</span>
<span id="cb11-40">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Shooter Motion</span></span>
<span id="cb11-41">    .join(</span>
<span id="cb11-42">        shot_speeds.select(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shooter_speed_fps"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shooter_speed_mph"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"match_quality"</span>),</span>
<span id="cb11-43">        on<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, how<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"left"</span>,</span>
<span id="cb11-44">    )</span>
<span id="cb11-45">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Pressure</span></span>
<span id="cb11-46">    .join(pressure, on<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, how<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"left"</span>)</span>
<span id="cb11-47">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Pre-Shot Movement</span></span>
<span id="cb11-48">    .join(crossings, on<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, how<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"left"</span>)</span>
<span id="cb11-49">    .join(avg_speed, on<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, how<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"left"</span>)</span>
<span id="cb11-50">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Traffic</span></span>
<span id="cb11-51">    .join(total_occlusion, on<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, how<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"left"</span>)</span>
<span id="cb11-52">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Goaltender Positioning</span></span>
<span id="cb11-53">    .join(goaltender_positioning, on<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_id"</span>, how<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"left"</span>)</span>
<span id="cb11-54">    .sort([<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Period"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Clock_Seconds"</span>], descending<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>[<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">False</span>, <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>])</span>
<span id="cb11-55">    .drop(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Clock_Seconds"</span>)</span>
<span id="cb11-56">    .drop(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"avg_puck_speed_mph"</span>)</span>
<span id="cb11-57">)</span>
<span id="cb11-58"></span>
<span id="cb11-59"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Final output:</span></span>
<span id="cb11-60"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#  A single table with one row per shot and all expected-goals features ready for modeling.</span></span>
<span id="cb11-61">shot_features</span></code></pre></div></div>
</details>
</div>
<p>The resulting dataset, with one row per shot and every feature attached, looks like this:</p>
<p><img src="https://your-website-url.example.com/posts/feature-based-xg/shot_features_preview.png" class="img-fluid"></p>
<section id="xgboost" class="level3">
<h3 class="anchored" data-anchor-id="xgboost">XGBoost</h3>
<p>Section 2.4.2 of the thesis lays out how to train an XGBoost model on the dataset. As the thesis emphasizes, the goal is not to classify each shot as a goal or non-goal, but to estimate its probability of becoming a goal. Since the output is a probability, the model is evaluated with three metrics: binary cross-entropy loss and the Brier score, which directly score probabilistic predictions, along with the area under the receiver operating characteristic curve (AUC).</p>
<p>We adopt the hyperparameters from the thesis: 125 trees, a maximum tree depth of 4, and a learning rate of 0.075. Because goals are rare, the target variable is heavily imbalanced, with only 4.9% of shots labeled as a goal, so we evaluate the model with stratified 5-fold group cross-validation, which preserves that goal rate within each fold. With 10 games in the dataset, each fold trains on the equivalent of 8 games and tests on the remaining 2 games.</p>
<div id="1c44ba05" class="cell" data-execution_count="12">
<details class="code-fold">
<summary>Show code</summary>
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb12" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb12-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> xgboost <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> xgb</span>
<span id="cb12-2"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> sklearn.model_selection <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> StratifiedGroupKFold</span>
<span id="cb12-3"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> sklearn.metrics <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> log_loss, brier_score_loss, roc_auc_score</span>
<span id="cb12-4"></span>
<span id="cb12-5">FEATURES <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [</span>
<span id="cb12-6">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Shot Location</span></span>
<span id="cb12-7">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot_Angle"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"d_m"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_distance"</span>,</span>
<span id="cb12-8">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Shooter Motion</span></span>
<span id="cb12-9">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shooter_speed_fps"</span>,</span>
<span id="cb12-10">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Pressure</span></span>
<span id="cb12-11">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"closest_def_dist"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"second_closest_def_dist"</span>,</span>
<span id="cb12-12">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Pre-Shot Movement</span></span>
<span id="cb12-13">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"time_since_meridian"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"meridian_cross_dist"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"avg_puck_speed_fps"</span>,</span>
<span id="cb12-14">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Traffic</span></span>
<span id="cb12-15">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"total_occlusion"</span>,</span>
<span id="cb12-16">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Goaltender Positioning</span></span>
<span id="cb12-17">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"angle_discrepancy"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"goaltender_depth"</span>,</span>
<span id="cb12-18">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Shot type</span></span>
<span id="cb12-19">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Detail_1"</span>,</span>
<span id="cb12-20">]</span>
<span id="cb12-21"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Excluded: identifiers (shot_id/Period/Clock/Team/Player_Id), Event (it *is* the</span></span>
<span id="cb12-22"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># target), match_quality (data-quality flag), and the *_mph speed duplicates.</span></span>
<span id="cb12-23"></span>
<span id="cb12-24">model_df <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> shot_features.to_pandas()</span>
<span id="cb12-25">X <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> model_df[FEATURES].copy()</span>
<span id="cb12-26">X[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Detail_1"</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> X[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Detail_1"</span>].astype(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"category"</span>)</span>
<span id="cb12-27">y <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> model_df[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"is_goal"</span>].to_numpy()</span>
<span id="cb12-28">groups <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> model_df[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Game"</span>].to_numpy()  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># split by game so no game leaks across folds</span></span>
<span id="cb12-29"></span>
<span id="cb12-30"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Hyperparameters from the cited paper (selected there via cross-validation):</span></span>
<span id="cb12-31"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># tree depth 4, 125 trees, learning rate 0.075.</span></span>
<span id="cb12-32">xgb_params <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">dict</span>(</span>
<span id="cb12-33">    max_depth<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>,</span>
<span id="cb12-34">    n_estimators<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">125</span>,</span>
<span id="cb12-35">    learning_rate<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.075</span>,</span>
<span id="cb12-36">    objective<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"binary:logistic"</span>,</span>
<span id="cb12-37">    eval_metric<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"logloss"</span>,</span>
<span id="cb12-38">    tree_method<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"hist"</span>,</span>
<span id="cb12-39">    enable_categorical<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>,  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># native handling of the shot-type categorical</span></span>
<span id="cb12-40">    random_state<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">42</span>,</span>
<span id="cb12-41">)</span>
<span id="cb12-42"></span>
<span id="cb12-43"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Pooled out-of-fold predictions: each fold's model predicts the shots it never saw.</span></span>
<span id="cb12-44"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Grouped by game so every test fold is made of whole games the model never trained on</span></span>
<span id="cb12-45"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># (avoids leaking game-specific signal across folds); stratified to keep each fold near</span></span>
<span id="cb12-46"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># the overall goal rate.</span></span>
<span id="cb12-47">cv <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> StratifiedGroupKFold(n_splits<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>, shuffle<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>, random_state<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">42</span>)</span>
<span id="cb12-48">oof_prob <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.full(<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(y), np.nan)</span>
<span id="cb12-49"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> train_idx, test_idx <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> cv.split(X, y, groups):</span>
<span id="cb12-50">    model <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> xgb.XGBClassifier(<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">**</span>xgb_params)</span>
<span id="cb12-51">    model.fit(X.iloc[train_idx], y[train_idx])</span>
<span id="cb12-52">    oof_prob[test_idx] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> model.predict_proba(X.iloc[test_idx])[:, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>]</span>
<span id="cb12-53"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">assert</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">not</span> np.isnan(oof_prob).<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">any</span>()</span>
<span id="cb12-54"></span>
<span id="cb12-55"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># No-skill baseline: predict the base rate for every shot.</span></span>
<span id="cb12-56">base_rate <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> y.mean()</span>
<span id="cb12-57">baseline_prob <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> np.full(<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(y), base_rate)</span>
<span id="cb12-58"></span>
<span id="cb12-59">metrics <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> pl.DataFrame({</span>
<span id="cb12-60">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Metric"</span>: [</span>
<span id="cb12-61">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Binary cross-entropy"</span>,</span>
<span id="cb12-62">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Brier score"</span>,</span>
<span id="cb12-63">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"ROC AUC"</span>,</span>
<span id="cb12-64">    ],</span>
<span id="cb12-65">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Model"</span>: [</span>
<span id="cb12-66">        log_loss(y, oof_prob),</span>
<span id="cb12-67">        brier_score_loss(y, oof_prob),</span>
<span id="cb12-68">        roc_auc_score(y, oof_prob),</span>
<span id="cb12-69">    ],</span>
<span id="cb12-70">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Baseline"</span>: [</span>
<span id="cb12-71">        log_loss(y, baseline_prob),</span>
<span id="cb12-72">        brier_score_loss(y, baseline_prob),</span>
<span id="cb12-73">        <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.5</span>,</span>
<span id="cb12-74">    ],</span>
<span id="cb12-75">})</span></code></pre></div></div>
</details>
</div>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://your-website-url.example.com/posts/feature-based-xg/model_performance.png" class="img-fluid quarto-figure quarto-figure-center figure-img"></p>
</figure>
</div>
<p>Our metrics are not as impressive as those in the thesis’s Table 2.4, largely because of the gap in sample size. The thesis works with 79,624 shots, of which 4.08% are goals, while our dataset has just 1,190 shots — roughly 67 times fewer. The difference in goals is just as stark: the thesis trains and evaluates on 3,248 goals, compared to our 58 goals.</p>
<p>We calculate absolute SHAP values, which quantify the impact of each feature on the model’s predictions.</p>
<div id="5a8e0c6a" class="cell" data-execution_count="13">
<details class="code-fold">
<summary>Show code</summary>
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb13" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb13-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> matplotlib.pyplot <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> plt</span>
<span id="cb13-2"></span>
<span id="cb13-3">FEATURE_LABELS <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> {</span>
<span id="cb13-4">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot_Angle"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot Angle"</span>,</span>
<span id="cb13-5">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"d_m"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Meridian Distance"</span>,</span>
<span id="cb13-6">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_distance"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot Distance"</span>,</span>
<span id="cb13-7">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shooter_speed_fps"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shooter Speed"</span>,</span>
<span id="cb13-8">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"closest_def_dist"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Closest Defender Dist."</span>,</span>
<span id="cb13-9">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"second_closest_def_dist"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"2nd Closest Defender Dist."</span>,</span>
<span id="cb13-10">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"time_since_meridian"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Time Since Meridian"</span>,</span>
<span id="cb13-11">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"meridian_cross_dist"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Meridian Crossing Dist."</span>,</span>
<span id="cb13-12">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"avg_puck_speed_fps"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Avg. Puck Speed (Feet/Second)"</span>,</span>
<span id="cb13-13">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"total_occlusion"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Total Occlusion"</span>,</span>
<span id="cb13-14">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"angle_discrepancy"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Angle Discrepancy"</span>,</span>
<span id="cb13-15">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"goaltender_depth"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Goaltender Depth"</span>,</span>
<span id="cb13-16">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Detail_1"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot Type"</span>,</span>
<span id="cb13-17">}</span>
<span id="cb13-18"></span>
<span id="cb13-19">shap_pd <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> shap_importance.to_pandas()</span>
<span id="cb13-20">shap_pd[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Label"</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> shap_pd[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Feature"</span>].<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">map</span>(FEATURE_LABELS)</span>
<span id="cb13-21"></span>
<span id="cb13-22">fig, ax <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> plt.subplots(figsize<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">8</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>))</span>
<span id="cb13-23">ax.barh(shap_pd[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Label"</span>], shap_pd[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Mean_Abs_SHAP"</span>])</span>
<span id="cb13-24">ax.invert_yaxis()</span>
<span id="cb13-25">ax.set_xlabel(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Mean |SHAP value|"</span>)</span>
<span id="cb13-26">ax.set_title(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Feature importance (mean absolute SHAP value)"</span>)</span>
<span id="cb13-27">plt.tight_layout()</span>
<span id="cb13-28">fig.savefig(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shap_feature_importance.png"</span>, dpi<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">300</span>)</span>
<span id="cb13-29">plt.show()</span></code></pre></div></div>
</details>
</div>
<div class="quarto-figure quarto-figure-left">
<figure class="figure">
<p><img src="https://your-website-url.example.com/posts/feature-based-xg/shap_feature_importance.png" class="img-fluid quarto-figure quarto-figure-left figure-img" style="width:80.0%"></p>
</figure>
</div>
<p>This plot closely resembles Figure 2.11 from the thesis, which shows that Shot Distance and Meridian Distance are the two most important features to the xG model. Interestingly, our plot illustrates that shot type and total occlusion are the two least important features.</p>
<p>Finally, following Figure 2.13 of the thesis, we create SHAP dependence plots, which chart each shot’s SHAP value against its underlying feature value, one panel per feature.</p>
<div id="caf1038d" class="cell" data-execution_count="14">
<details class="code-fold">
<summary>Show code</summary>
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb14" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb14-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> matplotlib.pyplot <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> plt</span>
<span id="cb14-2"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> numpy <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">as</span> np</span>
<span id="cb14-3"></span>
<span id="cb14-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Features to plot (excluding categorical Shot Type) sorted by importance</span></span>
<span id="cb14-5">plot_features <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [f <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> f <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> shap_importance[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Feature"</span>].to_list() <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> f <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">!=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Detail_1"</span>]</span>
<span id="cb14-6"></span>
<span id="cb14-7">FEATURE_LABELS_UNITS <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> {</span>
<span id="cb14-8">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shot_distance"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot Distance (ft)"</span>,</span>
<span id="cb14-9">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"d_m"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Meridian Distance (ft)"</span>,</span>
<span id="cb14-10">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"goaltender_depth"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Goaltender Depth (ft)"</span>,</span>
<span id="cb14-11">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"meridian_cross_dist"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Meridian Crossing Dist. (ft)"</span>,</span>
<span id="cb14-12">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot_Angle"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shot Angle (deg.)"</span>,</span>
<span id="cb14-13">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"avg_puck_speed_fps"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Avg. Puck Speed (ft/s)"</span>,</span>
<span id="cb14-14">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"time_since_meridian"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Time Since Meridian (s)"</span>,</span>
<span id="cb14-15">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"closest_def_dist"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Closest Defender Dist. (ft)"</span>,</span>
<span id="cb14-16">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"second_closest_def_dist"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"2nd Closest Defender Dist. (ft)"</span>,</span>
<span id="cb14-17">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shooter_speed_fps"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Shooter Speed (ft/s)"</span>,</span>
<span id="cb14-18">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"angle_discrepancy"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Angle Discrepancy (deg.)"</span>,</span>
<span id="cb14-19">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"total_occlusion"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Total Occlusion"</span>,</span>
<span id="cb14-20">}</span>
<span id="cb14-21"></span>
<span id="cb14-22"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Map feature names to their column index in X / shap_values</span></span>
<span id="cb14-23">feat_idx <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> {f: i <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> i, f <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">enumerate</span>(FEATURES)}</span>
<span id="cb14-24"></span>
<span id="cb14-25">ncols <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span></span>
<span id="cb14-26">nrows <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span></span>
<span id="cb14-27">fig, axes <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> plt.subplots(nrows, ncols, figsize<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">14</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">14</span>))</span>
<span id="cb14-28">axes_flat <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> axes.flatten()</span>
<span id="cb14-29"></span>
<span id="cb14-30"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> i, feat <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">enumerate</span>(plot_features):</span>
<span id="cb14-31">    ax <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> axes_flat[i]</span>
<span id="cb14-32">    col_idx <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> feat_idx[feat]</span>
<span id="cb14-33">    x_vals <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> X.iloc[:, col_idx].values.astype(<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">float</span>)</span>
<span id="cb14-34">    y_vals <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> shap_values[:, col_idx]</span>
<span id="cb14-35"></span>
<span id="cb14-36">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Background histogram</span></span>
<span id="cb14-37">    ax2 <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> ax.twinx()</span>
<span id="cb14-38">    ax2.hist(x_vals, bins<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">30</span>, color<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"lightgray"</span>, edgecolor<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"none"</span>, alpha<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.6</span>)</span>
<span id="cb14-39">    ax2.set_yticks([])</span>
<span id="cb14-40">    ax2.set_ylabel(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">""</span>)</span>
<span id="cb14-41"></span>
<span id="cb14-42">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># SHAP scatter (on top)</span></span>
<span id="cb14-43">    ax.scatter(x_vals, y_vals, s<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>, alpha<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.5</span>, color<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"#1f77b4"</span>, zorder<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">3</span>)</span>
<span id="cb14-44">    ax.set_xlabel(FEATURE_LABELS_UNITS.get(feat, feat))</span>
<span id="cb14-45">    ax.set_ylabel(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"SHAP Value"</span>)</span>
<span id="cb14-46">    ax.set_axisbelow(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>)</span>
<span id="cb14-47">    ax.grid(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>, alpha<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.3</span>)</span>
<span id="cb14-48"></span>
<span id="cb14-49"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Hide any unused subplots</span></span>
<span id="cb14-50"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> j <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">range</span>(<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(plot_features), <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(axes_flat)):</span>
<span id="cb14-51">    axes_flat[j].set_visible(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">False</span>)</span>
<span id="cb14-52"></span>
<span id="cb14-53">plt.tight_layout()</span>
<span id="cb14-54">fig.savefig(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"shap_dependence_grid.png"</span>, dpi<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">300</span>)</span>
<span id="cb14-55">plt.show()</span></code></pre></div></div>
</details>
</div>
<p><img src="https://your-website-url.example.com/posts/feature-based-xg/shap_dependence_grid.png" class="img-fluid"></p>
<p>A few patterns stand out from these plots: the closer the shot, the more likely it is to become a goal; the faster the shot, the more likely it is to become a goal; the farther away the defender is from the shot, the more likely it is to become a goal; and the wider the shot angle is, the less likely it is to become a goal.</p>
<p>The meridian features tell a related story: the more time that elapses between the meridian crossing and the shot, the less likely it is to score; the larger the meridian distance, the less likely it is to score; and shots whose meridian crossing happened close to the net are the most likely to score.</p>
<p>The goaltender depth panel adds an interesting pattern. When the goaltender is pinned deep in the net (depth of 2-4 ft) or has positioned far out of it (depth of 6-10 ft), the shot is less likely to score. Finally, the total occlusion panel shows that the heavier the traffic in the shooting lane, the more likely the shot finds the net. Notably, at a total occlusion of 1.0, the SHAP value is the highest.</p>
</section>
</section>
<section id="conclusion" class="level2">
<h2 class="anchored" data-anchor-id="conclusion">Conclusion</h2>
<p>In this post, we implement the Feature-Based Expected Goals (xG) Model in Section 2.4 of Jonathan Arsenault’s PhD thesis, <em>Quantitative Analysis of Hockey Using Spatiotemporal Tracking Data</em>. We use the events and tracking data from the 2026 Big Data Cup to compute features such as shot distance, shot speed, traffic, shot angle, meridian time, meridian distance, goaltender depth, and total occlusion. Then, we train an XGBoost model on the modeling dataset with the same hyperparameters as the thesis.</p>
<p>We find that our metrics fall short of the thesis’s due to the dataset size difference, but it still provides valuable insights into factors influencing goal likelihood. Shot distance and meridian distance emerge as the two most important features, and the closer and faster the shot, the more likely it is to become a goal.</p>
<p>Future work will be implementations of later sections of Arsenault’s thesis, such as Section 2.5’s Graph-Based Approach, in which he trains neural networks to estimate goal likelihood.</p>
<p>A Jupyter notebook of the code is available <a href="https://github.com/howardbaik/player-tracking-data/blob/main/feature-based-xg/feature-based-expected-goals.ipynb">here</a>.</p>


</section>

 ]]></description>
  <guid>https://your-website-url.example.com/posts/feature-based-xg/</guid>
  <pubDate>Thu, 18 Jun 2026 04:00:00 GMT</pubDate>
  <media:content url="https://your-website-url.example.com/posts/feature-based-xg/thumbnail.png" medium="image" type="image/png" height="90" width="144"/>
</item>
</channel>
</rss>
