Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

This notebook contains the model presented in a talk at PyData Boston 2025.

Title: The SAT Math Gap: Gender Difference or Selection Bias?

Abstract

Why do male test takers consistently score about 30 points higher than female test takers on the mathematics section of the SAT? Does this reflect an actual difference in math ability, or is it an artifact of selection bias—if young men with low math ability are less likely to take the test than young women with the same ability?

This talk presents a Bayesian model that estimates how much of the observed difference can be explained by selection effects. We will walk through a complete Bayesian workflow, including prior elicitation with PreliZ, model building in PyMC, and validation with ArviZ, showing how Bayesian methods disentangle latent traits from observed outcomes and separate signal from noise.

No prior knowledge of Bayesian statistics is required; attendees should be familiar with Python and common probability distributions.

Overview

For decades, male test takers have outperformed female test takers on the SAT math section by about 30 points. This outcome could reflect a real difference in ability, or it could be explained by selection bias if boys with weaker math skills are less likely to take the SAT than girls with comparable skills.

I present a generative Bayesian model that explicitly incorporates this selection mechanism and estimates the fraction of the observed gap attributable to bias. The talk emphasizes workflow over theory: how to build, validate, and interpret Bayesian models using PyMC, ArviZ, and PreliZ.

Setup

On Colab, install the packages that aren’t in Colab’s Python. The pins keep the stack on PyMC 5 and ArviZ 0.x, the versions this analysis was run with: newer nutpie and PreliZ would upgrade them and break Colab’s PyMC.

SAT Data

Boys who take the SAT get higher math scores than girls, and the difference is consistent. In data published by the College Board for 2011-2015, the differences were 31, 34, 32, 31, and 31 points.

Reasons for this difference are a topic of debate. They include

  • Boys are better at math, on average.

  • Boys are better at taking the SAT, so the difference in scores overstates the difference in efficacy.

  • Boys who are bad at math are less likely to take the SAT than girls with the same efficacy -- due to this selection bias, the difference in scores overstates the difference in efficacy.

These articles are examples of the debate:

If it’s true that boys with low math efficacy are less likely to take the SAT, we should see evidence of this effect in the distributions of scores, and we may be able to estimate the magnitude of the effect, and what fraction of the difference in scores we observe.

We used a PDF parser to extract the data from the PDF files. The results are available to download in a Zip file.

And use the following function to read them.

The following dictionary maps from each year (2011-2015) to a Pandas DataFrame.

Here are the total number of test takers in each year.

Total 1647123 Male 770605 Female 876518 dtype: int64
Total 1664479 Male 778142 Female 886337 dtype: int64
Total 1660047 Male 776092 Female 883955 dtype: int64
Total 1672395 Male 783570 Female 888825 dtype: int64
Total 1698521 Male 794802 Female 903719 dtype: int64

In every year, substantially more girls than boys took the SAT, which is evidence that the selection effect is stronger for boys.

TODO: Get data on the number of boys and girls alive at test-taking age and the number who were enrolled in high school.

The following function converts the number of test-takers with each score into a normalized PMF.

Comparing the distribution of scores across years, we can see that the location and spread of the distributions appears consistent.

<Figure size 450x262.5 with 1 Axes>

And likewise for female test takers.

<Figure size 450x262.5 with 1 Axes>

It is notable that the distributions are spiky. These perturbations are the result of the way SAT scores are computed. During 2011-2015 the math section of the SAT typically had 44 multiple choice questions and 10 grid questions. For the multiple-choice questions, incorrect answers were penalized by 1/4 point. In each year, some combinations of correct, incorrect, and blank responses were more likely than others. As a result, the PMF for each year shows a pattern of perturbations that is consistent for male and female test-takers, as shown in the following plots.

Male=531.0491886245221, Female=499.6034993006419, Diff=31.44568932388023
<Figure size 450x262.5 with 1 Axes>
Male=532.3130225588645, Female=498.5023529425038, Diff=33.81066961636071
<Figure size 450x262.5 with 1 Axes>
Male=530.910871391536, Female=499.4024808955207, Diff=31.508390496015352
<Figure size 450x262.5 with 1 Axes>
Male=529.823844710747, Female=498.5317582201221, Diff=31.292086490624968
<Figure size 450x262.5 with 1 Axes>
Male=527.1939174788187, Female=495.92306900706967, Diff=31.27084847174899
<Figure size 450x262.5 with 1 Axes>

It is also notable that the extreme scores (200 and 800) are overrepresented, which suggests that test scores don’t reflect the full range of efficacy.

We’ll use the following function later to plot the relationship between a potential score and the probability of taking the SAT.

Simple Model

As a first step, we’ll build a model that takes data from one year and estimates:

  • The mean and standard deviation of a latent distribution of efficacy. We’ll use “efficacy” to mean the ability to answer test questions correctly, which involves some combination of aptitude, achievement, and acumen.

  • The parameters of a selection function that relates efficacy to the probability of taking the test.

  • The perturbations of the distribution of test scores.

