28  Statistics in the Wild: The Sports Page Way

Everything up to here was training. You have the tools now - distributions, estimation, the linear model, reliability, resampling, Bayes, and the craft of showing data honestly. What no chapter can hand you is judgment: which tool, when, and why - and, just as important, when not to reach for the fancy one. Judgment is learned the way any craft is learned: by watching someone work, seeing the decisions, and hearing why they went one way and not the other. That is what this last part of the book is - an apprenticeship. We stop explaining tools in the abstract and start using them in public, on real projects, with every choice laid bare.

We begin with the friendliest lab I know: The Sports Page, a sister project to this book - a daily newsletter that takes one strange sports number and tells the truth about what it does and does not mean. Sports is a good teacher because the data are rich, the stakes are low, and the future arrives every single day to grade your predictions. If you can be honest about a baseball number, you can be honest about a clinical one.

28.1 Three Things You Can Do With Data

The whole discipline reduces to three verbs, and the first rule is to always know which one you are doing:

  1. Describe what happened. This is easy - the data already know the answer. A batting average, a group mean, an \(R^2\): all description.
  2. Predict what will happen. This is harder, because the future is a real test you can fail. A forecast, a fitted model applied to new cases.
  3. Control why it happens. This is hardest - it requires a mechanism, a cause, not just a curve that fits.

Most sports writing stops at #1 and pretends it is doing #2. Most analytics stops at #2 and pretends it is doing #3. The single most useful habit you can build is to label your own work honestly: this is description, this is prediction, this is a causal claim I have not earned.

ImportantThe past is cheating

Here is the idea that separates a describer from a forecaster: any model flexible enough will fit the past perfectly, and that tells you almost nothing. Fitting the past is easy because you already know how it turned out. The real test is predicting data you have not seen, and then acknowledging it when you were wrong. That is why The Sports Page grades every prediction it makes, on a schedule, in public. Being wrong is not failure; it is the information that improves the next model. If you never publish a prediction that could turn out wrong, you never give yourself a chance to learn.

28.2 A Worked Judgment: The Hot Start Is a Mirage

Every April, someone goes 6-for-9 and gets talked about as an MVP. Every April, that call turns out to be premature, and you already have the tools to know why. It is the same lesson as statistical power: small samples are noisy. Watch what happens when we plot every hitter’s performance against how many chances they have had:

NoteWorking in SPSS, Julia, or Python?

This chapter uses data/mlb_hitters_2026.csv directly - no setup needed. See Getting the Book’s Data if you want the setup files for your language.

library(tidyverse)    # dplyr, ggplot2, purrr, tibble, readr, stringr, forcats
source("_common.R")   # book-wide helpers: round2(), fmt_p(), tidy2()

h <- read_csv("data/mlb_hitters_2026.csv", show_col_types = FALSE)  # a partial season

league_median <- h |> filter(PA >= 200) |> summarise(m = median(OPS)) |> pull(m)

ggplot(h, aes(PA, OPS)) +
  geom_point(alpha = 0.35, color = "steelblue") +
  geom_hline(yintercept = league_median, color = "firebrick", linewidth = 0.8) +
  labs(x = "Plate appearances", y = "OPS") +
  theme_book()
Figure 28.1: OPS vs. plate appearances. Every extreme lives on the left — where the sample is tiny.
* OPS against plate appearances, with a reference line at the league median
* among hitters with 200+ PA.
* GET DATA reads columns POSITIONALLY, so every column in the file has to be
* named - listing only PA and OPS would silently read the first two columns
* (pid and name) under those names instead.
GET DATA /TYPE=TXT /FILE='data/mlb_hitters_2026.csv'
  /DELIMITERS=',' /QUALIFIER='"' /FIRSTCASE=2
  /VARIABLES=pid F16.6 name A24 team A24 PA F16.6 AB F16.6 H F16.6 HR F16.6
             BB F16.6 SO F16.6 AVG F16.6 OBP F16.6 SLG F16.6 OPS F16.6.

* First find the median among the qualified hitters.
TEMPORARY.
SELECT IF (PA >= 200).
FREQUENCIES VARIABLES=OPS /STATISTICS=MEDIAN.

GRAPH /SCATTERPLOT(BIVAR) = PA WITH OPS.
   Statistics
