Answer
What does it mean to control for a variable in regression?
The short answer
You usually do not split the data into subgroups. You put the control variables into a regression model alongside the predictor you care about. The predictor's coefficient then uses only the part of its variation that the controls cannot explain, so it estimates the association between predictor and outcome among people with the same values of the controls. It removes confounding by measured variables only, and only as well as the model describes them.
The short answer
Suppose you want to know whether study time raises exam scores, but stronger students both study more and score higher anyway. A plain comparison of hours and scores mixes the two influences together. Controlling for prior ability means asking a narrower question: among students with the same prior grade, do the ones who study more score higher?
The intuitive way to do that is to sort people into groups that match on the control variable and compare within each group. That works for one or two coarse variables, but with several controls the number of cells explodes and most of them end up with one person or none. Multiple regression gets the same kind of answer without building cells: you fit score ~ hours + prior, and the coefficient on hours is the expected change in score for one more hour of study with the prior grade held fixed. The model borrows strength across everyone by assuming a simple form for how each variable relates to the outcome (a straight line, unless you say otherwise).
What the model actually does
There is a precise way to see what 'held fixed' means, known as the Frisch-Waugh-Lovell theorem. The adjusted coefficient on hours is exactly what you get from three simple steps:
- Regress hours on the control variables and keep the residuals: the part of study time that prior grade cannot predict.
- Regress the outcome on the same controls and keep those residuals: the part of the score that prior grade cannot predict.
- Regress the second set of residuals on the first. The slope is identical to the coefficient in the full model.
So controlling does not compare two particular people with equal incomes or equal grades. It strips out of both variables whatever the controls account for, then looks at whether what is left over still moves together. With many controls the logic is the same; the first two steps just use all of them at once. This is also why a predictor that is almost completely explained by the controls gets a large standard error: very little independent variation remains to learn from.
See it in R and Python
The example simulates 2,000 students. Prior grade raises both study hours and the exam score, and the true effect of one extra hour is exactly 1 point. It compares the naive slope, the adjusted slope, the same slope rebuilt from residuals, and a subgroup version that controls for prior grade using five bands. The Python version fits the same models with NumPy least squares.
R
set.seed(17336)
n <- 2000
prior <- rnorm(n, 70, 10) # confounder: prior grade
hours <- 2 + 0.1 * prior + rnorm(n, sd = 1.5) # better students study more
score <- 20 + 1.0 * hours + 0.6 * prior + rnorm(n, sd = 5) # true effect of hours = 1
naive <- lm(score ~ hours)
adjusted <- lm(score ~ hours + prior)
# 1. Slope of hours without and with the control variable
round(c(naive = coef(naive)[["hours"]], adjusted = coef(adjusted)[["hours"]]), 3)
# 2. The same adjusted slope by hand: remove what prior explains from both
r_hours <- resid(lm(hours ~ prior))
r_score <- resid(lm(score ~ prior))
round(coef(lm(r_score ~ r_hours))[["r_hours"]], 3)
# 3. The subgroup approach: five bands of prior grade
band <- cut(prior, quantile(prior, 0:5 / 5), include.lowest = TRUE)
round(coef(lm(score ~ hours + band))[["hours"]], 3)
# Inference for the adjusted slope
round(summary(adjusted)$coefficients, 3)
round(confint(adjusted)["hours", ], 2)
round(summary(adjusted)$r.squared, 3)Python
import numpy as np
import pandas as pd
from scipy import stats
rng = np.random.default_rng(17336)
n = 2000
prior = rng.normal(70, 10, n) # confounder: prior grade
hours = 2 + 0.1 * prior + rng.normal(0, 1.5, n) # better students study more
score = 20 + 1.0 * hours + 0.6 * prior + rng.normal(0, 5, n) # true effect of hours = 1
def ols(X, y):
X = np.column_stack([np.ones(len(y)), X]) # add the intercept
b, *_ = np.linalg.lstsq(X, y, rcond=None)
return b, y - X @ b, X
# 1. Slope of hours without and with the control variable
b_naive, _, _ = ols(hours, score)
b_adj, res, X = ols(np.column_stack([hours, prior]), score)
print("naive:", round(b_naive[1], 3), "adjusted:", round(b_adj[1], 3))
# 2. The same adjusted slope by hand: remove what prior explains from both
_, r_hours, _ = ols(prior, hours)
_, r_score, _ = ols(prior, score)
print("residual-on-residual:", round(ols(r_hours, r_score)[0][1], 3))
# 3. The subgroup approach: five bands of prior grade
band = pd.qcut(prior, 5, labels=False)
dummies = pd.get_dummies(band, drop_first=True).to_numpy(dtype=float)
print("banded:", round(ols(np.column_stack([hours, dummies]), score)[0][1], 3))
# Inference for the adjusted slope
df = n - 3
se = np.sqrt(np.diag(res @ res / df * np.linalg.inv(X.T @ X)))
t = b_adj / se
print(pd.DataFrame({"est": b_adj, "se": se, "t": t, "p": 2 * stats.t.sf(abs(t), df)},
index=["(Intercept)", "hours", "prior"]).round(3))
tq = stats.t.ppf(0.975, df)
print("95% CI hours:", np.round([b_adj[1] - tq * se[1], b_adj[1] + tq * se[1]], 2))
print("R2:", round(1 - res @ res / np.sum((score - score.mean()) ** 2), 3)) naive adjusted
2.788 0.915
[1] 0.915
[1] 1.154
Estimate Std. Error t value Pr(>|t|)
(Intercept) 20.750 0.772 26.876 0
hours 0.915 0.071 12.847 0
prior 0.601 0.013 44.750 0
2.5 % 97.5 %
0.78 1.05
[1] 0.685
The figures come from one seeded run of the R code. Python's random numbers differ from R's, so its figures differ slightly (a naive slope of 2.699, an adjusted slope of 0.945 that the residual method reproduces exactly, and 1.149 with five bands), but they show the same pattern.
What the example shows
- The naive slope was badly inflated. Without the control, one extra hour appeared to be worth 2.79 points, close to three times the true effect, because hours carried along the advantage of the stronger students who put them in.
- Adding prior grade brought it back to the truth. The adjusted slope was 0.915, with a 95% confidence interval of 0.78 to 1.05 that contains the true value of 1.
- The residual route gave exactly the same number (0.915). That is the 'hold constant' idea made concrete: only the part of study time unrelated to prior grade was used.
- Coarse subgroups controlled only part of the confounding. Comparing students within five bands of prior grade gave 1.154. Inside each band, students still differ in prior grade, and those differences still push hours and scores up together. Finer bands would help, but the cells empty quickly once there are several controls.
What controlling cannot do
Adjustment is powerful, but it is only as good as the variables you include and the way you model them. The adjusted coefficient can still be biased when:
- An important confounder is missing. The model can only hold fixed what it sees. Unmeasured confounding is the main reason observational adjusted estimates are not automatically causal.
- A control is measured with error. A noisy measure of ability removes only part of its influence, leaving residual confounding much like the coarse bands above.
- The form is wrong. A straight-line control for a curved relationship leaves some confounding behind. Splines or polynomial terms, or interactions where effects differ by group, can help.
- You control for a mediator. Adjusting for something the predictor causes on the way to the outcome removes part of the very effect you want to measure.
- You control for a collider. Adjusting for something that both the predictor and the outcome cause can create an association that was not there. Choose controls from what you know about the causal structure, not by significance; automatic selection makes this worse.
- There is little overlap. If people with high values of a control almost never have low values of the predictor, the model is extrapolating its straight lines into regions with no data.
Randomised experiments sidestep all of this, because random assignment balances measured and unmeasured variables alike. For reading the rest of the model output, see how to interpret regression output in R; the DASS blog has more on building and checking regression models.
How to report an adjusted regression coefficient in APA style (7th edition)
Name the covariates you adjusted for, then give the coefficient with its standard error or confidence interval and the test. Using the example above:
"Controlling for prior grade, each additional hour of study was associated with a 0.915-point higher exam score, b = 0.915, SE = 0.071, 95% CI [0.78, 1.05], t(1997) = 12.85, p < .001. Without the adjustment the estimate was 2.79 points per hour. The model explained 68.5% of the variance in scores, R² = .685."
In the method section, list every covariate and explain why it was included (for example, as a likely confounder). With several models, a table showing the unadjusted and adjusted coefficients side by side lets readers see how much the adjustment changed the estimate.
Related tools and guides
- APA 7 formatter for regression
- How to interpret regression output in R
- Which interaction terms belong in a model?
- Why stepwise regression is a bad idea
- Centering vs standardizing predictors
- Frisch-Waugh-Lovell theorem (Wikipedia)
More answered questions
- When can you drop the intercept from a linear regression?
- Where does a curve level off? Estimating a plateau point
- Which interaction terms belong in a regression or mixed model?
Working with your own data?
General answers only go so far. Send us your situation and we'll reply by email within two business days.
Ask your questionWritten by AskStats with AI assistance. This is general information, not advice for your specific data or study. When the results matter, check your approach with a qualified statistician.