To model the excess of scores at the extremes, we’ll assume that the latent distribution of efficacy extends from 100 to 900 -- that is, beyond the range of SAT scores by 100 points at both ends.

array([100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200, 210, 220, 230, 240, 250, 260, 270, 280, 290, 300, 310, 320, 330, 340, 350, 360, 370, 380, 390, 400, 410, 420, 430, 440, 450, 460, 470, 480, 490, 500, 510, 520, 530, 540, 550, 560, 570, 580, 590, 600, 610, 620, 630, 640, 650, 660, 670, 680, 690, 700, 710, 720, 730, 740, 750, 760, 770, 780, 790, 800, 810, 820, 830, 840, 850, 860, 870, 880, 890, 900])

We’ll use these masks to select the center part of the distribution, with valid SAT scores, and the tails.

array([200, 210, 220, 230, 240, 250, 260, 270, 280, 290, 300, 310, 320, 330, 340, 350, 360, 370, 380, 390, 400, 410, 420, 430, 440, 450, 460, 470, 480, 490, 500, 510, 520, 530, 540, 550, 560, 570, 580, 590, 600, 610, 620, 630, 640, 650, 660, 670, 680, 690, 700, 710, 720, 730, 740, 750, 760, 770, 780, 790, 800])

To fit the model, we’ll center and scale the scores to the range from -3 to 3.

array([-300, -290, -280, -270, -260, -250, -240, -230, -220, -210, -200, -190, -180, -170, -160, -150, -140, -130, -120, -110, -100, -90, -80, -70, -60, -50, -40, -30, -20, -10, 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200, 210, 220, 230, 240, 250, 260, 270, 280, 290, 300])
(-3.0, 3.0)

As an example, we’ll run the model for a single year and sex.

Note: We’ll use the “sex” to refer to the binary categories reported by the College Board. These categories may not reflect the gender identity of the test takers.

Score 200 2456 210 1071 220 995 230 1426 240 1779 ... 760 2004 770 10434 780 2673 790 2563 800 11098 Name: Male, Length: 61, dtype: int64
794802

The parameter beta estimates the strength of the selection effect. As a prior, we’ll use a gamma distribution, which is positive valued, reflecting the expectation that if a selection effect exists, people with higher scores are more likely to take the test.

But the upper bound is moderately tight -- it represents some skepticism about the strength of the selection bias.

{'alpha': 4.300178987723759, 'beta': 83.84212820172407}
<Figure size 450x262.5 with 1 Axes>

For the standard deviation of efficacy, we also use a gamma distribution, with a high-density interval wide enough to cover all likely values.

{'alpha': 463.6083812559706, 'beta': 4.2172887180676595}
<Figure size 450x262.5 with 1 Axes>

sigma_noise controls the magnitude of the perturbations to the distribution of efficacy. The following prior is weakly informative,

{'alpha': 4.30010837792882, 'beta': 27.947108661773417}
<Figure size 450x262.5 with 1 Axes>

Now here’s the model.

We use a ZeroSumNormal so the perturbations can shift mass around but cannot add or remove mass overall.

Then we remove any linear trend in the perturbations to prevent them from absorbing the shape that should be attributed to the selection function.

Here’s a sample from the prior distributions.

Sampling: [beta, log_noise_raw, mu, obs_bins, sigma, sigma_noise]
<Figure size 450x262.5 with 1 Axes>
<Figure size 450x262.5 with 1 Axes>

And here’s how we fit the model.

Loading...
Loading...

The diagnostics are not perfect, but good enough for the current purposes: the posterior predictive check and parameter recovery test.

Loading...
<Figure size 900x450 with 6 Axes>

Here are the posterior distributions.

<Figure size 1035x301.875 with 2 Axes>
<Figure size 450x262.5 with 1 Axes>
<Figure size 450x757.5 with 1 Axes>
<Figure size 450x262.5 with 1 Axes>
<Figure size 450x262.5 with 1 Axes>

Here’s what the selection function looks like, indicating the relative probability of taking the test for a range of potential scores. These probabilities are scaled so the midpoint is 50%. The left side indicates how much less likely someone is to take the test if their potential score is low.

<Figure size 450x262.5 with 1 Axes>

To see how reliable this estimate is, let’s run a parameter recovery test.

Posterior predictive check

Sampling: [obs_bins]
Loading...
Loading...
(61, 8000)
(61,)
<Figure size 450x262.5 with 1 Axes>

Parameter recovery

We’ll use the following function to generate data given known parameters.

As an example, we’ll test it with parameters based on the previous example.

