---
title: "Gender wage gaps: Oaxaca-Blinder and causal estimation"
author: "Xiang Ao"
date: "2025-11-17"
---
```{r}
#| label: setup
#| include: false
library(tidyverse)
library(broom)
library(marginaleffects)
library(npcausal)
```
## Oaxaca-Blinder as regression adjustment
The Oaxaca-Blinder (OB) decomposition separates the difference in mean outcomes into a part associated with observed covariates and a part associated with different regression coefficients:
$$E[Y \mid A = 1] - E[Y \mid A = 0] = \underbrace{(E[X \mid A = 1] - E[X \mid A = 0]) \beta_0}_{\text{Explained (Covariate Differences)}} + \underbrace{E[X \mid A = 1] (\beta_1 - \beta_0)}_{\text{Unexplained (Coefficient Differences)}}$$
Here $A=1$ denotes female and $A=0$ denotes male. The decomposition depends on which group supplies the reference coefficients, $\beta_0$ or $\beta_1$.
Słoczyński (2013, 2015) connects this decomposition to potential outcomes. If we demean the covariates at the overall sample mean, the coefficient gives the population average gender effect ($\tau_{\text{PAGE}}$), which corresponds to the ATE. If we demean at the female-sample means, it gives the average effect for women ($\tau_{\text{PATT}}$), which corresponds to the ATT.
Under this interpretation, the unexplained gap is the treatment effect averaged over the chosen covariate distribution. The explained part is $E[Y(0)\mid A=1]-E[Y(0)\mid A=0]$, the selection term in the potential-outcomes decomposition. This causal reading requires the linear model and a meaningful intervention attached to $A$. Without them, the explained part is only a descriptive composition difference. We can estimate the same quantities with regression adjustment or with a doubly robust estimator.
## 2015 CPS data
I use a subsample from the March 2015 U.S. Current Population Survey. It contains 5,150 white, non-Hispanic, full-time workers aged 25--64. The outcome is log hourly wage (`lwage`), and `female` is the group indicator.
There are two qualifications. First, gender is not manipulable in Holland's (1986) sense. I interpret `female` as a proxy for being perceived and treated as female by an employer, which is closer to an intervention. Second, the extract contains only full-year, full-time, non-self-employed workers. Restricting the sample conditions on employment, which can be affected by sex and by unobserved wage determinants. This opens a collider path that covariate adjustment cannot close. We would need data on non-workers instead (Lee, 2009; Olivetti and Petrongolo, 2008). If employed women are positively selected on wage-raising unobservables, the gaps below understate the population gap.
```{r}
#| label: ra1
#| cache: true
#| message: false
#| warning: false
data <- read_csv("wage2015_subsample_inference.csv") |>
rename(socl = scl, sohs = shs, sout = so) |>
mutate(
exp_dm = exp1 - mean(exp1, na.rm = TRUE),
female = sex,
occ2 = factor(occ2),
ind2 = factor(ind2),
sohs_dm = sohs - mean(sohs, na.rm = TRUE),
hsg_dm = hsg - mean(hsg, na.rm = TRUE),
socl_dm = socl - mean(socl, na.rm = TRUE),
clg_dm = clg - mean(clg, na.rm = TRUE),
mw_dm = mw - mean(mw, na.rm = TRUE),
sout_dm = sout - mean(sout, na.rm = TRUE),
we_dm = we - mean(we, na.rm = TRUE)
)
# 1. Unadjusted (raw) difference
reg1 <- lm(lwage ~ female, data = data)
tidy(reg1) |> filter(term == "female")
# 2. Regression adjustment with demeaned human capital & region controls
reg2 <- lm(lwage ~ female * (exp_dm + sohs_dm + hsg_dm + socl_dm + clg_dm + mw_dm + sout_dm + we_dm),
data = data)
tidy(reg2) |> filter(term == "female")
```
### Why adjustment widens the gap
The raw wage gap is $-3.8\%$ ($p = 0.012$). After controlling for potential experience, education, and region, the estimated gap widens to $-11.3\%$ ($p < 0.001$).
Here adjustment nearly triples the gap. We can see why by comparing the covariate means:
```{r}
#| label: composition
#| cache: true
data |>
group_by(female) |>
summarise(
n = n(),
mean_lwage = mean(lwage),
exp1 = mean(exp1),
`<HS` = mean(sohs),
HS = mean(hsg),
`Some Coll` = mean(socl),
College = mean(clg),
Advanced = mean(ad)
) |>
mutate(across(where(is.numeric), ~round(.x, 3))) |>
knitr::kable(caption = "Covariate means by gender.")
```
Women in this sample have more education than men: 17.5% have advanced degrees, compared with 10.7% of men, while 1.3% have less than high school, compared with 3.2% of men. Average potential experience is about 13.8 years in both groups. Because education raises wages, the raw comparison partly hides the gap. Holding education fixed gives the larger $-11.3\%$ gap within education-experience cells.
### Regression adjustment with `marginaleffects`
Instead of manual demeaning, the same estimands can be obtained directly using `marginaleffects::avg_comparisons()`:
```{r}
#| label: ra2
#| cache: true
reg3 <- lm(lwage ~ female * (exp1 + sohs + hsg + socl + clg + mw + sout + we), data = data)
# Population Average Gender Effect (ATE / PAGE)
avg_comparisons(reg3, variables = "female")
# Female Subpopulation Gender Effect (ATT / PATT)
avg_comparisons(reg3, variables = "female", newdata = subset(data, female == 1))
```
Both the ATE and ATT are $-11.3\%$. In this sample, averaging the fully interacted OLS model over either covariate distribution gives the same result as the OB decomposition.
## Should we control for occupation and industry?
Pay-gap regressions often adjust for occupation and industry. Let's see what happens here:
```{r}
#| label: ra4
#| cache: true
reg4 <- lm(lwage ~ female * (exp1 + sohs + hsg + socl + clg + mw + sout + we + occ2 + ind2),
data = data)
avg_comparisons(reg4, variables = "female", newdata = subset(data, female == 1))
```
Adjusting for 22 occupation and industry categories narrows the gap from $-11.3\%$ to $-7.5\%$.
### Different controls answer different questions
If we take sex as the treatment, there is no earlier common cause to adjust for, so $Y(a)\perp A$ holds without conditioning. Adding controls changes the estimand rather than improving identification. The models below answer different questions; they are not progressively better estimates of one quantity.
| Model | Held fixed | Question answered | Estimate |
|---|---|---|---|
| `reg1` | nothing | Total effect of the gender signal | $-3.8\%$ |
| `reg3` | education, experience, region | Gap within education-experience cells | $-11.3\%$ |
| `reg4` | + occupation, industry | Gap within occupation cells too | $-7.5\%$ |
Occupation and industry are post-treatment ($A \to \text{Occupation} \to Y$), and they are also a common effect of gender and of unobserved wage determinants $U$ such as career preferences or flexibility constraints. Conditioning on them blocks the mediating path and at the same time opens $A \to \text{Occupation} \leftarrow U \to Y$, which was closed before (Elwert and Winship, 2014; Cinelli, Forney and Pearl, 2022).
The four-point move from $-11.3\%$ to $-7.5\%$ therefore cannot be read as occupational sorting accounting for a third of the gap. Three things are mixed into it and nothing in the output separates them: genuine mediation, collider bias from the gender-$U$ association that conditioning induces, and ordinary confounding of the occupation-wage relationship. The direction of the second can be signed. If women who enter male-dominated, high-paying occupations are positively selected on $U$, then within those occupations they are better on unobservables than the men they are compared with, which pushes the within-occupation gap toward zero. Part of the attenuation may be bias rather than sorting.
Calling $-7.5\%$ a controlled direct effect is licensed only under no unmeasured mediator-outcome confounding (VanderWeele and Robinson, 2014). $U$ is exactly such a confounder, and in a wage setting it is the reason this literature exists. Report the number as the within-occupation adjusted gap and state the condition that would upgrade it to a CDE.
## A nonparametric AIPW estimate
The regression adjustment still depends on its functional form. To make that part more flexible, I use augmented inverse probability weighting (AIPW) with SuperLearner through `npcausal` (Kennedy, 2022). The learner library includes `earth`, `glmnet`, `ranger`, and `xgboost`.
```{r}
#| label: npcausal1
#| warning: false
#| message: false
library(npcausal)
# To avoid prolonged cross-fitting during interactive compilation, we load the
# pre-computed SuperLearner results (SL.earth, SL.glmnet, SL.mean, SL.ranger, SL.xgboost)
# from the book cache if present; otherwise, compute from scratch.
cache_base <- "gwg_cache/html/npcausal1_5538101f2b9ff4f8b21593068dc74ef6"
if (file.exists(paste0(cache_base, ".rdx"))) {
cache_env <- new.env()
lazyLoad(cache_base, envir = cache_env)
aipw_ate <- cache_env$aipw_ate
aipw_att <- cache_env$aipw_att
} else {
SL.library <- c("SL.earth", "SL.glmnet", "SL.mean", "SL.ranger", "SL.xgboost")
reg3 <- lm(lwage ~ exp_dm + sohs + hsg + socl + clg + mw + sout + we + occ2 + ind2, data = data)
W <- as.data.frame(model.matrix(reg3)[, -1])
Y <- data$lwage
A <- data$female
set.seed(123)
aipw_ate <- ate(y = Y, a = A, x = W, nsplits = 5, sl.lib = SL.library)
aipw_att <- att(y = Y, a = A, x = W, nsplits = 5, sl.lib = SL.library)
}
```
```{r}
#| label: npcausal-results
#| warning: false
#| message: false
aipw_ate$res
aipw_att$res
```
### Comparing the estimates
The AIPW estimate of the ATT is $-6.7\%$ ($95\%\text{ CI}: [-9.5\%,-3.9\%]$), close to the interacted OLS estimate of $-7.5\%$. This suggests that the linear model is a reasonable approximation to the same conditional mean. AIPW only relaxes the functional-form assumption. It does not solve post-treatment adjustment, selection into the sample, or the interpretation of gender as a treatment.
## What the estimates mean
The raw gap is $-3.8\%$. Holding education, experience, and region fixed gives $-11.3\%$, partly because women in this sample have more education. Adding occupation and industry gives a within-occupation gap of $-7.5\%$; it is a controlled direct effect only if there is no unmeasured occupation-wage confounding. AIPW gives $-6.7\%$, so the within-occupation result is not driven mainly by the linear functional form. None of these methods solves selection into full-time employment, and no single number should be called *the* gender wage gap without saying which question it answers.