+---------+---+
|         |OPS|
+---------+---+
|N Valid  |182|
|  Missing|  0|
+---------+---+
|Median   |.75|
+---------+---+

                                OPS
+--------------+---------+-------+-------------+------------------+
|              |Frequency|Percent|Valid Percent|Cumulative Percent|
+--------------+---------+-------+-------------+------------------+
|Valid .516000 |        1|    .5%|          .5%|               .5%|
|      .535000 |        1|    .5%|          .5%|              1.1%|
|      .573000 |        1|    .5%|          .5%|              1.6%|
|      .584000 |        1|    .5%|          .5%|              2.2%|
|      .585000 |        1|    .5%|          .5%|              2.7%|
|      .589000 |        1|    .5%|          .5%|              3.3%|
|      .591000 |        1|    .5%|          .5%|              3.8%|
|      .593000 |        1|    .5%|          .5%|              4.4%|
|      .597000 |        1|    .5%|          .5%|              4.9%|
|      .599000 |        1|    .5%|          .5%|              5.5%|
|      .602000 |        1|    .5%|          .5%|              6.0%|
|      .603000 |        1|    .5%|          .5%|              6.6%|
|      .608000 |        1|    .5%|          .5%|              7.1%|
|      .609000 |        1|    .5%|          .5%|              7.7%|
|      .611000 |        2|   1.1%|         1.1%|              8.8%|
|      .612000 |        1|    .5%|          .5%|              9.3%|
|      .613000 |        2|   1.1%|         1.1%|             10.4%|
|      .616000 |        1|    .5%|          .5%|             11.0%|
|      .622000 |        2|   1.1%|         1.1%|             12.1%|
|      .624000 |        1|    .5%|          .5%|             12.6%|
|      .627000 |        1|    .5%|          .5%|             13.2%|
|      .631000 |        1|    .5%|          .5%|             13.7%|
|      .635000 |        2|   1.1%|         1.1%|             14.8%|
|      .637000 |        1|    .5%|          .5%|             15.4%|
|      .641000 |        1|    .5%|          .5%|             15.9%|
|      .646000 |        1|    .5%|          .5%|             16.5%|
|      .647000 |        1|    .5%|          .5%|             17.0%|
|      .649000 |        2|   1.1%|         1.1%|             18.1%|
|      .657000 |        1|    .5%|          .5%|             18.7%|
|      .660000 |        2|   1.1%|         1.1%|             19.8%|
|      .661000 |        1|    .5%|          .5%|             20.3%|
|      .662000 |        1|    .5%|          .5%|             20.9%|
|      .664000 |        3|   1.6%|         1.6%|             22.5%|
|      .667000 |        3|   1.6%|         1.6%|             24.2%|
|      .669000 |        1|    .5%|          .5%|             24.7%|
|      .670000 |        2|   1.1%|         1.1%|             25.8%|
|      .674000 |        1|    .5%|          .5%|             26.4%|
|      .682000 |        1|    .5%|          .5%|             26.9%|
|      .684000 |        1|    .5%|          .5%|             27.5%|
|      .686000 |        3|   1.6%|         1.6%|             29.1%|
|      .687000 |        1|    .5%|          .5%|             29.7%|
|      .690000 |        1|    .5%|          .5%|             30.2%|
|      .693000 |        1|    .5%|          .5%|             30.8%|
|      .697000 |        1|    .5%|          .5%|             31.3%|
|      .702000 |        1|    .5%|          .5%|             31.9%|
|      .703000 |        2|   1.1%|         1.1%|             33.0%|
|      .704000 |        2|   1.1%|         1.1%|             34.1%|
|      .708000 |        2|   1.1%|         1.1%|             35.2%|
|      .710000 |        1|    .5%|          .5%|             35.7%|
|      .714000 |        1|    .5%|          .5%|             36.3%|
|      .715000 |        1|    .5%|          .5%|             36.8%|
|      .716000 |        1|    .5%|          .5%|             37.4%|
|      .720000 |        2|   1.1%|         1.1%|             38.5%|
|      .721000 |        1|    .5%|          .5%|             39.0%|
|      .722000 |        1|    .5%|          .5%|             39.6%|
|      .723000 |        2|   1.1%|         1.1%|             40.7%|
|      .729000 |        2|   1.1%|         1.1%|             41.8%|
|      .730000 |        1|    .5%|          .5%|             42.3%|
|      .732000 |        1|    .5%|          .5%|             42.9%|
|      .733000 |        2|   1.1%|         1.1%|             44.0%|
|      .734000 |        1|    .5%|          .5%|             44.5%|
|      .738000 |        2|   1.1%|         1.1%|             45.6%|
|      .739000 |        2|   1.1%|         1.1%|             46.7%|
|      .740000 |        1|    .5%|          .5%|             47.3%|
|      .741000 |        1|    .5%|          .5%|             47.8%|
|      .743000 |        1|    .5%|          .5%|             48.4%|
|      .744000 |        1|    .5%|          .5%|             48.9%|
|      .747000 |        2|   1.1%|         1.1%|             50.0%|
|      .748000 |        1|    .5%|          .5%|             50.5%|
|      .749000 |        1|    .5%|          .5%|             51.1%|
|      .751000 |        1|    .5%|          .5%|             51.6%|
|      .753000 |        1|    .5%|          .5%|             52.2%|
|      .754000 |        1|    .5%|          .5%|             52.7%|
|      .755000 |        1|    .5%|          .5%|             53.3%|
|      .757000 |        1|    .5%|          .5%|             53.8%|
|      .760000 |        1|    .5%|          .5%|             54.4%|
|      .762000 |        1|    .5%|          .5%|             54.9%|
|      .765000 |        1|    .5%|          .5%|             55.5%|
|      .766000 |        1|    .5%|          .5%|             56.0%|
|      .767000 |        1|    .5%|          .5%|             56.6%|
|      .771000 |        1|    .5%|          .5%|             57.1%|
|      .772000 |        1|    .5%|          .5%|             57.7%|
|      .773000 |        1|    .5%|          .5%|             58.2%|
|      .778000 |        1|    .5%|          .5%|             58.8%|
|      .781000 |        1|    .5%|          .5%|             59.3%|
|      .784000 |        1|    .5%|          .5%|             59.9%|
|      .786000 |        1|    .5%|          .5%|             60.4%|
|      .787000 |        3|   1.6%|         1.6%|             62.1%|
|      .789000 |        2|   1.1%|         1.1%|             63.2%|
|      .791000 |        1|    .5%|          .5%|             63.7%|
|      .792000 |        1|    .5%|          .5%|             64.3%|
|      .793000 |        1|    .5%|          .5%|             64.8%|
|      .794000 |        1|    .5%|          .5%|             65.4%|
|      .795000 |        2|   1.1%|         1.1%|             66.5%|
|      .796000 |        1|    .5%|          .5%|             67.0%|
|      .797000 |        1|    .5%|          .5%|             67.6%|
|      .798000 |        2|   1.1%|         1.1%|             68.7%|
|      .801000 |        1|    .5%|          .5%|             69.2%|
|      .802000 |        2|   1.1%|         1.1%|             70.3%|
|      .803000 |        1|    .5%|          .5%|             70.9%|
|      .804000 |        1|    .5%|          .5%|             71.4%|
|      .807000 |        1|    .5%|          .5%|             72.0%|
|      .809000 |        1|    .5%|          .5%|             72.5%|
|      .810000 |        1|    .5%|          .5%|             73.1%|
|      .817000 |        1|    .5%|          .5%|             73.6%|
|      .819000 |        1|    .5%|          .5%|             74.2%|
|      .822000 |        1|    .5%|          .5%|             74.7%|
|      .823000 |        1|    .5%|          .5%|             75.3%|
|      .824000 |        1|    .5%|          .5%|             75.8%|
|      .825000 |        1|    .5%|          .5%|             76.4%|
|      .828000 |        1|    .5%|          .5%|             76.9%|
|      .832000 |        2|   1.1%|         1.1%|             78.0%|
|      .834000 |        1|    .5%|          .5%|             78.6%|
|      .835000 |        1|    .5%|          .5%|             79.1%|
|      .841000 |        1|    .5%|          .5%|             79.7%|
|      .842000 |        1|    .5%|          .5%|             80.2%|
|      .843000 |        1|    .5%|          .5%|             80.8%|
|      .844000 |        4|   2.2%|         2.2%|             83.0%|
|      .845000 |        2|   1.1%|         1.1%|             84.1%|
|      .850000 |        1|    .5%|          .5%|             84.6%|
|      .851000 |        1|    .5%|          .5%|             85.2%|
|      .853000 |        1|    .5%|          .5%|             85.7%|
|      .855000 |        2|   1.1%|         1.1%|             86.8%|
|      .861000 |        1|    .5%|          .5%|             87.4%|
|      .864000 |        1|    .5%|          .5%|             87.9%|
|      .865000 |        1|    .5%|          .5%|             88.5%|
|      .878000 |        1|    .5%|          .5%|             89.0%|
|      .882000 |        1|    .5%|          .5%|             89.6%|
|      .883000 |        1|    .5%|          .5%|             90.1%|
|      .895000 |        1|    .5%|          .5%|             90.7%|
|      .905000 |        2|   1.1%|         1.1%|             91.8%|
|      .908000 |        1|    .5%|          .5%|             92.3%|
|      .913000 |        1|    .5%|          .5%|             92.9%|
|      .914000 |        1|    .5%|          .5%|             93.4%|
|      .915000 |        1|    .5%|          .5%|             94.0%|
|      .926000 |        1|    .5%|          .5%|             94.5%|
|      .930000 |        1|    .5%|          .5%|             95.1%|
|      .931000 |        1|    .5%|          .5%|             95.6%|
|      .932000 |        1|    .5%|          .5%|             96.2%|
|      .935000 |        1|    .5%|          .5%|             96.7%|
|      .938000 |        1|    .5%|          .5%|             97.3%|
|      .953000 |        1|    .5%|          .5%|             97.8%|
|      .964000 |        1|    .5%|          .5%|             98.4%|
|      .976000 |        1|    .5%|          .5%|             98.9%|
|      .998000 |        1|    .5%|          .5%|             99.5%|
|      1.092000|        1|    .5%|          .5%|            100.0%|
+--------------+---------+-------+-------------+------------------+
|Total         |      182| 100.0%|             |                  |
+--------------+---------+-------+-------------+------------------+
using CSV, DataFrames, Statistics, Plots
h = CSV.read("data/mlb_hitters_2026.csv", DataFrame)   # a partial season

