Answer

When can you drop the intercept from a linear regression?

Inspired by a question on Cross Validated ·

regressionmodel selection

The short answer

Rarely, and never because the fit looks better. Dropping the intercept forces the line through the origin, which biases the slope unless the true line passes through zero. The higher R-squared is an artefact: without an intercept it is measured against zero, not against the mean of y. Leave it out only when theory says y must be zero when the predictors are zero, the data reach near that point, and the data agree.

The short answer

The intercept is the predicted value of y when every predictor equals zero. Removing it does not just delete one uninteresting number: it adds the constraint that the fitted line must pass through the point (0, 0). If that constraint is false, every other coefficient shifts to accommodate it, and the slope you report is wrong.

Keeping the intercept costs one degree of freedom and nothing else. Removing it when it should be there can cost you an unbiased slope, residuals that average zero, and an honest R-squared. That asymmetry is why the default is to keep it, even when its estimate is not significantly different from zero and even when zero is outside the range of your data.

Why R-squared jumps when you remove the intercept

R-squared is 1 minus the residual sum of squares divided by a total sum of squares. With an intercept, the total is measured around the mean of y: the model is compared with a flat line at ȳ. Without an intercept, R's summary(), Python's statsmodels and Stata, among others, measure the total around zero instead, so the model is compared with the line y = 0. Because the sum of y² is far larger than the sum of squared deviations from the mean whenever the values of y sit well away from zero, the ratio shrinks and R-squared climbs towards 1.

The two R-squared values therefore answer different questions and cannot be compared. To compare the models fairly, look at their residual sums of squares, or compute the usual (centred) R-squared for both. Since the no-intercept model is the full model with one coefficient fixed at zero, its residual sum of squares can never be smaller: on the same yardstick, removing the intercept can only make the fit worse or leave it unchanged.

See it in R and Python

The example simulates 50 points from a line with intercept 5 and slope 2, with x between 10 and 20, then fits the model with and without an intercept (y ~ 0 + x and y ~ x - 1 both remove it in R). The Python version fits the same two models with NumPy's least squares and reproduces R's definitions, including the zero-based R-squared R reports for a model without an intercept.

R

set.seed(7948)
n <- 50
x <- runif(n, 10, 20)               # x never comes close to 0
y <- 5 + 2 * x + rnorm(n, sd = 3)   # the true line has intercept 5

with_int <- lm(y ~ x)
no_int   <- lm(y ~ 0 + x)           # same as y ~ x - 1

# Slopes: the no-intercept fit bends the slope to pass through (0, 0)
round(c(slope_with = coef(with_int)[["x"]], slope_without = coef(no_int)[["x"]]), 3)
round(confint(with_int), 2)         # does the intercept's CI include 0?

# R-squared as each model reports it
round(c(r2_with = summary(with_int)$r.squared, r2_without = summary(no_int)$r.squared), 3)

# Same yardstick for both: residual sums of squares and centered R-squared
rss <- c(with = sum(resid(with_int)^2), without = sum(resid(no_int)^2))
tss_centered <- sum((y - mean(y))^2)
round(rss, 1)
round(1 - rss / tss_centered, 3)

# Residuals no longer average zero without an intercept
round(c(mean_resid_with = mean(resid(with_int)), mean_resid_without = mean(resid(no_int))), 3)

# Formal check: the no-intercept model is nested in the full one
anova(no_int, with_int)

Python

import numpy as np
from scipy import stats

rng = np.random.default_rng(7948)
n = 50
x = rng.uniform(10, 20, n)               # x never comes close to 0
y = 5 + 2 * x + rng.normal(0, 3, n)      # the true line has intercept 5

X_with = np.column_stack([np.ones(n), x])
X_without = x[:, None]                   # no column of ones = no intercept
b_with, *_ = np.linalg.lstsq(X_with, y, rcond=None)
b_without, *_ = np.linalg.lstsq(X_without, y, rcond=None)
res_with = y - X_with @ b_with
res_without = y - X_without @ b_without

print("slopes:", round(b_with[1], 3), round(b_without[0], 3))

# 95% CI for the intercept, as confint() gives it
df = n - 2
s2 = res_with @ res_with / df
se = np.sqrt(np.diag(s2 * np.linalg.inv(X_with.T @ X_with)))
tq = stats.t.ppf(0.975, df)
print("intercept CI:", np.round([b_with[0] - tq * se[0], b_with[0] + tq * se[0]], 2))

# R-squared as each model reports it (R uses an uncentered total without an intercept)
rss = np.array([res_with @ res_with, res_without @ res_without])
tss_centered = np.sum((y - y.mean()) ** 2)
print("reported R2:", round(1 - rss[0] / tss_centered, 3), round(1 - rss[1] / np.sum(y ** 2), 3))

# Same yardstick for both
print("RSS:", np.round(rss, 1), "centered R2:", np.round(1 - rss / tss_centered, 3))
print("mean residuals:", round(res_with.mean(), 3), round(res_without.mean(), 3))

# Nested-model F test, as anova(no_int, with_int)
F = (rss[1] - rss[0]) / (rss[0] / df)
print("F =", round(F, 4), "p =", round(stats.f.sf(F, 1, df), 5))
   slope_with slope_without 
        2.015         2.326 
            2.5 % 97.5 %
(Intercept) -0.23   9.47
x            1.68   2.35
   r2_with r2_without 
     0.755      0.991 
   with without 
  478.9   515.6 
   with without 
  0.755   0.737 
   mean_resid_with mean_resid_without 
             0.000              0.158 
Analysis of Variance Table

Model 1: y ~ 0 + x
Model 2: y ~ x
  Res.Df    RSS Df Sum of Sq      F  Pr(>F)  
1     49 515.56                              
2     48 478.95  1    36.618 3.6698 0.06137 .

The figures come from one seeded run of the R code. Python's random numbers differ from R's, so its figures differ slightly (for example slopes of 2.068 and 2.297, and a reported R-squared of .823 with the intercept against .994 without), but they show the same pattern.

What the example shows

When removing the intercept is justified

Fitting through the origin (sometimes called regression through the origin) is reasonable only when all three of these hold:

One common case is not really a removed intercept. With a categorical predictor, y ~ 0 + group estimates one mean per group instead of a baseline and differences. The fitted values and residuals are identical to y ~ group; only the coefficients' meaning changes, and R-squared is again reported against zero, so ignore it. If the problem is that zero is a meaningless value of x, centre the predictor instead: that makes the intercept interpretable without constraining the fit. The DASS blog has more on building and checking regression models.

How to report a regression without an intercept in APA style (7th edition)

If you do fit through the origin, say so in the method section and justify it, because readers will otherwise assume a standard model. Report the comparison with the full model and the centred fit rather than the zero-based R-squared. Using the example above:

"Because the intercept was not significantly different from zero, 95% CI [−0.23, 9.47], F(1, 48) = 3.67, p = .061, a model through the origin was considered. It was rejected because the slope changed from 2.015 to 2.326 and the centred R² fell from .755 to .737."

When a no-intercept model is justified by theory, write, for example, "The regression was constrained through the origin because [reason]; the slope was b = x.xx, 95% CI [LL, UL]. The R² reported by the software is computed about zero and is not comparable with R² from models with an intercept."

Related tools and guides

More answered questions

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 question

Written 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.