The Zero Conditional Mean Assumption
What it says
The assumption is written \(E(u \mid X) = 0\), and it is the one that makes OLS trustworthy. In words:
- Pick any value of \(X\) — say \(X = 8\) — and look at the unobserved errors \(u\) for all the observations that have it. Their average is zero.
- The same is true at \(X = 2\), at \(X = 5\), everywhere. In every slice of \(X\), the errors average to zero.
- It is assumption SLR.4, and it is the reason \(\hat{\beta}_1\) lands on the truth.
Why it is the whole ballgame
Recall from the sampling distribution of OLS that
\[\hat{\beta}_1 = \beta_1 + \sum_i w_i u_i, \qquad w_i = \frac{x_i - \bar{x}}{SST_x}.\]
Take expectations. The bias is \(\sum_i w_i \, E(u_i \mid X)\).
- If \(E(u \mid X) = 0\), every term is zero, so \(E(\hat{\beta}_1) = \beta_1\) — unbiased.
- If \(E(u \mid X)\) is not flat — if the errors drift with \(X\) — that bias term survives, and \(\hat{\beta}_1\) is systematically off, no matter how much data you have.
The classic example: wages and ability
Take \(\text{wage} = \beta_0 + \beta_1 \, \text{educ} + u\), where \(u\) holds everything else that affects wages — including ability.
- \(E(u \mid \text{educ}) = 0\) would require the average ability to be the same for people with 8 years of schooling as for people with 16.
- That is almost certainly false: people who get more education tend to have higher ability, so \(E(u \mid \text{educ})\) rises with \(\text{educ}\).
- The assumption fails, and \(\hat{\beta}_1\) quietly soaks up part of ability’s effect. That is omitted variable bias, seen from the error side.
See it
The left panel is the usual scatter with the true line (slope 1) and the OLS line. The right panel is the thing the assumption is actually about: the errors \(u\) plotted against \(X\), with the conditional mean \(E(u \mid X)\) drawn in red. Slide the tilt: at 0 the red line is flat on zero and OLS sits right on the truth; tilt it and the red line slopes, the assumption breaks, and the OLS slope drifts away by exactly that tilt.
#| standalone: true
#| viewerHeight: 560
library(shiny)
set.seed(7)
n <- 120
x <- runif(n, 0, 10)
base_noise <- rnorm(n, sd = 1.5)
ui <- fluidPage(
tags$head(tags$style(HTML("
.eq-box { background:#f0f4f8; border-radius:6px; padding:14px;
margin-top:14px; font-size:14px; line-height:1.8; }
.eq-box b { color:#2c3e50; }
.bad { color:#e74c3c; font-weight:bold; }
.ok { color:#27ae60; font-weight:bold; }
"))),
sidebarLayout(
sidebarPanel(
width = 3,
sliderInput("g", "Tilt of E(u | X):", min = -1.2, max = 1.2, value = 0, step = 0.1),
helpText("0 = the assumption holds: errors average to zero in every slice of X. Tilt it and E(u|X) slopes with X."),
uiOutput("box")
),
mainPanel(
width = 9,
fluidRow(
column(6, plotOutput("fit", height = "380px")),
column(6, plotOutput("uplot", height = "380px"))
)
)
)
)
server <- function(input, output, session) {
d <- reactive({
g <- input$g
u <- g * (x - 5) + base_noise
y <- 2 + 1 * x + u
b <- coef(lm(y ~ x))
list(y = y, u = u, b0 = b[1], b1 = b[2], g = g)
})
output$fit <- renderPlot({
s <- d(); par(mar = c(4.5, 4.5, 3, 1))
plot(x, s$y, pch = 19, col = "#95a5a6",
main = "Data: true line vs OLS line", xlab = "X", ylab = "Y")
abline(a = 2, b = 1, col = "#27ae60", lwd = 2, lty = 2)
abline(a = s$b0, b = s$b1, col = "#2c3e50", lwd = 2.5)
legend("topleft", bty = "n", cex = 0.9,
legend = c("true line (slope 1)", "OLS line"),
col = c("#27ae60", "#2c3e50"), lwd = c(2, 2.5), lty = c(2, 1))
})
output$uplot <- renderPlot({
s <- d(); par(mar = c(4.5, 4.5, 3, 1))
plot(x, s$u, pch = 19, col = adjustcolor("#2980b9", 0.5),
main = "Errors u vs X: is E(u | X) flat?", xlab = "X", ylab = "error u")
abline(h = 0, col = "#7f8c8d", lty = 3)
abline(a = -5 * s$g, b = s$g, col = "#e74c3c", lwd = 2.5)
legend("topleft", bty = "n", cex = 0.9, legend = "E(u | X)",
col = "#e74c3c", lwd = 2.5)
})
output$box <- renderUI({
s <- d(); bias <- s$b1 - 1
cls <- if (abs(bias) < 0.12) "ok" else "bad"
lab <- if (abs(bias) < 0.12) "holds" else "violated"
tags$div(class = "eq-box",
HTML(sprintf(
"<b>Assumption:</b> <span class='%s'>%s</span><br><br><b>True β<sub>1</sub>:</b> 1.00<br><b>OLS β̂<sub>1</sub>:</b> %.2f<br><b>Bias:</b> <span class='%s'>%.2f</span>",
cls, lab, s$b1, cls, bias)))
})
}
shinyApp(ui, server)
When the red \(E(u \mid X)\) line is flat, the errors carry no information about \(X\) and OLS is unbiased. When it slopes, part of what looks like the effect of \(X\) is really the errors moving with \(X\) — and OLS cannot tell the two apart.
Connections
- Sampling Distribution of OLS — where \(\hat{\beta}_1 = \beta_1 + \sum_i w_i u_i\) comes from; this assumption is what zeroes the bias term.
- Omitted Variable Bias — the same failure named from the other side: a left-out variable sitting in \(u\) that correlates with \(X\).
- Gauss-Markov & Gaussian Assumptions — where SLR.4 sits among the full set.