league_median = median(h.OPS[h.PA .>= 200])

scatter(h.PA, h.OPS, alpha = 0.35, legend = false,
        xlabel = "Plate appearances", ylabel = "OPS",
        title = "The funnel: extremes only survive at small samples")
hline!([league_median], color = :firebrick, linewidth = 2)

import pandas as pd
import matplotlib.pyplot as plt

h = pd.read_csv("data/mlb_hitters_2026.csv")           # a partial season

league_median = h.loc[h.PA >= 200, "OPS"].median()

plt.scatter(h.PA, h.OPS, alpha=0.35, color="steelblue")
plt.axhline(league_median, color="firebrick", linewidth=2)
plt.xlabel("Plate appearances"); plt.ylabel("OPS")
plt.title("The funnel: extremes only survive at small samples")
plt.show()
<matplotlib.collections.PathCollection object at 0x7fd33f5b2540>
<matplotlib.lines.Line2D object at 0x7fd33f0b95e0>
Text(0.5, 0, 'Plate appearances')
Text(0, 0.5, 'OPS')
Text(0.5, 1.0, 'The funnel: extremes only survive at small samples')

The plot is a funnel. On the right, where hitters have 200+ chances, everyone clusters near the league median (the red line). On the left, where a hitter has batted a handful of times, OPS ranges from near zero to the stratosphere. The numbers make it stark:

h |>
  summarise(
    sd_OPS_under_20_PA = sd(OPS[PA < 20]),      # tiny samples: huge spread
    sd_OPS_over_200_PA = sd(OPS[PA >= 200]),    # big samples: tight
    best_OPS           = max(OPS),
    its_plate_appearances = PA[which.max(OPS)]
  ) |>
  round2()
sd_OPS_under_20_PA sd_OPS_over_200_PA best_OPS its_plate_appearances
0.59 0.1 3.25 4
GET DATA /TYPE=TXT /FILE='data/mlb_hitters_2026.csv'
  /DELIMITERS=',' /QUALIFIER='"' /FIRSTCASE=2
  /VARIABLES=pid F16.6 name A24 team A24 PA F16.6 AB F16.6 H F16.6 HR F16.6 BB F16.6 SO F16.6 AVG F16.6 OBP F16.6 SLG F16.6 OPS F16.6.
* Spread of OPS at small vs large samples, and the season's best mark.
TEMPORARY.
SELECT IF (PA < 20).
DESCRIPTIVES VARIABLES=OPS /STATISTICS=STDDEV.

TEMPORARY.
SELECT IF (PA >= 200).
DESCRIPTIVES VARIABLES=OPS /STATISTICS=STDDEV.

* The best OPS and the plate appearances behind it.
SORT CASES BY OPS (D).
LIST VARIABLES=OPS PA /CASES=1.
      Descriptive Statistics
