<?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>maydaystats</title>
<link>https://maydaystats.com/</link>
<atom:link href="https://maydaystats.com/index.xml" rel="self" type="application/rss+xml"/>
<description>A public sports analytics project covering baseball, hockey, and volleyball. Most posts are backed by a real, automated data pipeline - pulling live data into published analysis, with the code and methodology shown alongside the writing. Any post that isn&#39;t pipeline-backed says so up front.</description>
<image>
<url>https://maydaystats.com/images/og-default.png</url>
<title>maydaystats</title>
<link>https://maydaystats.com/</link>
<height>76</height>
<width>144</width>
</image>
<generator>quarto-1.6.39</generator>
<lastBuildDate>Thu, 09 Jul 2026 00:00:00 GMT</lastBuildDate>
<item>
  <title>Year in Review: The 2025-26 MLV Season</title>
  <link>https://maydaystats.com/posts/volleyball/2026-07-09-pro-year-in-review/</link>
  <description><![CDATA[ 




<p>The MLV pipeline is the odd one out on this site: every other pipeline here reads a first-party JSON API directly, but MLV (provolleyball.com’s own Major League Volleyball) only publishes per-player box scores as PDF scoresheets attached to its schedule API. This pipeline downloads each match’s PDF, extracts the text, and parses it back into structured rows, working around two different scoresheet templates the league used across the season (see the pipeline’s <code>DEPLOY.md</code> for the full story). This post is the first analysis built on the result: every completed match from January through the May championship, backfilled into BigQuery.</p>
<p>One exclusion worth flagging up front: schedule_event 636 (Team Launiere vs.&nbsp;Team Meske, March 28) is an All-Star exhibition with draft-style team names, not a real matchup between league teams. It’s filtered out of every query below, the same way the <a href="../../../posts/hockey/2026-07-08-year-in-review/index.html">hockey year in review</a> filters out Olympic break games.</p>
<section id="querying-the-season" class="level2">
<h2 class="anchored" data-anchor-id="querying-the-season">Querying the season</h2>
<div id="cell-fetch-points-leaders" class="cell" data-execution_count="1">
<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;">from</span> google.cloud <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> bigquery</span>
<span id="cb1-2"></span>
<span id="cb1-3">client <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> bigquery.Client()</span>
<span id="cb1-4"></span>
<span id="cb1-5">query <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>
<span id="cb1-6"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">SELECT</span></span>
<span id="cb1-7"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  player_name,</span></span>
<span id="cb1-8"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  team,</span></span>
<span id="cb1-9"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  SUM(points) AS points,</span></span>
<span id="cb1-10"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  SUM(kills) AS kills,</span></span>
<span id="cb1-11"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  SUM(attack_attempts) AS attempts,</span></span>
<span id="cb1-12"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  SAFE_DIVIDE(SUM(kills), SUM(attack_attempts)) AS kill_pct,</span></span>
<span id="cb1-13"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  COUNT(DISTINCT schedule_event_id) AS matches</span></span>
<span id="cb1-14"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">FROM `maydaystats.mlv_volleyball.boxscores`</span></span>
<span id="cb1-15"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">WHERE team NOT IN ('Team Launiere', 'Team Meske')</span></span>
<span id="cb1-16"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">GROUP BY player_name, team</span></span>
<span id="cb1-17"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">ORDER BY points DESC</span></span>
<span id="cb1-18"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">LIMIT 10</span></span>
<span id="cb1-19"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"""</span></span>
<span id="cb1-20"></span>
<span id="cb1-21">leaders <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> client.query(query).to_dataframe()</span>
<span id="cb1-22">leaders</span></code></pre></div>
<div id="fetch-points-leaders" class="cell-output cell-output-display" data-execution_count="1">
<div>


<table class="dataframe caption-top table table-sm table-striped small" data-quarto-postprocess="true" data-border="1">
<thead>
<tr class="header">
<th data-quarto-table-cell-role="th"></th>
<th data-quarto-table-cell-role="th">player_name</th>
<th data-quarto-table-cell-role="th">team</th>
<th data-quarto-table-cell-role="th">points</th>
<th data-quarto-table-cell-role="th">kills</th>
<th data-quarto-table-cell-role="th">attempts</th>
<th data-quarto-table-cell-role="th">kill_pct</th>
<th data-quarto-table-cell-role="th">matches</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td data-quarto-table-cell-role="th">0</td>
<td>Mimi Colyer</td>
<td>Dallas Pulse</td>
<td>516.0</td>
<td>455.0</td>
<td>1180.0</td>
<td>0.385593</td>
<td>28</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">1</td>
<td>Diaz Maldonado</td>
<td>Dallas Pulse</td>
<td>509.0</td>
<td>445.0</td>
<td>1113.0</td>
<td>0.399820</td>
<td>28</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">2</td>
<td>Raina Terry</td>
<td>Columbus Fury</td>
<td>398.0</td>
<td>344.0</td>
<td>1048.0</td>
<td>0.328244</td>
<td>24</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">3</td>
<td>Leah Edmond</td>
<td>Atlanta Vibe</td>
<td>387.0</td>
<td>338.0</td>
<td>984.0</td>
<td>0.343496</td>
<td>24</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">4</td>
<td>Grace Loberg</td>
<td>San Diego Mojo</td>
<td>361.0</td>
<td>301.0</td>
<td>998.0</td>
<td>0.301603</td>
<td>26</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">5</td>
<td>Paige Briggs-Romine</td>
<td>Grand Rapids Rise</td>
<td>351.0</td>
<td>315.0</td>
<td>931.0</td>
<td>0.338346</td>
<td>25</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">6</td>
<td>Carli Snyder</td>
<td>Grand Rapids Rise</td>
<td>334.0</td>
<td>293.0</td>
<td>956.0</td>
<td>0.306485</td>
<td>26</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">7</td>
<td>Brooke Nuneviller</td>
<td>Omaha Supernovas</td>
<td>325.0</td>
<td>306.0</td>
<td>950.0</td>
<td>0.322105</td>
<td>28</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">8</td>
<td>Azhani Tealer</td>
<td>Indy Ignite</td>
<td>319.0</td>
<td>287.0</td>
<td>634.0</td>
<td>0.452681</td>
<td>26</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">9</td>
<td>Aiko Jones</td>
<td>Atlanta Vibe</td>
<td>316.0</td>
<td>273.0</td>
<td>789.0</td>
<td>0.346008</td>
<td>24</td>
</tr>
</tbody>
</table>

</div>
</div>
</div>
<p>Mimi Colyer of Dallas Pulse led the league with 516 points on 455 kills across 28 matches. Her teammate Diaz Maldonado finished second, meaning the league’s two most productive hitters played for the same team all season, a preview of who’d end up cutting down the net in May.</p>
</section>
<section id="the-top-ten-by-points" class="level2">
<h2 class="anchored" data-anchor-id="the-top-ten-by-points">The top ten, by points</h2>
<div id="cell-fig-top-points" class="cell" data-execution_count="2">
<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"><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-2"></span>
<span id="cb2-3">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;">9</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>))</span>
<span id="cb2-4">labels <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> leaders[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"player_name"</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;">" ("</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> leaders[<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> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">")"</span></span>
<span id="cb2-5">ax.barh(labels[::<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>], leaders[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"points"</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>], 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;">"#2c3e50"</span>)</span>
<span id="cb2-6">ax.set_xlabel(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Points"</span>)</span>
<span id="cb2-7">ax.set_title(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Top 10 Scorers, 2025-26 MLV Season"</span>)</span>
<span id="cb2-8">ax.spines[[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"top"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"right"</span>]].set_visible(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">False</span>)</span>
<span id="cb2-9">plt.tight_layout()</span>
<span id="cb2-10">plt.show()</span></code></pre></div>
<div class="cell-output cell-output-display">
<div id="fig-top-points" class="quarto-float quarto-figure quarto-figure-center anchored">
<figure class="quarto-float quarto-float-fig figure">
<div aria-describedby="fig-top-points-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
<img src="https://maydaystats.com/posts/volleyball/2026-07-09-pro-year-in-review/index_files/figure-html/fig-top-points-output-1.png" width="868" height="471" class="figure-img">
</div>
<figcaption class="quarto-float-caption-bottom quarto-float-caption quarto-float-fig" id="fig-top-points-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
Figure&nbsp;1: Top 10 point scorers, 2025-26 MLV regular season and playoffs combined
</figcaption>
</figure>
</div>
</div>
</div>
</section>
<section id="digs-aces-and-assists" class="level2">
<h2 class="anchored" data-anchor-id="digs-aces-and-assists">Digs, aces, and assists</h2>
<div id="fetch-other-leaders" class="cell" data-execution_count="3">
<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">digs_query <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>
<span id="cb3-2"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">SELECT player_name, team, SUM(digs) AS total</span></span>
<span id="cb3-3"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">FROM `maydaystats.mlv_volleyball.boxscores`</span></span>
<span id="cb3-4"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">WHERE team NOT IN ('Team Launiere', 'Team Meske')</span></span>
<span id="cb3-5"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">GROUP BY player_name, team</span></span>
<span id="cb3-6"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">ORDER BY total DESC</span></span>
<span id="cb3-7"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">LIMIT 1</span></span>
<span id="cb3-8"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"""</span></span>
<span id="cb3-9">digs_leader <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> client.query(digs_query).to_dataframe()</span>
<span id="cb3-10"></span>
<span id="cb3-11">assists_query <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>
<span id="cb3-12"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">SELECT player_name, team, SUM(assists) AS total</span></span>
<span id="cb3-13"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">FROM `maydaystats.mlv_volleyball.boxscores`</span></span>
<span id="cb3-14"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">WHERE team NOT IN ('Team Launiere', 'Team Meske')</span></span>
<span id="cb3-15"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">GROUP BY player_name, team</span></span>
<span id="cb3-16"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">ORDER BY total DESC</span></span>
<span id="cb3-17"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">LIMIT 1</span></span>
<span id="cb3-18"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"""</span></span>
<span id="cb3-19">assists_leader <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> client.query(assists_query).to_dataframe()</span>
<span id="cb3-20"></span>
<span id="cb3-21">aces_query <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>
<span id="cb3-22"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">SELECT player_name, team, SUM(service_aces) AS total</span></span>
<span id="cb3-23"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">FROM `maydaystats.mlv_volleyball.boxscores`</span></span>
<span id="cb3-24"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">WHERE team NOT IN ('Team Launiere', 'Team Meske')</span></span>
<span id="cb3-25"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">GROUP BY player_name, team</span></span>
<span id="cb3-26"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">ORDER BY total DESC</span></span>
<span id="cb3-27"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">LIMIT 1</span></span>
<span id="cb3-28"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"""</span></span>
<span id="cb3-29">aces_leader <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> client.query(aces_query).to_dataframe()</span></code></pre></div>
</div>
<p>Shara Venegas (San Diego Mojo) led all players in digs with 320. Marlie Monserez (San Diego Mojo) ran the offense from the back row, topping the league with 1067 assists. And Natalie Foster (Orlando Valkyries) led in aces.</p>
</section>
<section id="the-best-record-didnt-win-it-either" class="level2">
<h2 class="anchored" data-anchor-id="the-best-record-didnt-win-it-either">The best record didn’t win it either</h2>
<div id="cell-fetch-standings" class="cell" data-execution_count="4">
<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">standings_query <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>
<span id="cb4-2"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">WITH regular_season AS (</span></span>
<span id="cb4-3"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  SELECT * FROM `maydaystats.mlv_volleyball.matches`</span></span>
<span id="cb4-4"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  WHERE schedule_event_id NOT IN (637, 638, 639)</span></span>
<span id="cb4-5"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">),</span></span>
<span id="cb4-6"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">team_results AS (</span></span>
<span id="cb4-7"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  SELECT first_team_name AS team,</span></span>
<span id="cb4-8"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">    IF(first_team_score &gt; second_team_score, 1, 0) AS win</span></span>
<span id="cb4-9"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  FROM regular_season</span></span>
<span id="cb4-10"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  UNION ALL</span></span>
<span id="cb4-11"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  SELECT second_team_name AS team,</span></span>
<span id="cb4-12"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">    IF(second_team_score &gt; first_team_score, 1, 0) AS win</span></span>
<span id="cb4-13"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  FROM regular_season</span></span>
<span id="cb4-14"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb4-15"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">SELECT team, SUM(win) AS wins, COUNT(*) - SUM(win) AS losses</span></span>
<span id="cb4-16"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">FROM team_results</span></span>
<span id="cb4-17"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">GROUP BY team</span></span>
<span id="cb4-18"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">ORDER BY wins DESC</span></span>
<span id="cb4-19"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">LIMIT 5</span></span>
<span id="cb4-20"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"""</span></span>
<span id="cb4-21"></span>
<span id="cb4-22">standings <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> client.query(standings_query).to_dataframe()</span>
<span id="cb4-23">standings</span></code></pre></div>
<div id="fetch-standings" class="cell-output cell-output-display" data-execution_count="4">
<div>


<table class="dataframe caption-top table table-sm table-striped small" data-quarto-postprocess="true" data-border="1">
<thead>
<tr class="header">
<th data-quarto-table-cell-role="th"></th>
<th data-quarto-table-cell-role="th">team</th>
<th data-quarto-table-cell-role="th">wins</th>
<th data-quarto-table-cell-role="th">losses</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td data-quarto-table-cell-role="th">0</td>
<td>Indy Ignite</td>
<td>20</td>
<td>5</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">1</td>
<td>Dallas Pulse</td>
<td>18</td>
<td>8</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">2</td>
<td>San Diego Mojo</td>
<td>13</td>
<td>12</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">3</td>
<td>Omaha Supernovas</td>
<td>12</td>
<td>14</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">4</td>
<td>Grand Rapids Rise</td>
<td>11</td>
<td>15</td>
</tr>
</tbody>
</table>

</div>
</div>
</div>
<p>Indy Ignite had the best regular-season record in the league at 20-5, good enough for the No.&nbsp;1 seed in a four-team playoff. It didn’t matter: the No.&nbsp;4 seed knocked them out in the semifinal, the same shape as this site’s <a href="../../../posts/hockey/2026-07-08-year-in-review/index.html">NHL year in review</a>, where Colorado had the league’s best regular season and still lost in the Western Conference Final. The best regular season and the tournament that actually decides the title are two different contests.</p>
</section>
<section id="the-playoffs" class="level2">
<h2 class="anchored" data-anchor-id="the-playoffs">The playoffs</h2>
<div id="cell-fetch-playoffs" class="cell" data-execution_count="5">
<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"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> re</span>
<span id="cb5-2"></span>
<span id="cb5-3">playoff_query <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>
<span id="cb5-4"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">SELECT schedule_event_id, game_date, first_team_name, first_team_score,</span></span>
<span id="cb5-5"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  second_team_name, second_team_score</span></span>
<span id="cb5-6"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">FROM `maydaystats.mlv_volleyball.matches`</span></span>
<span id="cb5-7"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">WHERE schedule_event_id IN (637, 638, 639)</span></span>
<span id="cb5-8"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">ORDER BY game_date</span></span>
<span id="cb5-9"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"""</span></span>
<span id="cb5-10">playoffs <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> client.query(playoff_query).to_dataframe()</span>
<span id="cb5-11"></span>
<span id="cb5-12"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Playoff schedule-events carry a seed prefix in the raw team name</span></span>
<span id="cb5-13"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># ("No. 4 Omaha Supernovas") that the regular-season rows don't - strip</span></span>
<span id="cb5-14"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># it for display, same cleanup the pipeline itself does before matching</span></span>
<span id="cb5-15"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># team names against the PDF text (see fetch.py's _strip_seed).</span></span>
<span id="cb5-16">seed_re <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> re.<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">compile</span>(<span class="vs" style="color: #20794D;
background-color: null;
font-style: inherit;">r"^No\.\s*\d+\s+"</span>)</span>
<span id="cb5-17"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> col <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> (<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"first_team_name"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"second_team_name"</span>):</span>
<span id="cb5-18">    playoffs[col] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> playoffs[col].<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">str</span>.replace(seed_re, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">""</span>, regex<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="cb5-19"></span>
<span id="cb5-20">playoffs</span></code></pre></div>
<div id="fetch-playoffs" class="cell-output cell-output-display" data-execution_count="5">
<div>


<table class="dataframe caption-top table table-sm table-striped small" data-quarto-postprocess="true" data-border="1">
<thead>
<tr class="header">
<th data-quarto-table-cell-role="th"></th>
<th data-quarto-table-cell-role="th">schedule_event_id</th>
<th data-quarto-table-cell-role="th">game_date</th>
<th data-quarto-table-cell-role="th">first_team_name</th>
<th data-quarto-table-cell-role="th">first_team_score</th>
<th data-quarto-table-cell-role="th">second_team_name</th>
<th data-quarto-table-cell-role="th">second_team_score</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td data-quarto-table-cell-role="th">0</td>
<td>637</td>
<td>2026-05-07T23:00:00.000000Z</td>
<td>Indy Ignite</td>
<td>2.0</td>
<td>Omaha Supernovas</td>
<td>3.0</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">1</td>
<td>638</td>
<td>2026-05-08T00:00:00.000000Z</td>
<td>Dallas Pulse</td>
<td>3.0</td>
<td>San Diego Mojo</td>
<td>1.0</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">2</td>
<td>639</td>
<td>2026-05-09T19:00:00.000000Z</td>
<td>Dallas Pulse</td>
<td>3.0</td>
<td>Omaha Supernovas</td>
<td>2.0</td>
</tr>
</tbody>
</table>

</div>
</div>
</div>
<p>Four teams made the playoffs, seeded 1 through 4 on regular-season record, in a single-elimination bracket: 1-seed vs.&nbsp;4-seed, 2-seed vs. 3-seed, winners meet in the final. The No.&nbsp;4 seed Omaha Supernovas beat the No.&nbsp;1 seed Indy Ignite 3-2 in the semifinal, while the No.&nbsp;2 seed Dallas Pulse handled the No.&nbsp;3 seed San Diego Mojo in four sets to reach the final.</p>
<section id="the-semifinal-upset" class="level3">
<h3 class="anchored" data-anchor-id="the-semifinal-upset">The semifinal upset</h3>
<div id="cell-fetch-semifinal-box" class="cell" data-execution_count="6">
<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">semifinal_query <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>
<span id="cb6-2"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">SELECT team, SUM(kills) AS kills, SUM(attack_attempts) AS attempts,</span></span>
<span id="cb6-3"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  SUM(service_errors) AS service_errors,</span></span>
<span id="cb6-4"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  SUM(reception_errors) AS reception_errors, SUM(total_blocks) AS blocks</span></span>
<span id="cb6-5"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">FROM `maydaystats.mlv_volleyball.boxscores`</span></span>
<span id="cb6-6"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">WHERE schedule_event_id = 637</span></span>
<span id="cb6-7"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">GROUP BY team</span></span>
<span id="cb6-8"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"""</span></span>
<span id="cb6-9">semifinal_team_totals <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> client.query(semifinal_query).to_dataframe()</span>
<span id="cb6-10">semifinal_team_totals</span></code></pre></div>
<div id="fetch-semifinal-box" class="cell-output cell-output-display" data-execution_count="6">
<div>


<table class="dataframe caption-top table table-sm table-striped small" data-quarto-postprocess="true" data-border="1">
<thead>
<tr class="header">
<th data-quarto-table-cell-role="th"></th>
<th data-quarto-table-cell-role="th">team</th>
<th data-quarto-table-cell-role="th">kills</th>
<th data-quarto-table-cell-role="th">attempts</th>
<th data-quarto-table-cell-role="th">service_errors</th>
<th data-quarto-table-cell-role="th">reception_errors</th>
<th data-quarto-table-cell-role="th">blocks</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td data-quarto-table-cell-role="th">0</td>
<td>Indy Ignite</td>
<td>70.0</td>
<td>187.0</td>
<td>17.0</td>
<td>4.0</td>
<td>12.0</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">1</td>
<td>Omaha Supernovas</td>
<td>63.0</td>
<td>178.0</td>
<td>13.0</td>
<td>1.0</td>
<td>13.0</td>
</tr>
</tbody>
</table>

</div>
</div>
</div>
<p>The team totals are the surprise: Indy Ignite actually out-hit Omaha in this match, 70 kills on 187 attempts to Omaha’s 63 kills on 178 attempts, and blocking was close, 12 to 13. Indy lost the match anyway, and the reason shows up in two other columns rather than hitting: Indy committed 17 service errors and 4 reception errors, against Omaha’s 13 and 1. Four extra free points from bad passing on top of four extra serves into the net or out of bounds accounts for most of the margin in a match that went the full five sets.</p>
<div id="cell-fetch-semifinal-players" class="cell" data-execution_count="7">
<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">semifinal_players_query <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>
<span id="cb7-2"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">SELECT player_name, team, kills, attack_attempts, total_blocks</span></span>
<span id="cb7-3"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">FROM `maydaystats.mlv_volleyball.boxscores`</span></span>
<span id="cb7-4"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">WHERE schedule_event_id = 637</span></span>
<span id="cb7-5"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  AND player_name IN ('Leketor Member-Meneh', 'Janice Leao')</span></span>
<span id="cb7-6"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"""</span></span>
<span id="cb7-7">semifinal_players <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> client.query(semifinal_players_query).to_dataframe()</span>
<span id="cb7-8">semifinal_players</span></code></pre></div>
<div id="fetch-semifinal-players" class="cell-output cell-output-display" data-execution_count="7">
<div>


<table class="dataframe caption-top table table-sm table-striped small" data-quarto-postprocess="true" data-border="1">
<thead>
<tr class="header">
<th data-quarto-table-cell-role="th"></th>
<th data-quarto-table-cell-role="th">player_name</th>
<th data-quarto-table-cell-role="th">team</th>
<th data-quarto-table-cell-role="th">kills</th>
<th data-quarto-table-cell-role="th">attack_attempts</th>
<th data-quarto-table-cell-role="th">total_blocks</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td data-quarto-table-cell-role="th">0</td>
<td>Janice Leao</td>
<td>Omaha Supernovas</td>
<td>10.0</td>
<td>17.0</td>
<td>4.0</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">1</td>
<td>Leketor Member-Meneh</td>
<td>Indy Ignite</td>
<td>6.0</td>
<td>22.0</td>
<td>NaN</td>
</tr>
</tbody>
</table>

</div>
</div>
</div>
<p>The individual swing goes the other way. Leketor Member-Meneh came in averaging 9.7 kills a match on a .367 hitting percentage all season, Indy’s second-most productive hitter behind Azhani Tealer. Against Omaha she went 6-for-22, a .273 clip on well below her usual number of swings. Omaha, meanwhile, got a night nobody could have projected from its bench: Janice Leao entered the match averaging 1.6 kills and 1.3 blocks per match on a .344 hitting percentage for the season, and went 10-for-17 (.588) with 4 blocks, by a wide margin her best match of the year, in the one match her team needed it most.</p>
<div id="cell-fetch-final-box" class="cell" data-execution_count="8">
<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">final_query <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>
<span id="cb8-2"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">SELECT player_name, team, kills, points, digs, total_blocks</span></span>
<span id="cb8-3"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">FROM `maydaystats.mlv_volleyball.boxscores`</span></span>
<span id="cb8-4"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">WHERE schedule_event_id = 639</span></span>
<span id="cb8-5"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">ORDER BY points DESC</span></span>
<span id="cb8-6"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">LIMIT 5</span></span>
<span id="cb8-7"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"""</span></span>
<span id="cb8-8">final_box <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> client.query(final_query).to_dataframe()</span>
<span id="cb8-9">final_box</span></code></pre></div>
<div id="fetch-final-box" class="cell-output cell-output-display" data-execution_count="8">
<div>


<table class="dataframe caption-top table table-sm table-striped small" data-quarto-postprocess="true" data-border="1">
<thead>
<tr class="header">
<th data-quarto-table-cell-role="th"></th>
<th data-quarto-table-cell-role="th">player_name</th>
<th data-quarto-table-cell-role="th">team</th>
<th data-quarto-table-cell-role="th">kills</th>
<th data-quarto-table-cell-role="th">points</th>
<th data-quarto-table-cell-role="th">digs</th>
<th data-quarto-table-cell-role="th">total_blocks</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td data-quarto-table-cell-role="th">0</td>
<td>Diaz Maldonado</td>
<td>Dallas Pulse</td>
<td>26.0</td>
<td>27.0</td>
<td>11.0</td>
<td>1.0</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">1</td>
<td>Merritt Beason</td>
<td>Omaha Supernovas</td>
<td>14.0</td>
<td>20.0</td>
<td>6.0</td>
<td>5.0</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">2</td>
<td>Brooke Nuneviller</td>
<td>Omaha Supernovas</td>
<td>15.0</td>
<td>16.0</td>
<td>22.0</td>
<td>1.0</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">3</td>
<td>Parsons Wilhite</td>
<td>Omaha Supernovas</td>
<td>12.0</td>
<td>16.0</td>
<td>19.0</td>
<td>1.0</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">4</td>
<td>Kaylee Cox</td>
<td>Dallas Pulse</td>
<td>9.0</td>
<td>13.0</td>
<td>11.0</td>
<td>4.0</td>
</tr>
</tbody>
</table>

</div>
</div>
</div>
<p>Dallas Pulse beat Omaha Supernovas 3-2 in the final to win the title, powered by Diaz Maldonado’s 26 kills and 27 points, a clean encore of the two-hitter attack that carried the team all season. The runner-up’s semifinal upset ran out of steam one match short of the trophy.</p>
</section>
</section>
<section id="whats-next" class="level2">
<h2 class="anchored" data-anchor-id="whats-next">What’s next</h2>
<p>This covers the season at a high level: the league leaderboards, the regular-season standings, and the playoff bracket itself. The same table supports much narrower questions too, like a single hitter’s efficiency swings across the two scoresheet eras, or how a team’s block numbers held up on the road. Those are posts for another day, now that a full season of validated data is sitting in BigQuery.</p>
<p>The pipeline code behind this post <a href="https://github.com/maydaytek/maydaystats-pipelines">lives here</a>.</p>
<div class="callout callout-style-default callout-note callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Note
</div>
</div>
<div class="callout-body-container callout-body">
<p>Every match row in this dataset carries a <code>checksum_ok</code> flag: the pipeline sums each parsed player’s stats and cross-checks the total against the scoresheet’s own printed team total, and flags anything that doesn’t match exactly rather than trusting it silently. 88 of the 99 matches in the dataset check out clean; the other 11 are flagged for a small, isolated discrepancy in one column (attack attempts) that traces back to the scoresheet’s own printed total rather than a parsing error - see the pipeline’s <code>DEPLOY.md</code> for the full investigation. Nothing in this post depends on that column, but a query that does should filter <code>WHERE checksum_ok</code>.</p>
<p>Like the other posts on this site, this one uses Quarto’s frozen execution (<code>freeze: auto</code>): the queries above ran once, locally, against BigQuery, and the deployed site reuses that committed output rather than re-querying on every build.</p>
</div>
</div>


</section>

 ]]></description>
  <category>Volleyball</category>
  <category>Pro</category>
  <category>Featured</category>
  <category>Season Recap</category>
  <guid>https://maydaystats.com/posts/volleyball/2026-07-09-pro-year-in-review/</guid>
  <pubDate>Thu, 09 Jul 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>First Pitches: Standing Up the Statcast Pipeline</title>
  <link>https://maydaystats.com/posts/baseball/2026-07-08-first-pitches/</link>
  <description><![CDATA[ 




<p>This is the first post through the full pipeline: a Cloud Run Job pulls pitch-level Statcast data daily and appends it to BigQuery, and this page reads straight from that table and renders a chart. Nothing here is a deep analytical claim yet - the point of this post is narrower and more important than that: confirming that data really does flow from the source all the way to a published page, with no manual step in between.</p>
<section id="querying-the-pipelines-output" class="level2">
<h2 class="anchored" data-anchor-id="querying-the-pipelines-output">Querying the pipeline’s output</h2>
<div id="cell-fetch-data" class="cell" data-execution_count="1">
<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;">from</span> google.cloud <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> bigquery</span>
<span id="cb1-2"></span>
<span id="cb1-3">client <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> bigquery.Client()</span>
<span id="cb1-4"></span>
<span id="cb1-5">query <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>
<span id="cb1-6"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">SELECT</span></span>
<span id="cb1-7"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  pitch_type,</span></span>
<span id="cb1-8"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  COUNT(*) AS n_pitches,</span></span>
<span id="cb1-9"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  AVG(release_speed) AS avg_release_speed,</span></span>
<span id="cb1-10"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  AVG(release_spin_rate) AS avg_spin_rate</span></span>
<span id="cb1-11"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">FROM `maydaystats.mlb_statcast.pitches`</span></span>
<span id="cb1-12"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">WHERE pitch_type IS NOT NULL</span></span>
<span id="cb1-13"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">GROUP BY pitch_type</span></span>
<span id="cb1-14"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">ORDER BY n_pitches DESC</span></span>
<span id="cb1-15"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"""</span></span>
<span id="cb1-16"></span>
<span id="cb1-17">df <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> client.query(query).to_dataframe()</span>
<span id="cb1-18">df</span></code></pre></div>
<div id="fetch-data" class="cell-output cell-output-display" data-execution_count="1">
<div>


<table class="dataframe caption-top table table-sm table-striped small" data-quarto-postprocess="true" data-border="1">
<thead>
<tr class="header">
<th data-quarto-table-cell-role="th"></th>
<th data-quarto-table-cell-role="th">pitch_type</th>
<th data-quarto-table-cell-role="th">n_pitches</th>
<th data-quarto-table-cell-role="th">avg_release_speed</th>
<th data-quarto-table-cell-role="th">avg_spin_rate</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td data-quarto-table-cell-role="th">0</td>
<td>FF</td>
<td>1479</td>
<td>95.085936</td>
<td>2340.053414</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">1</td>
<td>SI</td>
<td>832</td>
<td>93.824760</td>
<td>2213.739183</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">2</td>
<td>SL</td>
<td>593</td>
<td>85.249747</td>
<td>2434.556492</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">3</td>
<td>CH</td>
<td>461</td>
<td>85.870065</td>
<td>1746.629067</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">4</td>
<td>ST</td>
<td>433</td>
<td>83.066051</td>
<td>2686.658199</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">5</td>
<td>FC</td>
<td>386</td>
<td>89.453627</td>
<td>2413.054404</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">6</td>
<td>CU</td>
<td>283</td>
<td>80.454770</td>
<td>2629.568905</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">7</td>
<td>FS</td>
<td>148</td>
<td>88.922973</td>
<td>1570.432432</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">8</td>
<td>KC</td>
<td>124</td>
<td>84.465323</td>
<td>2385.064516</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">9</td>
<td>FO</td>
<td>22</td>
<td>83.954545</td>
<td>1258.636364</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">10</td>
<td>FA</td>
<td>20</td>
<td>63.150000</td>
<td>1503.700000</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">11</td>
<td>SV</td>
<td>13</td>
<td>82.800000</td>
<td>2873.769231</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">12</td>
<td>CS</td>
<td>1</td>
<td>67.200000</td>
<td>2449.000000</td>
</tr>
</tbody>
</table>

</div>
</div>
</div>
<p>As of this post, the table holds 4795 pitches across 13 distinct pitch types.</p>
</section>
<section id="average-release-speed-by-pitch-type" class="level2">
<h2 class="anchored" data-anchor-id="average-release-speed-by-pitch-type">Average release speed by pitch type</h2>
<div id="cell-fig-velocity-by-pitch-type" class="cell" data-execution_count="2">
<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"><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-2"></span>
<span id="cb2-3">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="cb2-4">ax.bar(df[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"pitch_type"</span>], df[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"avg_release_speed"</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;">"#2c3e50"</span>)</span>
<span id="cb2-5">ax.set_xlabel(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Pitch type"</span>)</span>
<span id="cb2-6">ax.set_ylabel(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Average release speed (mph)"</span>)</span>
<span id="cb2-7">ax.set_title(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Average Release Speed by Pitch Type"</span>)</span>
<span id="cb2-8">ax.spines[[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"top"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"right"</span>]].set_visible(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">False</span>)</span>
<span id="cb2-9">plt.tight_layout()</span>
<span id="cb2-10">plt.show()</span></code></pre></div>
<div class="cell-output cell-output-display">
<div id="fig-velocity-by-pitch-type" class="quarto-float quarto-figure quarto-figure-center anchored">
<figure class="quarto-float quarto-float-fig figure">
<div aria-describedby="fig-velocity-by-pitch-type-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
<img src="https://maydaystats.com/posts/baseball/2026-07-08-first-pitches/index_files/figure-html/fig-velocity-by-pitch-type-output-1.png" width="759" height="470" class="figure-img">
</div>
<figcaption class="quarto-float-caption-bottom quarto-float-caption quarto-float-fig" id="fig-velocity-by-pitch-type-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
Figure&nbsp;1: Average release speed by pitch type, all games loaded so far
</figcaption>
</figure>
</div>
</div>
</div>
</section>
<section id="what-this-confirms-and-whats-next" class="level2">
<h2 class="anchored" data-anchor-id="what-this-confirms-and-whats-next">What this confirms, and what’s next</h2>
<p>Every number and every bar above came from a live query against <code>mlb_statcast.pitches</code>, which itself only exists because a Cloud Scheduler job triggers a Cloud Run Job every morning that pulls the previous day’s Statcast data. As more days accumulate, this same query pattern (and others like it) becomes the basis for real analysis rather than pipeline validation.</p>
<p>The pipeline code behind this post <a href="https://github.com/maydaytek/maydaystats-pipelines">lives here</a>.</p>
<div class="callout callout-style-default callout-note callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Note
</div>
</div>
<div class="callout-body-container callout-body">
<p>This post uses Quarto’s frozen execution (<code>freeze: auto</code>): the code above only re-runs when I re-render locally with fresh BigQuery access. The deployed site reuses that committed output rather than re-querying BigQuery on every build, which is what lets Cloudflare Pages build this site without needing my GCP credentials.</p>
</div>
</div>


</section>

 ]]></description>
  <category>Baseball</category>
  <category>Pipeline</category>
  <guid>https://maydaystats.com/posts/baseball/2026-07-08-first-pitches/</guid>
  <pubDate>Wed, 08 Jul 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Year in Review: The 2025-26 NHL Season</title>
  <link>https://maydaystats.com/posts/hockey/2026-07-08-year-in-review/</link>
  <description><![CDATA[ 




<p>The hockey pipeline has now pulled a complete season: every regular-season and playoff game from the 2025-26 NHL season, backfilled game by game into BigQuery. This post is the first real analysis built on top of it, a look back at the season through the same data the daily pipeline keeps adding to.</p>
<p>One wrinkle worth flagging up front: the NHL’s schedule API also listed games from the Milano Cortina Winter Olympics during the league’s February break, tagged with a different game type than regular-season or playoff games. Those are country-vs-country games, not NHL team games, so they’re filtered out of everything below.</p>
<section id="querying-the-season" class="level2">
<h2 class="anchored" data-anchor-id="querying-the-season">Querying the season</h2>
<div id="cell-fetch-scorers" class="cell" data-execution_count="1">
<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;">from</span> google.cloud <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> bigquery</span>
<span id="cb1-2"></span>
<span id="cb1-3">client <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> bigquery.Client()</span>
<span id="cb1-4"></span>
<span id="cb1-5">query <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>
<span id="cb1-6"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">SELECT</span></span>
<span id="cb1-7"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  player_name,</span></span>
<span id="cb1-8"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  team,</span></span>
<span id="cb1-9"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  SUM(goals) AS goals,</span></span>
<span id="cb1-10"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  SUM(assists) AS assists,</span></span>
<span id="cb1-11"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  SUM(points) AS points,</span></span>
<span id="cb1-12"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  COUNT(DISTINCT game_id) AS games_played</span></span>
<span id="cb1-13"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">FROM `maydaystats.nhl_stats.boxscores`</span></span>
<span id="cb1-14"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">WHERE CAST(SUBSTR(CAST(game_id AS STRING), 5, 2) AS INT64) IN (2, 3)</span></span>
<span id="cb1-15"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  AND position_group != 'goalie'</span></span>
<span id="cb1-16"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">GROUP BY player_name, team</span></span>
<span id="cb1-17"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">ORDER BY points DESC</span></span>
<span id="cb1-18"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">LIMIT 10</span></span>
<span id="cb1-19"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"""</span></span>
<span id="cb1-20"></span>
<span id="cb1-21">scorers <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> client.query(query).to_dataframe()</span>
<span id="cb1-22">scorers</span></code></pre></div>
<div id="fetch-scorers" class="cell-output cell-output-display" data-execution_count="1">
<div>


<table class="dataframe caption-top table table-sm table-striped small" data-quarto-postprocess="true" data-border="1">
<thead>
<tr class="header">
<th data-quarto-table-cell-role="th"></th>
<th data-quarto-table-cell-role="th">player_name</th>
<th data-quarto-table-cell-role="th">team</th>
<th data-quarto-table-cell-role="th">goals</th>
<th data-quarto-table-cell-role="th">assists</th>
<th data-quarto-table-cell-role="th">points</th>
<th data-quarto-table-cell-role="th">games_played</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td data-quarto-table-cell-role="th">0</td>
<td>N. MacKinnon</td>
<td>COL</td>
<td>64.0</td>
<td>86.0</td>
<td>150.0</td>
<td>93</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">1</td>
<td>C. McDavid</td>
<td>EDM</td>
<td>49.0</td>
<td>98.0</td>
<td>147.0</td>
<td>88</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">2</td>
<td>N. Kucherov</td>
<td>TBL</td>
<td>47.0</td>
<td>91.0</td>
<td>138.0</td>
<td>84</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">3</td>
<td>N. Suzuki</td>
<td>MTL</td>
<td>33.0</td>
<td>89.0</td>
<td>122.0</td>
<td>101</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">4</td>
<td>M. Necas</td>
<td>COL</td>
<td>42.0</td>
<td>79.0</td>
<td>121.0</td>
<td>91</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">5</td>
<td>J. Eichel</td>
<td>VGK</td>
<td>31.0</td>
<td>88.0</td>
<td>119.0</td>
<td>96</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">6</td>
<td>M. Celebrini</td>
<td>SJS</td>
<td>45.0</td>
<td>73.0</td>
<td>118.0</td>
<td>82</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">7</td>
<td>D. Pastrnak</td>
<td>BOS</td>
<td>33.0</td>
<td>79.0</td>
<td>112.0</td>
<td>83</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">8</td>
<td>K. Kaprizov</td>
<td>MIN</td>
<td>52.0</td>
<td>59.0</td>
<td>111.0</td>
<td>89</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">9</td>
<td>M. Marner</td>
<td>VGK</td>
<td>34.0</td>
<td>77.0</td>
<td>111.0</td>
<td>103</td>
</tr>
</tbody>
</table>

</div>
</div>
</div>
<p>N. MacKinnon led the league with 150 points (64 goals, 86 assists) across 93 games, including the playoffs. Connor McDavid finished a close second, and Colorado and Vegas each placed two players in the top ten scorers, more than any other team.</p>
</section>
<section id="the-top-ten-by-points" class="level2">
<h2 class="anchored" data-anchor-id="the-top-ten-by-points">The top ten, by points</h2>
<div id="cell-fig-top-scorers" class="cell" data-execution_count="2">
<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"><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-2"></span>
<span id="cb2-3">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;">9</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>))</span>
<span id="cb2-4">labels <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> scorers[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"player_name"</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;">" ("</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> scorers[<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> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">")"</span></span>
<span id="cb2-5">ax.barh(labels[::<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>], scorers[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"points"</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>], 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;">"#2c3e50"</span>)</span>
<span id="cb2-6">ax.set_xlabel(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Points"</span>)</span>
<span id="cb2-7">ax.set_title(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Top 10 Scorers, 2025-26 Season"</span>)</span>
<span id="cb2-8">ax.spines[[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"top"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"right"</span>]].set_visible(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">False</span>)</span>
<span id="cb2-9">plt.tight_layout()</span>
<span id="cb2-10">plt.show()</span></code></pre></div>
<div class="cell-output cell-output-display">
<div id="fig-top-scorers" class="quarto-float quarto-figure quarto-figure-center anchored">
<figure class="quarto-float quarto-float-fig figure">
<div aria-describedby="fig-top-scorers-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
<img src="https://maydaystats.com/posts/hockey/2026-07-08-year-in-review/index_files/figure-html/fig-top-scorers-output-1.png" width="862" height="471" class="figure-img">
</div>
<figcaption class="quarto-float-caption-bottom quarto-float-caption quarto-float-fig" id="fig-top-scorers-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
Figure&nbsp;1: Top 10 point scorers, 2025-26 regular season and playoffs combined
</figcaption>
</figure>
</div>
</div>
</div>
</section>
<section id="the-best-record-didnt-win-the-cup" class="level2">
<h2 class="anchored" data-anchor-id="the-best-record-didnt-win-the-cup">The best record didn’t win the Cup</h2>
<div id="cell-fetch-standings" class="cell" data-execution_count="3">
<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">standings_query <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>
<span id="cb3-2"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">SELECT</span></span>
<span id="cb3-3"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  team,</span></span>
<span id="cb3-4"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  COUNTIF(decision = 'W') AS wins,</span></span>
<span id="cb3-5"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  COUNTIF(decision = 'L') AS losses</span></span>
<span id="cb3-6"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">FROM `maydaystats.nhl_stats.boxscores`</span></span>
<span id="cb3-7"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">WHERE CAST(SUBSTR(CAST(game_id AS STRING), 5, 2) AS INT64) = 2</span></span>
<span id="cb3-8"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  AND position_group = 'goalie'</span></span>
<span id="cb3-9"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  AND decision IS NOT NULL</span></span>
<span id="cb3-10"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">GROUP BY team</span></span>
<span id="cb3-11"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">ORDER BY wins DESC</span></span>
<span id="cb3-12"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">LIMIT 5</span></span>
<span id="cb3-13"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"""</span></span>
<span id="cb3-14"></span>
<span id="cb3-15">standings <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> client.query(standings_query).to_dataframe()</span>
<span id="cb3-16">standings</span></code></pre></div>
<div id="fetch-standings" class="cell-output cell-output-display" data-execution_count="3">
<div>


<table class="dataframe caption-top table table-sm table-striped small" data-quarto-postprocess="true" data-border="1">
<thead>
<tr class="header">
<th data-quarto-table-cell-role="th"></th>
<th data-quarto-table-cell-role="th">team</th>
<th data-quarto-table-cell-role="th">wins</th>
<th data-quarto-table-cell-role="th">losses</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td data-quarto-table-cell-role="th">0</td>
<td>COL</td>
<td>58</td>
<td>16</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">1</td>
<td>CAR</td>
<td>55</td>
<td>22</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">2</td>
<td>DAL</td>
<td>52</td>
<td>20</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">3</td>
<td>TBL</td>
<td>51</td>
<td>28</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">4</td>
<td>MTL</td>
<td>50</td>
<td>25</td>
</tr>
</tbody>
</table>

</div>
</div>
</div>
<p>Colorado had the best regular-season record in the league at 58-16, built in part on Nathan MacKinnon’s scoring, and Carolina wasn’t far behind. But the regular season and the playoffs are different tournaments. Colorado swept the Kings in the first round and beat Minnesota 4 games to 1 in the second, then ran into Vegas in the Western Conference Final and lost in a sweep of their own. Carolina kept going all the way to the Stanley Cup Final, where they met that same Vegas team.</p>
</section>
<section id="what-actually-happened-to-colorado" class="level2">
<h2 class="anchored" data-anchor-id="what-actually-happened-to-colorado">What actually happened to Colorado</h2>
<div id="cell-fetch-col-series" class="cell" data-execution_count="4">
<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">col_series_query <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>
<span id="cb4-2"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">SELECT game_id, team, SUM(goals) AS goals</span></span>
<span id="cb4-3"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">FROM `maydaystats.nhl_stats.boxscores`</span></span>
<span id="cb4-4"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">WHERE game_id IN (2025030321, 2025030322, 2025030323, 2025030324)</span></span>
<span id="cb4-5"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">GROUP BY game_id, team</span></span>
<span id="cb4-6"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">ORDER BY game_id, team</span></span>
<span id="cb4-7"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"""</span></span>
<span id="cb4-8">col_series <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> client.query(col_series_query).to_dataframe()</span>
<span id="cb4-9">col_series</span></code></pre></div>
<div id="fetch-col-series" class="cell-output cell-output-display" data-execution_count="4">
<div>


<table class="dataframe caption-top table table-sm table-striped small" data-quarto-postprocess="true" data-border="1">
<thead>
<tr class="header">
<th data-quarto-table-cell-role="th"></th>
<th data-quarto-table-cell-role="th">game_id</th>
<th data-quarto-table-cell-role="th">team</th>
<th data-quarto-table-cell-role="th">goals</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td data-quarto-table-cell-role="th">0</td>
<td>2025030321</td>
<td>COL</td>
<td>2.0</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">1</td>
<td>2025030321</td>
<td>VGK</td>
<td>4.0</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">2</td>
<td>2025030322</td>
<td>COL</td>
<td>1.0</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">3</td>
<td>2025030322</td>
<td>VGK</td>
<td>3.0</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">4</td>
<td>2025030323</td>
<td>COL</td>
<td>3.0</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">5</td>
<td>2025030323</td>
<td>VGK</td>
<td>5.0</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">6</td>
<td>2025030324</td>
<td>COL</td>
<td>1.0</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">7</td>
<td>2025030324</td>
<td>VGK</td>
<td>2.0</td>
</tr>
</tbody>
</table>

</div>
</div>
</div>
<p>Four games, four Vegas wins: 4-2, 3-1, 5-3, 2-1. It wasn’t a case of Colorado getting run off the ice, three of the four games were decided by a single goal or two, but a sweep is a sweep, and the box scores point at two specific reasons rather than a generic cold streak.</p>
<div id="cell-fetch-col-goalies" class="cell" data-execution_count="5">
<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">goalie_query <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>
<span id="cb5-2"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">SELECT game_id, team, player_name, save_pctg</span></span>
<span id="cb5-3"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">FROM `maydaystats.nhl_stats.boxscores`</span></span>
<span id="cb5-4"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">WHERE game_id IN (2025030321, 2025030322, 2025030323, 2025030324)</span></span>
<span id="cb5-5"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  AND position_group = 'goalie'</span></span>
<span id="cb5-6"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  AND save_pctg IS NOT NULL</span></span>
<span id="cb5-7"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">ORDER BY game_id, team</span></span>
<span id="cb5-8"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"""</span></span>
<span id="cb5-9">goalies <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> client.query(goalie_query).to_dataframe()</span>
<span id="cb5-10">goalies</span></code></pre></div>
<div id="fetch-col-goalies" class="cell-output cell-output-display" data-execution_count="5">
<div>


<table class="dataframe caption-top table table-sm table-striped small" data-quarto-postprocess="true" data-border="1">
<thead>
<tr class="header">
<th data-quarto-table-cell-role="th"></th>
<th data-quarto-table-cell-role="th">game_id</th>
<th data-quarto-table-cell-role="th">team</th>
<th data-quarto-table-cell-role="th">player_name</th>
<th data-quarto-table-cell-role="th">save_pctg</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td data-quarto-table-cell-role="th">0</td>
<td>2025030321</td>
<td>COL</td>
<td>S. Wedgewood</td>
<td>0.888889</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">1</td>
<td>2025030321</td>
<td>VGK</td>
<td>C. Hart</td>
<td>0.947368</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">2</td>
<td>2025030322</td>
<td>COL</td>
<td>S. Wedgewood</td>
<td>0.916667</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">3</td>
<td>2025030322</td>
<td>VGK</td>
<td>C. Hart</td>
<td>0.967742</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">4</td>
<td>2025030323</td>
<td>COL</td>
<td>S. Wedgewood</td>
<td>0.818182</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">5</td>
<td>2025030323</td>
<td>VGK</td>
<td>C. Hart</td>
<td>0.914286</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">6</td>
<td>2025030324</td>
<td>COL</td>
<td>M. Blackwood</td>
<td>0.923077</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">7</td>
<td>2025030324</td>
<td>VGK</td>
<td>C. Hart</td>
<td>0.952381</td>
</tr>
</tbody>
</table>

</div>
</div>
</div>
<p>Vegas starter Cam Hart never had a bad night: a .947 save percentage in Game 1, .968 in Game 2, .914 in Game 3, .952 in Game 4, four consecutive starts above his own regular-season form. Colorado’s own goaltending was more uneven than collapsed: Scott Wedgewood started the first three games and posted a .908 save percentage across the regular season, but ranged from .889 to .917 to .818 in this series, a rough Game 3 more than a full collapse. Mackenzie Blackwood took over for Game 4 and actually outplayed his own .894 season average, stopping .923. Colorado’s goaltending wasn’t the story on its own. Vegas’s goalie having the series of his season was.</p>
<div id="cell-fetch-mackinnon-series" class="cell" data-execution_count="6">
<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">mackinnon_query <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>
<span id="cb6-2"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">SELECT COUNT(DISTINCT game_id) AS games, SUM(goals) AS goals,</span></span>
<span id="cb6-3"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  SUM(assists) AS assists, SUM(points) AS points</span></span>
<span id="cb6-4"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">FROM `maydaystats.nhl_stats.boxscores`</span></span>
<span id="cb6-5"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">WHERE player_name = 'N. MacKinnon' AND team = 'COL'</span></span>
<span id="cb6-6"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  AND game_id IN (2025030321, 2025030322, 2025030323, 2025030324)</span></span>
<span id="cb6-7"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"""</span></span>
<span id="cb6-8">mackinnon_series <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> client.query(mackinnon_query).to_dataframe()</span>
<span id="cb6-9">mackinnon_series</span></code></pre></div>
<div id="fetch-mackinnon-series" class="cell-output cell-output-display" data-execution_count="6">
<div>


<table class="dataframe caption-top table table-sm table-striped small" data-quarto-postprocess="true" data-border="1">
<thead>
<tr class="header">
<th data-quarto-table-cell-role="th"></th>
<th data-quarto-table-cell-role="th">games</th>
<th data-quarto-table-cell-role="th">goals</th>
<th data-quarto-table-cell-role="th">assists</th>
<th data-quarto-table-cell-role="th">points</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td data-quarto-table-cell-role="th">0</td>
<td>4</td>
<td>0.0</td>
<td>2.0</td>
<td>2.0</td>
</tr>
</tbody>
</table>

</div>
</div>
</div>
<p>The bigger story is Nathan MacKinnon, the same player whose scoring this post already credited with carrying Colorado’s regular season. Across 80 regular-season games he scored 57 goals, a .71-goal-per-game pace. Across the four-game sweep against Vegas he scored 0. Zero. Two assists, two total points, in the series that ended his team’s season. Gabriel Landeskog led the team in goals with 3, and tied Nazem Kadri and Devon Toews for the team lead in points at 3 apiece, but a team built around its best player suddenly not scoring, against a goalie who was outplaying his own season averages every single night, is a specific, findable reason a 58-win team went home in May rather than a vague “playoffs are different” shrug.</p>
<div id="cell-fetch-final" class="cell" data-execution_count="7">
<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">final_query <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>
<span id="cb7-2"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">WITH final_games AS (</span></span>
<span id="cb7-3"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  SELECT game_id, game_date, team, SUM(goals) AS team_goals</span></span>
<span id="cb7-4"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  FROM `maydaystats.nhl_stats.boxscores`</span></span>
<span id="cb7-5"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  WHERE CAST(SUBSTR(CAST(game_id AS STRING), 5, 2) AS INT64) = 3</span></span>
<span id="cb7-6"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">    AND team IN ('CAR', 'VGK')</span></span>
<span id="cb7-7"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  GROUP BY game_id, game_date, team</span></span>
<span id="cb7-8"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">)</span></span>
<span id="cb7-9"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">SELECT</span></span>
<span id="cb7-10"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  game_date,</span></span>
<span id="cb7-11"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  MAX(IF(team = 'CAR', team_goals, NULL)) AS car_goals,</span></span>
<span id="cb7-12"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  MAX(IF(team = 'VGK', team_goals, NULL)) AS vgk_goals</span></span>
<span id="cb7-13"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">FROM final_games</span></span>
<span id="cb7-14"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">GROUP BY game_date</span></span>
<span id="cb7-15"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">HAVING car_goals IS NOT NULL AND vgk_goals IS NOT NULL</span></span>
<span id="cb7-16"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">ORDER BY game_date</span></span>
<span id="cb7-17"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"""</span></span>
<span id="cb7-18"></span>
<span id="cb7-19">final <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> client.query(final_query).to_dataframe()</span>
<span id="cb7-20">final</span></code></pre></div>
<div id="fetch-final" class="cell-output cell-output-display" data-execution_count="7">
<div>


<table class="dataframe caption-top table table-sm table-striped small" data-quarto-postprocess="true" data-border="1">
<thead>
<tr class="header">
<th data-quarto-table-cell-role="th"></th>
<th data-quarto-table-cell-role="th">game_date</th>
<th data-quarto-table-cell-role="th">car_goals</th>
<th data-quarto-table-cell-role="th">vgk_goals</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td data-quarto-table-cell-role="th">0</td>
<td>2026-05-04</td>
<td>3.0</td>
<td>3.0</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">1</td>
<td>2026-06-02</td>
<td>4.0</td>
<td>5.0</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">2</td>
<td>2026-06-04</td>
<td>4.0</td>
<td>3.0</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">3</td>
<td>2026-06-06</td>
<td>4.0</td>
<td>5.0</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">4</td>
<td>2026-06-09</td>
<td>5.0</td>
<td>3.0</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">5</td>
<td>2026-06-11</td>
<td>4.0</td>
<td>2.0</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">6</td>
<td>2026-06-14</td>
<td>3.0</td>
<td>0.0</td>
</tr>
</tbody>
</table>

</div>
</div>
</div>
<p>The Final went six games, and three of them (Games 1 through 3) were decided by a single goal. Carolina closed it out on June 14 with a 3-0 shutout, taking the series 4 games to 2. Vegas rode Cam Hart the entire playoffs, all 22 games across all four rounds, all the way through the Final loss. Carolina’s path in goal was less steady: Frederik Andersen started most of the postseason, including the first two games of the Final, but Carolina turned to Brandon Bussi for Game 3, which Bussi lost, before he settled in and won Games 4, 5, and 6 to close out the series. Neither goalie led the league in wins during the regular season; that distinction was an exact tie between Tampa Bay’s Andrei Vasilevskiy and Utah’s Karel Vejmelka, both at 39.</p>
</section>
<section id="whats-next" class="level2">
<h2 class="anchored" data-anchor-id="whats-next">What’s next</h2>
<p>This post covers the season at a high level: scoring, standings, and the playoff run. The same table supports much narrower questions too, like how a specific team’s goaltending held up across back-to-backs, or how a rookie’s production changed after the Olympic break. Those are posts for another day, now that a full season of clean data is sitting in BigQuery.</p>
<p>The pipeline code behind this post <a href="https://github.com/maydaytek/maydaystats-pipelines">lives here</a>.</p>
<div class="callout callout-style-default callout-note callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Note
</div>
</div>
<div class="callout-body-container callout-body">
<p>Like the <a href="../../../baseball.html">baseball</a> posts, this one uses Quarto’s frozen execution (<code>freeze: auto</code>): the queries above ran once, locally, against BigQuery, and the deployed site reuses that committed output rather than re-querying on every build.</p>
</div>
</div>


</section>

 ]]></description>
  <category>Hockey</category>
  <category>Featured</category>
  <category>Season Recap</category>
  <guid>https://maydaystats.com/posts/hockey/2026-07-08-year-in-review/</guid>
  <pubDate>Wed, 08 Jul 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Year in Review: The 2025-26 NCAA Men’s Volleyball Season</title>
  <link>https://maydaystats.com/posts/volleyball/2026-07-08-mens-year-in-review/</link>
  <description><![CDATA[ 




<p>The men’s volleyball pipeline has now pulled a complete season: every Division I match from January through the national championship in early May, backfilled game by game into BigQuery via the self-hosted NCAA API proxy. This post is the first real analysis built on top of it.</p>
<p>One note on the data: about 6% of rows carry a blank player name despite having real stat values, a gap on NCAA’s own side for a small number of games rather than a pipeline bug (see the pipeline’s <code>DEPLOY.md</code> for the full explanation). Everything below filters those rows out.</p>
<section id="querying-the-season" class="level2">
<h2 class="anchored" data-anchor-id="querying-the-season">Querying the season</h2>
<div id="cell-fetch-kills-leaders" class="cell" data-execution_count="1">
<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;">from</span> google.cloud <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> bigquery</span>
<span id="cb1-2"></span>
<span id="cb1-3">client <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> bigquery.Client()</span>
<span id="cb1-4"></span>
<span id="cb1-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># NCAA's API returns team as its own 6-character code (e.g. "PEPPER"), not</span></span>
<span id="cb1-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># a school name. Tables and charts below keep the raw code as-is (matches</span></span>
<span id="cb1-7"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># how the hockey post's chart also uses raw team codes); this mapping is</span></span>
<span id="cb1-8"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># only used to spell out real school names in the prose. A code not in</span></span>
<span id="cb1-9"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># this dict falls back to showing the raw abbreviation rather than</span></span>
<span id="cb1-10"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># failing, in case a future re-render surfaces a school not covered yet.</span></span>
<span id="cb1-11">TEAM_NAMES <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> {</span>
<span id="cb1-12">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"PEPPER"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Pepperdine"</span>,</span>
<span id="cb1-13">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"UC IRV"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"UC Irvine"</span>,</span>
<span id="cb1-14">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"BALLST"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Ball State"</span>,</span>
<span id="cb1-15">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"CATAWB"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Catawba"</span>,</span>
<span id="cb1-16">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"PENNST"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Penn State"</span>,</span>
<span id="cb1-17">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"MCKEND"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"McKendree"</span>,</span>
<span id="cb1-18">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"EMMAN"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Emmanuel (Ga.)"</span>,</span>
<span id="cb1-19">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"LEWIS"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Lewis"</span>,</span>
<span id="cb1-20">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"HAWAII"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Hawaii"</span>,</span>
<span id="cb1-21">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"UCLA"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"UCLA"</span>,</span>
<span id="cb1-22">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"USC"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"USC"</span>,</span>
<span id="cb1-23">}</span>
<span id="cb1-24"></span>
<span id="cb1-25"></span>
<span id="cb1-26"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> team_name(code):</span>
<span id="cb1-27">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> TEAM_NAMES.get(code, code)</span>
<span id="cb1-28"></span>
<span id="cb1-29"></span>
<span id="cb1-30">query <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>
<span id="cb1-31"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">SELECT</span></span>
<span id="cb1-32"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  player_name,</span></span>
<span id="cb1-33"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  team,</span></span>
<span id="cb1-34"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  SUM(CAST(kills AS INT64)) AS kills,</span></span>
<span id="cb1-35"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  SUM(CAST(attack_attempts AS INT64)) AS attempts,</span></span>
<span id="cb1-36"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  SAFE_DIVIDE(</span></span>
<span id="cb1-37"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">    SUM(CAST(kills AS INT64)) - SUM(CAST(attack_errors AS INT64)),</span></span>
<span id="cb1-38"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">    SUM(CAST(attack_attempts AS INT64))</span></span>
<span id="cb1-39"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  ) AS hitting_pct,</span></span>
<span id="cb1-40"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  COUNT(DISTINCT game_id) AS games</span></span>
<span id="cb1-41"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">FROM `maydaystats.ncaa_volleyball_men.boxscores`</span></span>
<span id="cb1-42"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">WHERE player_name != ''</span></span>
<span id="cb1-43"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">GROUP BY player_name, team</span></span>
<span id="cb1-44"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">HAVING attempts &gt;= 200</span></span>
<span id="cb1-45"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">ORDER BY kills DESC</span></span>
<span id="cb1-46"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">LIMIT 10</span></span>
<span id="cb1-47"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"""</span></span>
<span id="cb1-48"></span>
<span id="cb1-49">leaders <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> client.query(query).to_dataframe()</span>
<span id="cb1-50">leaders</span></code></pre></div>
<div id="fetch-kills-leaders" class="cell-output cell-output-display" data-execution_count="1">
<div>


<table class="dataframe caption-top table table-sm table-striped small" data-quarto-postprocess="true" data-border="1">
<thead>
<tr class="header">
<th data-quarto-table-cell-role="th"></th>
<th data-quarto-table-cell-role="th">player_name</th>
<th data-quarto-table-cell-role="th">team</th>
<th data-quarto-table-cell-role="th">kills</th>
<th data-quarto-table-cell-role="th">attempts</th>
<th data-quarto-table-cell-role="th">hitting_pct</th>
<th data-quarto-table-cell-role="th">games</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td data-quarto-table-cell-role="th">0</td>
<td>Cole Hartke</td>
<td>PEPPER</td>
<td>452</td>
<td>937</td>
<td>0.352188</td>
<td>29</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">1</td>
<td>Andrej Jokanovic</td>
<td>UC IRV</td>
<td>419</td>
<td>998</td>
<td>0.278557</td>
<td>30</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">2</td>
<td>Patrick Rogers</td>
<td>BALLST</td>
<td>414</td>
<td>786</td>
<td>0.399491</td>
<td>31</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">3</td>
<td>Justin Arrowood</td>
<td>CATAWB</td>
<td>397</td>
<td>938</td>
<td>0.210021</td>
<td>27</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">4</td>
<td>Sean Kelly</td>
<td>UCLA</td>
<td>396</td>
<td>753</td>
<td>0.398406</td>
<td>31</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">5</td>
<td>Sean Harvey</td>
<td>PENNST</td>
<td>388</td>
<td>806</td>
<td>0.328784</td>
<td>29</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">6</td>
<td>Zach Rama</td>
<td>UCLA</td>
<td>386</td>
<td>803</td>
<td>0.332503</td>
<td>30</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">7</td>
<td>Dillon Klein</td>
<td>USC</td>
<td>385</td>
<td>770</td>
<td>0.340260</td>
<td>27</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">8</td>
<td>Bryce Wetjen</td>
<td>MCKEND</td>
<td>375</td>
<td>813</td>
<td>0.258303</td>
<td>29</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">9</td>
<td>Aidan Feeney</td>
<td>EMMAN</td>
<td>374</td>
<td>880</td>
<td>0.244318</td>
<td>26</td>
</tr>
</tbody>
</table>

</div>
</div>
</div>
<p>Cole Hartke of Pepperdine led the country with 452 kills, on a 0.352 hitting percentage across 29 matches. UC Irvine’s Andrej Jokanovic finished a close second in raw kills, but needed about 60 more swings to get there, a volume-over-efficiency trade that shows up again later in his team’s championship match.</p>
</section>
<section id="the-top-ten-by-kills" class="level2">
<h2 class="anchored" data-anchor-id="the-top-ten-by-kills">The top ten, by kills</h2>
<div id="cell-fig-top-kills" class="cell" data-execution_count="2">
<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"><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-2"></span>
<span id="cb2-3">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;">9</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>))</span>
<span id="cb2-4">labels <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> leaders[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"player_name"</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;">" ("</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> leaders[<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> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">")"</span></span>
<span id="cb2-5">ax.barh(labels[::<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>], leaders[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"kills"</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>], 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;">"#2c3e50"</span>)</span>
<span id="cb2-6">ax.set_xlabel(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Kills"</span>)</span>
<span id="cb2-7">ax.set_title(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Top 10 Kills Leaders, 2025-26 Men's Season"</span>)</span>
<span id="cb2-8">ax.spines[[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"top"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"right"</span>]].set_visible(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">False</span>)</span>
<span id="cb2-9">plt.tight_layout()</span>
<span id="cb2-10">plt.show()</span></code></pre></div>
<div class="cell-output cell-output-display">
<div id="fig-top-kills" class="quarto-float quarto-figure quarto-figure-center anchored">
<figure class="quarto-float quarto-float-fig figure">
<div aria-describedby="fig-top-kills-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
<img src="https://maydaystats.com/posts/volleyball/2026-07-08-mens-year-in-review/index_files/figure-html/fig-top-kills-output-1.png" width="863" height="470" class="figure-img">
</div>
<figcaption class="quarto-float-caption-bottom quarto-float-caption quarto-float-fig" id="fig-top-kills-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
Figure&nbsp;1: Top 10 kills leaders, 2025-26 NCAA D1 men’s season (min. 200 attempts)
</figcaption>
</figure>
</div>
</div>
</div>
</section>
<section id="digs-blocks-and-aces" class="level2">
<h2 class="anchored" data-anchor-id="digs-blocks-and-aces">Digs, blocks, and aces</h2>
<div id="fetch-other-leaders" class="cell" data-execution_count="3">
<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">digs_query <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>
<span id="cb3-2"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">SELECT player_name, team, SUM(CAST(digs AS INT64)) AS total</span></span>
<span id="cb3-3"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">FROM `maydaystats.ncaa_volleyball_men.boxscores`</span></span>
<span id="cb3-4"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">WHERE player_name != ''</span></span>
<span id="cb3-5"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">GROUP BY player_name, team</span></span>
<span id="cb3-6"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">ORDER BY total DESC</span></span>
<span id="cb3-7"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">LIMIT 1</span></span>
<span id="cb3-8"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"""</span></span>
<span id="cb3-9">digs_leader <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> client.query(digs_query).to_dataframe()</span>
<span id="cb3-10"></span>
<span id="cb3-11">blocks_query <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>
<span id="cb3-12"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">SELECT player_name, team, SUM(CAST(total_blocks AS FLOAT64)) AS total</span></span>
<span id="cb3-13"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">FROM `maydaystats.ncaa_volleyball_men.boxscores`</span></span>
<span id="cb3-14"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">WHERE player_name != ''</span></span>
<span id="cb3-15"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">GROUP BY player_name, team</span></span>
<span id="cb3-16"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">ORDER BY total DESC</span></span>
<span id="cb3-17"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">LIMIT 1</span></span>
<span id="cb3-18"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"""</span></span>
<span id="cb3-19">blocks_leader <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> client.query(blocks_query).to_dataframe()</span>
<span id="cb3-20"></span>
<span id="cb3-21">aces_query <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>
<span id="cb3-22"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">SELECT player_name, team, SUM(CAST(service_aces AS INT64)) AS total</span></span>
<span id="cb3-23"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">FROM `maydaystats.ncaa_volleyball_men.boxscores`</span></span>
<span id="cb3-24"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">WHERE player_name != ''</span></span>
<span id="cb3-25"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">GROUP BY player_name, team</span></span>
<span id="cb3-26"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">ORDER BY total DESC</span></span>
<span id="cb3-27"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">LIMIT 1</span></span>
<span id="cb3-28"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"""</span></span>
<span id="cb3-29">aces_leader <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> client.query(aces_query).to_dataframe()</span></code></pre></div>
</div>
<p>Nico Paula (Lewis) led all players in digs with 264. Owen Rose (Penn State) topped the block chart with 156, well clear of the field. And Andrew Rowan (UCLA) led in aces with 57.</p>
</section>
<section id="the-championship-hawaii-over-uc-irvine" class="level2">
<h2 class="anchored" data-anchor-id="the-championship-hawaii-over-uc-irvine">The championship: Hawaii over UC Irvine</h2>
<div id="cell-fetch-final" class="cell" data-execution_count="4">
<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">final_query <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>
<span id="cb4-2"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">SELECT player_name, team, kills, attack_attempts, hitting_percentage, total_blocks</span></span>
<span id="cb4-3"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">FROM `maydaystats.ncaa_volleyball_men.boxscores`</span></span>
<span id="cb4-4"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">WHERE game_id = '6598557'</span></span>
<span id="cb4-5"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  AND CAST(kills AS INT64) &gt; 0</span></span>
<span id="cb4-6"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">ORDER BY team, CAST(kills AS INT64) DESC</span></span>
<span id="cb4-7"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"""</span></span>
<span id="cb4-8">final_box <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> client.query(final_query).to_dataframe()</span>
<span id="cb4-9">final_box</span></code></pre></div>
<div id="fetch-final" class="cell-output cell-output-display" data-execution_count="4">
<div>


<table class="dataframe caption-top table table-sm table-striped small" data-quarto-postprocess="true" data-border="1">
<thead>
<tr class="header">
<th data-quarto-table-cell-role="th"></th>
<th data-quarto-table-cell-role="th">player_name</th>
<th data-quarto-table-cell-role="th">team</th>
<th data-quarto-table-cell-role="th">kills</th>
<th data-quarto-table-cell-role="th">attack_attempts</th>
<th data-quarto-table-cell-role="th">hitting_percentage</th>
<th data-quarto-table-cell-role="th">total_blocks</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td data-quarto-table-cell-role="th">0</td>
<td>Kristian Titriyski</td>
<td>HAWAII</td>
<td>16</td>
<td>31</td>
<td>0.387</td>
<td>1</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">1</td>
<td>Adrien Roure</td>
<td>HAWAII</td>
<td>15</td>
<td>31</td>
<td>0.323</td>
<td>0</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">2</td>
<td>Louis Sakanoko</td>
<td>HAWAII</td>
<td>12</td>
<td>20</td>
<td>0.500</td>
<td>3</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">3</td>
<td>Justin Todd</td>
<td>HAWAII</td>
<td>7</td>
<td>10</td>
<td>0.500</td>
<td>1</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">4</td>
<td>Trevell Jordan</td>
<td>HAWAII</td>
<td>5</td>
<td>7</td>
<td>0.714</td>
<td>3</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">5</td>
<td>Tread Rosenthal</td>
<td>HAWAII</td>
<td>3</td>
<td>6</td>
<td>0.167</td>
<td>3</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">6</td>
<td>Andrej Jokanovic</td>
<td>UC IRV</td>
<td>13</td>
<td>35</td>
<td>0.229</td>
<td>3</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">7</td>
<td>William D'arcy</td>
<td>UC IRV</td>
<td>9</td>
<td>24</td>
<td>0.292</td>
<td>0</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">8</td>
<td>Andreas Brinck</td>
<td>UC IRV</td>
<td>6</td>
<td>18</td>
<td>0.111</td>
<td>1</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">9</td>
<td>Trevor Clark</td>
<td>UC IRV</td>
<td>6</td>
<td>12</td>
<td>0.333</td>
<td>6</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">10</td>
<td>Micah Goss</td>
<td>UC IRV</td>
<td>3</td>
<td>4</td>
<td>0.750</td>
<td>6</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">11</td>
<td>Cameron Kosty</td>
<td>UC IRV</td>
<td>1</td>
<td>2</td>
<td>.000</td>
<td>5</td>
</tr>
</tbody>
</table>

</div>
</div>
</div>
<p>Hawaii beat UC Irvine 3-1 on May 11, 2026 to win the national championship. Kristian Titriyski led the way with 16 kills on a .387 hitting percentage, with Adrien Roure adding 15 more. Louis Sakanoko went 12-for-20 (.500) with 3 blocks off the bench, the best hitting percentage of anyone with real volume in the match (two Hawaii players posted a higher percentage on much smaller sample sizes, 5-for-7 and 7-for-10).</p>
<p>UC Irvine’s offense leaned almost entirely on Andrej Jokanovic, the same volume hitter who finished second nationally in kills for the season. He had a team-high 13 kills in the final, but needed 35 attempts to get there, a .229 hitting percentage that never let UC Irvine pull away the way Hawaii’s more balanced, more efficient attack did.</p>
</section>
<section id="whats-next" class="level2">
<h2 class="anchored" data-anchor-id="whats-next">What’s next</h2>
<p>This covers the season at a high level: the national leaderboards and the championship match itself. The same table supports much narrower questions too, like a single team’s hitting efficiency by conference opponent, or how a freshman’s role grew over the season. Those are posts for another day, now that a full season of clean data is sitting in BigQuery.</p>
<p>The pipeline code behind this post <a href="https://github.com/maydaytek/maydaystats-pipelines">lives here</a>.</p>
<div class="callout callout-style-default callout-note callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Note
</div>
</div>
<div class="callout-body-container callout-body">
<p>Like the <a href="../../../baseball.html">baseball</a> and <a href="../../../hockey.html">hockey</a> posts, this one uses Quarto’s frozen execution (<code>freeze: auto</code>): the queries above ran once, locally, against BigQuery, and the deployed site reuses that committed output rather than re-querying on every build.</p>
</div>
</div>


</section>

 ]]></description>
  <category>Volleyball</category>
  <category>Men&#39;s</category>
  <category>Featured</category>
  <category>Season Recap</category>
  <guid>https://maydaystats.com/posts/volleyball/2026-07-08-mens-year-in-review/</guid>
  <pubDate>Wed, 08 Jul 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Year in Review: The 2025 NCAA Women’s Volleyball Season</title>
  <link>https://maydaystats.com/posts/volleyball/2026-07-08-womens-year-in-review/</link>
  <description><![CDATA[ 




<p>The women’s volleyball pipeline has now pulled a complete season: every Division I match from late August through the national championship in mid-December, backfilled game by game into BigQuery via the same self-hosted NCAA API proxy behind the men’s pipeline. This post is the first real analysis built on top of it.</p>
<p>One note on the data: a much smaller share of rows here (well under 1%) carry a blank player name, the same kind of NCAA-side gap described in the <a href="../../../posts/volleyball/2026-07-08-mens-year-in-review/index.html">men’s year in review</a>, not a pipeline bug.</p>
<section id="querying-the-season" class="level2">
<h2 class="anchored" data-anchor-id="querying-the-season">Querying the season</h2>
<div id="cell-fetch-kills-leaders" class="cell" data-execution_count="1">
<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;">from</span> google.cloud <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> bigquery</span>
<span id="cb1-2"></span>
<span id="cb1-3">client <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> bigquery.Client()</span>
<span id="cb1-4"></span>
<span id="cb1-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># NCAA's API returns team as its own code (e.g. "STFRPA"), not a school</span></span>
<span id="cb1-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># name. Tables and charts below keep the raw code as-is (matches how the</span></span>
<span id="cb1-7"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># hockey post's chart also uses raw team codes); this mapping is only</span></span>
<span id="cb1-8"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># used to spell out real school names in the prose. A code not in this</span></span>
<span id="cb1-9"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># dict falls back to showing the raw abbreviation rather than failing,</span></span>
<span id="cb1-10"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># in case a future re-render surfaces a school not covered yet.</span></span>
<span id="cb1-11">TEAM_NAMES <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> {</span>
<span id="cb1-12">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"MIAMI"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Miami"</span>,</span>
<span id="cb1-13">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"PITT"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Pittsburgh"</span>,</span>
<span id="cb1-14">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"PENNST"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Penn State"</span>,</span>
<span id="cb1-15">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"JMU"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"James Madison"</span>,</span>
<span id="cb1-16">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"WISC"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Wisconsin"</span>,</span>
<span id="cb1-17">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"STFRPA"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Saint Francis (PA)"</span>,</span>
<span id="cb1-18">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"PURDUE"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Purdue"</span>,</span>
<span id="cb1-19">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"SAC ST"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Sacramento State"</span>,</span>
<span id="cb1-20">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"UK"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Kentucky"</span>,</span>
<span id="cb1-21">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"TCU"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"TCU"</span>,</span>
<span id="cb1-22">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"UALR"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Little Rock"</span>,</span>
<span id="cb1-23">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"UT VAL"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Utah Valley"</span>,</span>
<span id="cb1-24">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"TX A&amp;M"</span>: <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Texas A&amp;M"</span>,</span>
<span id="cb1-25">}</span>
<span id="cb1-26"></span>
<span id="cb1-27"></span>
<span id="cb1-28"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> team_name(code):</span>
<span id="cb1-29">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> TEAM_NAMES.get(code, code)</span>
<span id="cb1-30"></span>
<span id="cb1-31"></span>
<span id="cb1-32">query <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>
<span id="cb1-33"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">SELECT</span></span>
<span id="cb1-34"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  player_name,</span></span>
<span id="cb1-35"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  team,</span></span>
<span id="cb1-36"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  SUM(CAST(kills AS INT64)) AS kills,</span></span>
<span id="cb1-37"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  SUM(CAST(attack_attempts AS INT64)) AS attempts,</span></span>
<span id="cb1-38"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  SAFE_DIVIDE(</span></span>
<span id="cb1-39"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">    SUM(CAST(kills AS INT64)) - SUM(CAST(attack_errors AS INT64)),</span></span>
<span id="cb1-40"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">    SUM(CAST(attack_attempts AS INT64))</span></span>
<span id="cb1-41"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  ) AS hitting_pct,</span></span>
<span id="cb1-42"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  COUNT(DISTINCT game_id) AS games</span></span>
<span id="cb1-43"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">FROM `maydaystats.ncaa_volleyball_women.boxscores`</span></span>
<span id="cb1-44"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">WHERE player_name != ''</span></span>
<span id="cb1-45"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">GROUP BY player_name, team</span></span>
<span id="cb1-46"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">HAVING attempts &gt;= 200</span></span>
<span id="cb1-47"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">ORDER BY kills DESC</span></span>
<span id="cb1-48"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">LIMIT 10</span></span>
<span id="cb1-49"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"""</span></span>
<span id="cb1-50"></span>
<span id="cb1-51">leaders <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> client.query(query).to_dataframe()</span>
<span id="cb1-52">leaders</span></code></pre></div>
<div id="fetch-kills-leaders" class="cell-output cell-output-display" data-execution_count="1">
<div>


<table class="dataframe caption-top table table-sm table-striped small" data-quarto-postprocess="true" data-border="1">
<thead>
<tr class="header">
<th data-quarto-table-cell-role="th"></th>
<th data-quarto-table-cell-role="th">player_name</th>
<th data-quarto-table-cell-role="th">team</th>
<th data-quarto-table-cell-role="th">kills</th>
<th data-quarto-table-cell-role="th">attempts</th>
<th data-quarto-table-cell-role="th">hitting_pct</th>
<th data-quarto-table-cell-role="th">games</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td data-quarto-table-cell-role="th">0</td>
<td>Flormarie Heredia Colon</td>
<td>MIAMI</td>
<td>687</td>
<td>1447</td>
<td>0.289565</td>
<td>28</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">1</td>
<td>Olivia Babcock</td>
<td>PITT</td>
<td>646</td>
<td>1383</td>
<td>0.334056</td>
<td>35</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">2</td>
<td>Kennedy Martin</td>
<td>PENNST</td>
<td>634</td>
<td>1423</td>
<td>0.319044</td>
<td>31</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">3</td>
<td>Kennedy Louisell</td>
<td>JMU</td>
<td>605</td>
<td>1436</td>
<td>0.300836</td>
<td>31</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">4</td>
<td>Mimi Colyer</td>
<td>WISC</td>
<td>598</td>
<td>1313</td>
<td>0.340442</td>
<td>33</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">5</td>
<td>Korrin Burns</td>
<td>STFRPA</td>
<td>588</td>
<td>1335</td>
<td>0.268165</td>
<td>29</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">6</td>
<td>Kenna Wollard</td>
<td>PURDUE</td>
<td>577</td>
<td>1383</td>
<td>0.261750</td>
<td>34</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">7</td>
<td>Victoria Marthaler</td>
<td>SAC ST</td>
<td>553</td>
<td>1440</td>
<td>0.213194</td>
<td>30</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">8</td>
<td>Eva Hudson</td>
<td>UK</td>
<td>546</td>
<td>1376</td>
<td>0.319041</td>
<td>33</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">9</td>
<td>Evan Hendrix</td>
<td>TCU</td>
<td>543</td>
<td>1440</td>
<td>0.241667</td>
<td>32</td>
</tr>
</tbody>
</table>

</div>
</div>
</div>
<p>Flormarie Heredia Colon of Miami led the country with 687 kills across 28 matches. Miami shows up twice in the national leaderboards this season: the same team that produced the kills leader also produced the national leader in aces, a rare two-way offensive season for one program.</p>
</section>
<section id="the-top-ten-by-kills" class="level2">
<h2 class="anchored" data-anchor-id="the-top-ten-by-kills">The top ten, by kills</h2>
<div id="cell-fig-top-kills" class="cell" data-execution_count="2">
<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"><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-2"></span>
<span id="cb2-3">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;">9</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>))</span>
<span id="cb2-4">labels <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> leaders[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"player_name"</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;">" ("</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> leaders[<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> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">")"</span></span>
<span id="cb2-5">ax.barh(labels[::<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>], leaders[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"kills"</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>], 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;">"#2c3e50"</span>)</span>
<span id="cb2-6">ax.set_xlabel(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Kills"</span>)</span>
<span id="cb2-7">ax.set_title(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Top 10 Kills Leaders, 2025 Women's Season"</span>)</span>
<span id="cb2-8">ax.spines[[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"top"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"right"</span>]].set_visible(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">False</span>)</span>
<span id="cb2-9">plt.tight_layout()</span>
<span id="cb2-10">plt.show()</span></code></pre></div>
<div class="cell-output cell-output-display">
<div id="fig-top-kills" class="quarto-float quarto-figure quarto-figure-center anchored">
<figure class="quarto-float quarto-float-fig figure">
<div aria-describedby="fig-top-kills-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
<img src="https://maydaystats.com/posts/volleyball/2026-07-08-womens-year-in-review/index_files/figure-html/fig-top-kills-output-1.png" width="866" height="470" class="figure-img">
</div>
<figcaption class="quarto-float-caption-bottom quarto-float-caption quarto-float-fig" id="fig-top-kills-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
Figure&nbsp;1: Top 10 kills leaders, 2025 NCAA D1 women’s season (min. 200 attempts)
</figcaption>
</figure>
</div>
</div>
</div>
</section>
<section id="digs-blocks-and-aces" class="level2">
<h2 class="anchored" data-anchor-id="digs-blocks-and-aces">Digs, blocks, and aces</h2>
<div id="fetch-other-leaders" class="cell" data-execution_count="3">
<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">digs_query <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>
<span id="cb3-2"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">SELECT player_name, team, SUM(CAST(digs AS INT64)) AS total</span></span>
<span id="cb3-3"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">FROM `maydaystats.ncaa_volleyball_women.boxscores`</span></span>
<span id="cb3-4"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">WHERE player_name != ''</span></span>
<span id="cb3-5"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">GROUP BY player_name, team</span></span>
<span id="cb3-6"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">ORDER BY total DESC</span></span>
<span id="cb3-7"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">LIMIT 1</span></span>
<span id="cb3-8"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"""</span></span>
<span id="cb3-9">digs_leader <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> client.query(digs_query).to_dataframe()</span>
<span id="cb3-10"></span>
<span id="cb3-11">blocks_query <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>
<span id="cb3-12"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">SELECT player_name, team, SUM(CAST(total_blocks AS FLOAT64)) AS total</span></span>
<span id="cb3-13"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">FROM `maydaystats.ncaa_volleyball_women.boxscores`</span></span>
<span id="cb3-14"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">WHERE player_name != ''</span></span>
<span id="cb3-15"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">GROUP BY player_name, team</span></span>
<span id="cb3-16"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">ORDER BY total DESC</span></span>
<span id="cb3-17"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">LIMIT 1</span></span>
<span id="cb3-18"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"""</span></span>
<span id="cb3-19">blocks_leader <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> client.query(blocks_query).to_dataframe()</span>
<span id="cb3-20"></span>
<span id="cb3-21">aces_query <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>
<span id="cb3-22"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">SELECT player_name, team, SUM(CAST(service_aces AS INT64)) AS total</span></span>
<span id="cb3-23"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">FROM `maydaystats.ncaa_volleyball_women.boxscores`</span></span>
<span id="cb3-24"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">WHERE player_name != ''</span></span>
<span id="cb3-25"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">GROUP BY player_name, team</span></span>
<span id="cb3-26"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">ORDER BY total DESC</span></span>
<span id="cb3-27"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">LIMIT 1</span></span>
<span id="cb3-28"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"""</span></span>
<span id="cb3-29">aces_leader <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> client.query(aces_query).to_dataframe()</span></code></pre></div>
</div>
<p>Andrea Roman (Little Rock) led all players in digs with 632. Bella Wooden of Utah Valley led the country in individual blocks with 188. And Ariana Rodriguez (Miami) topped the ace leaderboard with 77, the same program that produced the season’s kills leader.</p>
</section>
<section id="the-championship-texas-am-sweeps-kentucky" class="level2">
<h2 class="anchored" data-anchor-id="the-championship-texas-am-sweeps-kentucky">The championship: Texas A&amp;M sweeps Kentucky</h2>
<div id="cell-fetch-final" class="cell" data-execution_count="4">
<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">final_query <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>
<span id="cb4-2"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">SELECT player_name, team, kills, attack_attempts, hitting_percentage, digs</span></span>
<span id="cb4-3"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">FROM `maydaystats.ncaa_volleyball_women.boxscores`</span></span>
<span id="cb4-4"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">WHERE game_id = '6500718'</span></span>
<span id="cb4-5"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">  AND CAST(kills AS INT64) &gt; 0</span></span>
<span id="cb4-6"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">ORDER BY team, CAST(kills AS INT64) DESC</span></span>
<span id="cb4-7"><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"""</span></span>
<span id="cb4-8">final_box <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> client.query(final_query).to_dataframe()</span>
<span id="cb4-9">final_box</span></code></pre></div>
<div id="fetch-final" class="cell-output cell-output-display" data-execution_count="4">
<div>


<table class="dataframe caption-top table table-sm table-striped small" data-quarto-postprocess="true" data-border="1">
<thead>
<tr class="header">
<th data-quarto-table-cell-role="th"></th>
<th data-quarto-table-cell-role="th">player_name</th>
<th data-quarto-table-cell-role="th">team</th>
<th data-quarto-table-cell-role="th">kills</th>
<th data-quarto-table-cell-role="th">attack_attempts</th>
<th data-quarto-table-cell-role="th">hitting_percentage</th>
<th data-quarto-table-cell-role="th">digs</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td data-quarto-table-cell-role="th">0</td>
<td>Logan Lednicky</td>
<td>TX A&amp;M</td>
<td>11</td>
<td>32</td>
<td>0.250</td>
<td>7</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">1</td>
<td>Kyndal Stowers</td>
<td>TX A&amp;M</td>
<td>10</td>
<td>23</td>
<td>0.304</td>
<td>6</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">2</td>
<td>Ifenna Cos-Okpalla</td>
<td>TX A&amp;M</td>
<td>8</td>
<td>17</td>
<td>0.235</td>
<td>0</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">3</td>
<td>Emily Hellmuth</td>
<td>TX A&amp;M</td>
<td>6</td>
<td>18</td>
<td>0.167</td>
<td>3</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">4</td>
<td>Morgan Perkins</td>
<td>TX A&amp;M</td>
<td>3</td>
<td>9</td>
<td>0.333</td>
<td>1</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">5</td>
<td>Maddie Waak</td>
<td>TX A&amp;M</td>
<td>1</td>
<td>2</td>
<td>0.500</td>
<td>5</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">6</td>
<td>Eva Hudson</td>
<td>UK</td>
<td>13</td>
<td>45</td>
<td>0.200</td>
<td>4</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">7</td>
<td>Brooklyn DeLeye</td>
<td>UK</td>
<td>9</td>
<td>28</td>
<td>0.036</td>
<td>8</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">8</td>
<td>Asia Thigpen</td>
<td>UK</td>
<td>7</td>
<td>15</td>
<td>0.333</td>
<td>2</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">9</td>
<td>Lizzie Carr</td>
<td>UK</td>
<td>7</td>
<td>15</td>
<td>0.200</td>
<td>1</td>
</tr>
<tr class="odd">
<td data-quarto-table-cell-role="th">10</td>
<td>Kennedy Washington</td>
<td>UK</td>
<td>2</td>
<td>7</td>
<td>-0.286</td>
<td>1</td>
</tr>
<tr class="even">
<td data-quarto-table-cell-role="th">11</td>
<td>Kassie O'Brien</td>
<td>UK</td>
<td>2</td>
<td>4</td>
<td>0.500</td>
<td>6</td>
</tr>
</tbody>
</table>

</div>
</div>
</div>
<p>Texas A&amp;M swept Kentucky 3-0 on December 21, 2025 to win the national championship. Logan Lednicky led the Aggies with 11 kills on a .250 hitting percentage, with Kyndal Stowers adding 10 more on .304 hitting, a balanced attack across multiple hitters rather than one player carrying the offense.</p>
<p>Kentucky’s Eva Hudson, who finished among the national top ten in kills for the season, led all players in the match with 13 kills, but needed 45 attempts to get there, a .200 hitting percentage. By far the highest attempt count on the floor, it meant Kentucky’s offense ran through one hitter without the efficiency to match the volume, the same volume-over- efficiency pattern that showed up in the <a href="../../../posts/volleyball/2026-07-08-mens-year-in-review/index.html#the-championship-hawaii-over-uc-irvine">men’s championship match</a>.</p>
</section>
<section id="whats-next" class="level2">
<h2 class="anchored" data-anchor-id="whats-next">What’s next</h2>
<p>This covers the season at a high level: the national leaderboards and the championship match itself. The same table supports much narrower questions too, like how a team’s blocking efficiency changed after a lineup shift, or a transfer’s production before and after switching programs. Those are posts for another day, now that a full season of clean data is sitting in BigQuery.</p>
<p>The pipeline code behind this post <a href="https://github.com/maydaytek/maydaystats-pipelines">lives here</a>.</p>
<div class="callout callout-style-default callout-note callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Note
</div>
</div>
<div class="callout-body-container callout-body">
<p>Like the <a href="../../../baseball.html">baseball</a>, <a href="../../../hockey.html">hockey</a>, and <a href="../../../posts/volleyball/2026-07-08-mens-year-in-review/index.html">men’s volleyball</a> posts, this one uses Quarto’s frozen execution (<code>freeze: auto</code>): the queries above ran once, locally, against BigQuery, and the deployed site reuses that committed output rather than re-querying on every build.</p>
</div>
</div>


</section>

 ]]></description>
  <category>Volleyball</category>
  <category>Women&#39;s</category>
  <category>Featured</category>
  <category>Season Recap</category>
  <guid>https://maydaystats.com/posts/volleyball/2026-07-08-womens-year-in-review/</guid>
  <pubDate>Wed, 08 Jul 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Coursework Deep Dive: What Actually Predicts a Whiff</title>
  <link>https://maydaystats.com/posts/baseball/2026-06-16-purdue-grad505-pitch-analytics/</link>
  <description><![CDATA[ 




<p>Everything else on this site runs through a live pipeline: a Cloud Run Job pulls fresh data every day, loads it into BigQuery, and the posts query that table directly. This one is different, and worth flagging as different. It’s a project from GRAD 505 (Foundations in Data Science) in Purdue’s MS in Data Science program, built on a static Kaggle dataset rather than the site’s own Statcast pipeline: pitch-level data for every MLB regular-season game from 2015 through 2018, about 2.87 million pitches. No live queries here, just the analysis, with the full code, report, and slide deck linked at the bottom.</p>
<p>It’s included in Featured Research because the bar for that section is rigor, not whether the data happens to be live. This project ran three formal hypothesis tests, a linear regression, and a one-way ANOVA with post-hoc comparisons, and landed on a finding worth stating plainly: almost everything came back statistically significant, and almost none of it was large.</p>
<section id="three-research-questions" class="level2">
<h2 class="anchored" data-anchor-id="three-research-questions">Three research questions</h2>
<p>The first deliverable checked the data for major quality problems before any modeling started, a missing-values audit across all five joined tables, shown below by column. Nothing in that audit compromised any of the three analyses that follow.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://assets.maydaystats.com/fig1_missing_data.png" class="img-fluid figure-img"></p>
<figcaption>Missing data by column, across all five joined tables</figcaption>
</figure>
</div>
<p><strong>Does a pitcher’s pitch mix change the third time through the order?</strong> A chi-square test of independence on pitch type versus times through the order came back significant (chi-square = 41,249.35, df = 10, p &lt; 0.001), but Cramer’s V was 0.0895, a small effect by any standard cutoff. Pitchers do shift their mix as a game goes on, just not by much.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://assets.maydaystats.com/fig2_q1_pitch_type_tto.png" class="img-fluid figure-img"></p>
<figcaption>Pitch-type distribution by times through the order</figcaption>
</figure>
</div>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://assets.maydaystats.com/fig3_q1_table.png" class="img-fluid figure-img"></p>
<figcaption>Pitch-type share summary table</figcaption>
</figure>
</div>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://assets.maydaystats.com/fig7_q1_hypothesis_test.png" class="img-fluid figure-img"></p>
<figcaption>Chi-square test of independence: pitch type vs.&nbsp;times through the order</figcaption>
</figure>
</div>
<p><strong>Does spin rate on a four-seam fastball predict a whiff?</strong> A one-sided Mann-Whitney U test found whiffs came on pitches with a higher median spin rate (2,209.1 RPM) than pitches put in play (2,179.7 RPM), and the difference was significant (p = 8.82e-105). The rank-biserial effect size was 0.0462, again small. A follow-up OLS regression of whiff rate on average four-seam spin rate (n = 928 pitchers with at least 100 four-seamers) found a significant positive slope (0.000016 per RPM, p = 1.82e-06), but the model explained just 2.4% of the variance in whiff rate (R-squared = 0.0243). Spin rate matters, but it’s one input among many.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://assets.maydaystats.com/fig4_q2_spinrate_boxplot.png" class="img-fluid figure-img"></p>
<figcaption>Four-seam spin rate distribution, whiffs vs.&nbsp;balls in play</figcaption>
</figure>
</div>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://assets.maydaystats.com/fig5_q2_spinrate_whiff.png" class="img-fluid figure-img"></p>
<figcaption>Whiff rate by four-seam spin rate bin</figcaption>
</figure>
</div>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://assets.maydaystats.com/fig8_q2_hypothesis_test.png" class="img-fluid figure-img"></p>
<figcaption>Mann-Whitney U test: four-seam spin rate, whiffs vs.&nbsp;balls in play</figcaption>
</figure>
</div>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://assets.maydaystats.com/fig11_regression_spin_whiff.png" class="img-fluid figure-img"></p>
<figcaption>OLS regression: whiff rate vs.&nbsp;average four-seam spin rate</figcaption>
</figure>
</div>
<p><strong>Does average fastball velocity predict strike-zone rate in high-leverage innings (7th-9th)?</strong> This is the one question where the data didn’t clear the bar. Pearson correlation between average four-seam velocity and zone percentage came back essentially flat (r = 0.0379, p = 0.276, n = 830 pitchers with at least 50 late-inning fastballs), so this test failed to reject the null. A secondary Spearman rank correlation was technically significant (rho = 0.0727, p = 0.036), but at that size it’s not a finding to build anything on.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://assets.maydaystats.com/fig6_q3_velo_zone.png" class="img-fluid figure-img"></p>
<figcaption>Average four-seam velocity vs.&nbsp;zone percentage, high-leverage innings</figcaption>
</figure>
</div>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://assets.maydaystats.com/fig9_q3_hypothesis_test.png" class="img-fluid figure-img"></p>
<figcaption>Pearson correlation: velocity vs.&nbsp;zone percentage, with 95% CI</figcaption>
</figure>
</div>
</section>
<section id="the-velocity-creep-that-is-real-and-tiny" class="level2">
<h2 class="anchored" data-anchor-id="the-velocity-creep-that-is-real-and-tiny">The velocity creep that is real, and tiny</h2>
<p>The final analysis ran a one-way ANOVA on four-seam fastball velocity across all four seasons in the dataset:</p>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Season</th>
<th>n</th>
<th>Mean (mph)</th>
<th>Std</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>2015</td>
<td>249,755</td>
<td>92.48</td>
<td>2.80</td>
</tr>
<tr class="even">
<td>2016</td>
<td>257,649</td>
<td>92.58</td>
<td>2.75</td>
</tr>
<tr class="odd">
<td>2017</td>
<td>253,387</td>
<td>93.22</td>
<td>2.79</td>
</tr>
<tr class="even">
<td>2018</td>
<td>254,086</td>
<td>93.17</td>
<td>2.75</td>
</tr>
</tbody>
</table>
<p>The ANOVA rejected the null outright (F = 4,942.80, p &lt; 0.001), and every pairwise comparison came back significant under a Bonferroni correction, including 2017 versus 2018, a 0.057 mph difference. League-average fastball velocity really did climb across this window, about three quarters of a mile per hour from 2015 to 2017 before leveling off. It’s a real trend. It’s also a small one, and the effect size confirms it: eta squared came out to 0.0144, meaning season explains under 2% of the variation in any single pitch’s velocity.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://assets.maydaystats.com/fig12_anova_velocity_by_season.png" class="img-fluid figure-img"></p>
<figcaption>Four-seam fastball velocity by season, with post-hoc pairwise significance</figcaption>
</figure>
</div>
</section>
<section id="whiff-rate-by-pitch-type" class="level2">
<h2 class="anchored" data-anchor-id="whiff-rate-by-pitch-type">Whiff rate by pitch type</h2>
<p>One more result worth surfacing on its own, a 95% Wilson confidence interval on whiff rate for each pitch type across the full 2.87 million pitches:</p>
<table class="caption-top table">
<thead>
<tr class="header">
<th>Pitch</th>
<th>Whiff rate</th>
<th>95% CI</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Changeup (CH)</td>
<td>14.2%</td>
<td>14.1% - 14.4%</td>
</tr>
<tr class="even">
<td>Slider (SL)</td>
<td>14.2%</td>
<td>14.1% - 14.3%</td>
</tr>
<tr class="odd">
<td>Cutter (FC)</td>
<td>10.7%</td>
<td>10.6% - 10.9%</td>
</tr>
<tr class="even">
<td>Knuckle-curve (KC)</td>
<td>10.0%</td>
<td>9.8% - 10.3%</td>
</tr>
<tr class="odd">
<td>Curveball (CU)</td>
<td>9.4%</td>
<td>9.2% - 9.5%</td>
</tr>
<tr class="even">
<td>Four-seam (FF)</td>
<td>7.9%</td>
<td>7.8% - 7.9%</td>
</tr>
<tr class="odd">
<td>Sinker (SI)</td>
<td>5.8%</td>
<td>5.7% - 5.9%</td>
</tr>
<tr class="even">
<td>Two-seam (FT)</td>
<td>5.5%</td>
<td>5.4% - 5.6%</td>
</tr>
</tbody>
</table>
<p>At this sample size the confidence intervals are tight enough that every pitch type is distinguishable from every other, changeups and sliders generate whiffs at roughly the same rate and both clear fastballs by nearly double.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://assets.maydaystats.com/fig10_ci_whiff_by_pitch_type.png" class="img-fluid figure-img"></p>
<figcaption>Whiff rate by pitch type, with 95% Wilson confidence intervals</figcaption>
</figure>
</div>
</section>
<section id="why-this-belongs-next-to-the-pipeline-posts" class="level2">
<h2 class="anchored" data-anchor-id="why-this-belongs-next-to-the-pipeline-posts">Why this belongs next to the pipeline posts</h2>
<p>The throughline across all five analyses is the same: with millions of pitches, almost any real difference clears the p &lt; 0.05 bar, so the question that actually matters is how big the effect is, not whether it exists. That’s a distinction worth being explicit about on a site built around real data, and it’s the main thing this project set out to demonstrate.</p>
<p>Full write-up, slide deck, and all analysis code:</p>
<ul>
<li><a href="https://github.com/maydaytek/GRAD_505_PROJECT">GRAD_505_PROJECT on GitHub</a></li>
<li><a href="https://github.com/maydaytek/GRAD_505_PROJECT/blob/main/BMAY_FINAL_PROJECT_REPORT.pdf">Written report (PDF)</a></li>
<li><a href="https://github.com/maydaytek/GRAD_505_PROJECT/blob/main/BMAY_FINAL_PROJECT_PRESENTATION.pdf">Presentation slides (PDF)</a></li>
</ul>
<div class="callout callout-style-default callout-note callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Note
</div>
</div>
<div class="callout-body-container callout-body">
<p>Unlike the pipeline-driven posts on this site, this one isn’t frozen against a live BigQuery table, there’s no BigQuery table behind it. The numbers above are transcribed from the project’s own output files (<code>OUTPUTS/hypothesis_results.txt</code> and <code>OUTPUTS/final_project_results.txt</code> in the repo linked above), generated from a static Kaggle dataset rather than this site’s own pipeline.</p>
</div>
</div>


</section>

 ]]></description>
  <category>Baseball</category>
  <category>Featured</category>
  <category>Academic Project</category>
  <guid>https://maydaystats.com/posts/baseball/2026-06-16-purdue-grad505-pitch-analytics/</guid>
  <pubDate>Tue, 16 Jun 2026 00:00:00 GMT</pubDate>
</item>
</channel>
</rss>
