<?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. Every post is backed by a real, automated data pipeline - pulling live data into published analysis, with the code and methodology shown alongside the writing.</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>Wed, 08 Jul 2026 00:00:00 GMT</lastBuildDate>
<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: true</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>Featured</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>
</channel>
</rss>