Sampling: [obs_bins]
array([ 1994, 1008, 877, 1371, 1719, 1464, 1691, 3247, 2528, 4434, 6754, 5600, 7998, 7722, 8718, 11482, 14182, 13467, 15109, 15871, 18048, 21667, 24307, 23823, 23111, 25708, 26144, 32311, 24941, 24441, 30497, 28573, 26949, 25047, 23842, 28191, 25056, 18696, 21282, 17346, 17731, 18486, 16857, 17627, 13541, 12452, 11780, 11601, 9814, 9366, 8111, 4134, 5371, 3926, 3686, 4755, 1069, 5569, 1274, 1241, 4393])

And we can confirm that the generated data resembles the actual data it is based on.

527.1939174788187
<Figure size 450x262.5 with 1 Axes>

Now let’s run the model on the generated data.

Loading...
Loading...

The diagnostics are good enough -- although some of the effective sample sizes are small.

Loading...

Ideally, the true values should fall within the posterior HDIs. The estimate of mu is a little low, but the HDI covers the true value.

<Figure size 450x262.5 with 1 Axes>

The estimate of sigma is a little high.

<Figure size 450x262.5 with 1 Axes>

And the estimate of beta is a little high, but the HDI covers the true value.

<Figure size 450x262.5 with 1 Axes>

The parameter recovery test is pretty good.

But with data from only a single year, we can’t be confident in the estimates, in part because the selection parameter and the perturbations compete to explain the shape of the distribution. That’s why we need a hierarchical model.

Hierarchical

A hierarchical model makes it possible to share information between years and sexes. In particular, recall that the distribution of scores appears to be different between sexes but consistent from year to year. And the perturbations in the distribution of scores appear to be different from year to year but consistent between sexes. The hierarchical model can take advantage of these consistencies.

We’ll keep the parameters of the selection model unpooled, so we estimate values separately for each combination of year and sex.

First let’s get the data into shape for the hierarchical model.

(5, 2, 61)
array([[770605, 876518], [778142, 886337], [776092, 883955], [783570, 888825], [794802, 903719]])

We’ll define coordinates for the various dimensions, which makes the model easier to read and validate.

Here’s the hierarchical model.

Loading...
Loading...

We’ll save the trace so we can run additional analysis later without running the model again.

The diagnostics are good -- r_hats are 1.0 across the board, and the effective sample sizes are sufficient.

Loading...
<Figure size 900x600 with 8 Axes>
<Figure size 450x2587.5 with 1 Axes>

Here are the posterior distributions.

<Figure size 450x262.5 with 1 Axes>

The posterior means of the mu parameters are substantially closer together than the 31-34 point difference seen in the mean test scores.

<Figure size 450x262.5 with 1 Axes>

The estimated standard deviation is considerably higher for males.

<Figure size 1552.5x1207.5 with 12 Axes>

Here are the estimated selection functions for male test-takers. They are consistent from year to year, and show a strong selection effect.

<Figure size 450x262.5 with 1 Axes>

Here are the estimated selection functions for female test-takers. They are reasonably consistent from year to year, and show a much weaker selection effect.

<Figure size 450x262.5 with 1 Axes>

If we generate a sample from the posterior predictive distribution, we can confirm that the score distributions are similar to the observed data.

Sampling: [obs_bins]
Loading...
Loading...
(5, 2, 61, 4000)
(5, 2, 61)
<Figure size 450x262.5 with 1 Axes>
<Figure size 450x262.5 with 1 Axes>
<Figure size 450x262.5 with 1 Axes>
<Figure size 450x262.5 with 1 Axes>
<Figure size 450x262.5 with 1 Axes>
<Figure size 450x262.5 with 1 Axes>
<Figure size 450x262.5 with 1 Axes>
<Figure size 450x262.5 with 1 Axes>
<Figure size 450x262.5 with 1 Axes>
<Figure size 450x262.5 with 1 Axes>
<Figure size 1350x450 with 10 Axes>

These results depend strongly on the assumption that the latent distribution of efficacy is Gaussian -- if the distribution of scores is skewed, that is taken to imply a selection effect. We can’t rule out the possibility that the underlying distribution is truly skewed.

The results are also sensitive to a free parameter of the model, the amount of deviation from a normal distribution that can be attributed to the quirks of SAT scoring. We have set this parameter to the lowest value that allows the model to reproduce the quirks without obscuring the effects we are interested in: selection bias and differences in the latent distributions for males and females.

So we should probably not take the estimated selection functions too seriously. For example, the selection functions for males suggest that someone with the efficacy to score 800 is 2-3 times more likely to take the SAT than someone whose expected score is 200. It’s hard to say whether that’s realistic.

However, the results of the hierarchical model provide strong evidence that:

  • The selection effect is substantially stronger for males than females, and

  • The difference in selection effects could explain a large part of the observed difference in scores between male and female test-takers.

While the difference in scores is consistently 31-34 points, the difference in average efficacy might be only 8 points.

SelectionSAT

Copyright 2025 Allen Downey

Code license: MIT License

Text license: CC BY-NC-SA 4.0