+--------------------+--+-------+
|                    | N|Std Dev|
+--------------------+--+-------+
|OPS                 |64|    .59|
|Valid N (listwise)  |64|       |
|Missing N (listwise)| 0|       |
+--------------------+--+-------+

      Descriptive Statistics
+--------------------+---+-------+
|                    | N |Std Dev|
+--------------------+---+-------+
|OPS                 |182|    .10|
|Valid N (listwise)  |182|       |
|Missing N (listwise)|  0|       |
+--------------------+---+-------+

     Data List
+--------+--------+
|   OPS  |   PA   |
+--------+--------+
|3.250000|4.000000|
+--------+--------+
using CSV, DataFrames, Statistics, Plots
h = CSV.read("data/mlb_hitters_2026.csv", DataFrame)

using Statistics
std(h.OPS[h.PA .< 20])        # tiny samples: huge spread
std(h.OPS[h.PA .>= 200])      # big samples: tight

best = argmax(h.OPS)
h.OPS[best], h.PA[best]       # the best mark, and how few chances made it
0.5896091385485137
0.1019636849192731
(3.25, 4)
import pandas as pd
import matplotlib.pyplot as plt
h = pd.read_csv("data/mlb_hitters_2026.csv")

h.loc[h.PA < 20, "OPS"].std()      # tiny samples: huge spread
h.loc[h.PA >= 200, "OPS"].std()   # big samples: tight

best = h.OPS.idxmax()
h.loc[best, ["OPS", "PA"]]        # the best mark, and how few chances made it
np.float64(0.5896091385485139)
np.float64(0.1019636849192731)
OPS    3.25
PA     4.00
Name: 0, dtype: float64

The best OPS in the league belongs to someone with a single-digit number of at-bats. That is not a hitter who has cracked the code; that is mostly sampling noise in a very small sample. The honest read: a hot start is description of a tiny sample, and mistaking it for prediction is one of the most common errors in sports, and in science more broadly. The player will “regress to the mean” not because of some mysterious force, but because the extreme value was mostly luck, and luck does not carry over to the rest of the season.

This is the entire apprenticeship in one figure. The math was easy (a standard deviation). The judgment, recognizing that the April headline is describing noise while treating it as signal, is the hard part, and the valuable one.

28.3 Describe, Predict, Control - On One Trend

Return to the strikeout revolution. Notice how the three verbs stack:

  • Describe: strikeouts per nine innings rose from about 5.4 to about 8.5 since 1985. (True, easy, and only a description.)
  • Predict: will it keep rising? You could fit a line and extrapolate, but now you owe the reader a graded forecast, and a cautious one, because trends bend. Extrapolation is where flexible models most often go wrong.
  • Control: why did it rise? Harder velocity, analytics rewarding the home-run-or-strikeout trade-off, bigger bullpens. Those are causal claims, and a line through the past cannot establish any of them; it can only describe the pattern they leave behind.

Same data, three very different epistemic commitments. Keeping them straight is not pedantry; it is the difference between careful analysis and guessing.

28.4 What Comes Next

With the philosophy in hand, we turn to the real thing: published research, walked through end to end, with every decision on the table. You will watch measures get built and defended, watch a total score throw away information it should have kept, and watch missing data quietly change an answer. These are not toy examples; they are real studies, real datasets, and real judgment calls, some of which we would make differently today. That honesty is the point. Welcome to the apprenticeship; let’s go to work.