---
title: "SMA or OLS for a scaling exponent?"
description: "The standardised major axis is ordinary least squares divided by the correlation: why that identity makes SMA steeper, and when it is the unbiased choice."
date: "2026-07-27 12:00"
categories: [R, allometry, body size, measurement error, regression, ecology tutorial]
image: thumbnail.png
image-alt: "Scatter with two fitted lines of different slope, one from ordinary least squares and one from the standardised major axis"
---
Once you have logged both variables, you still have to draw a line through them, and allometry has two competing conventions. Ordinary least squares minimises vertical distances and treats the x variable as known. The standardised major axis, also called the reduced major axis, treats the two variables symmetrically and almost always gives a steeper slope.
The literature on which to use is long and not entirely friendly. Most of it can be compressed into one identity and one condition, both of which you can verify in a few lines of R. This post derives them by measurement, then shows what happens when the condition fails.
## The identity
Write `r` for the correlation between the logged variables. The ordinary least squares slope is `sd(y) / sd(x)` multiplied by `r`. The standardised major axis slope drops that multiplier:
```{r estimators}
sma_slope <- function(x, y) sign(cor(x, y)) * sd(y) / sd(x)
deming_slope <- function(x, y, delta) {
sxx <- var(x); syy <- var(y); sxy <- cov(x, y)
(syy - delta * sxx + sqrt((syy - delta * sxx)^2 + 4 * delta * sxy^2)) / (2 * sxy)
}
set.seed(380)
n_sp <- 300
x_obs <- rnorm(n_sp, 3, 0.8)
y_obs <- 0.75 * x_obs + rnorm(n_sp, 0, 0.45)
b_ols <- unname(coef(lm(y_obs ~ x_obs))[2])
b_sma <- sma_slope(x_obs, y_obs)
r_obs <- cor(x_obs, y_obs)
c(ols = b_ols, sma = b_sma, r = r_obs,
ols_over_r = b_ols / r_obs,
gap = abs(b_sma - b_ols / r_obs))
```
The standardised major axis slope is the ordinary least squares slope divided by the correlation, and the two agree to `r sprintf("%.1e", abs(b_sma - b_ols/r_obs))`, which is machine precision rather than approximation. On these data the correlation is `r sprintf("%.3f", r_obs)`, so the standardised major axis runs `r sprintf("%.1f", 100*(1/abs(r_obs) - 1))` percent steeper.
That is worth sitting with. The gap between the two exponents carries no biological information at all. It is a function of the scatter and nothing else.
```{r ratio-table}
rr <- c(0.99, 0.95, 0.90, 0.80, 0.70, 0.50)
data.frame(correlation = rr, sma_over_ols = round(1 / rr, 3))
```
At a correlation of 0.9, a perfectly respectable value for an interspecific dataset, the standardised major axis exponent is `r sprintf("%.1f", 100*(1/0.9 - 1))` percent above the least squares one. Papers have argued over exponent differences smaller than that.
```{r fig-two-lines}
#| echo: false
#| fig-cap: "The same logged data with both lines. The standardised major axis passes through the same centroid but tilts towards the axis of greatest spread, and the gap widens as the scatter grows."
#| fig-alt: "Scatter of logged points with two straight lines crossing at the centre of the cloud. The steeper line is labelled SMA and the shallower one OLS."
#| fig-width: 6.4
#| fig-height: 4.2
library(ggplot2)
theme_te <- function(base_size = 11) {
theme_minimal(base_size = base_size) +
theme(
panel.grid.minor = element_blank(),
panel.grid.major = element_line(colour = "#dad9ca", linewidth = 0.3),
plot.background = element_rect(fill = "white", colour = NA),
panel.background = element_rect(fill = "white", colour = NA),
axis.title = element_text(colour = "#2c3a31"),
axis.text = element_text(colour = "#5d6b61"),
legend.text = element_text(colour = "#2c3a31"),
legend.title = element_blank(),
plot.title = element_text(colour = "#16241d", face = "bold", size = base_size + 1),
plot.subtitle = element_text(colour = "#5d6b61", size = base_size - 1)
)
}
dd <- data.frame(x = x_obs, y = y_obs)
int_ols <- mean(y_obs) - b_ols * mean(x_obs)
int_sma <- mean(y_obs) - b_sma * mean(x_obs)
ggplot(dd, aes(x, y)) +
geom_point(colour = "#275139", alpha = 0.4, size = 1.6) +
geom_abline(aes(slope = b_ols, intercept = int_ols, colour = "OLS"), linewidth = 0.9) +
geom_abline(aes(slope = b_sma, intercept = int_sma, colour = "SMA"), linewidth = 0.9) +
scale_colour_manual(values = c(OLS = "#c9b458", SMA = "#b5534e")) +
labs(x = "log body size", y = "log trait",
title = "Two lines through one cloud",
subtitle = sprintf("r = %.3f, so the SMA slope is exactly 1/r times the OLS slope", r_obs)) +
theme_te() + theme(legend.position = "top")
```
## The standardised major axis is a Deming fit in disguise
A Deming regression asks you to supply `delta`, the ratio of the error variance in y to the error variance in x, and then minimises a weighted perpendicular distance. Set `delta` to the ratio of the two observed variances and the algebra collapses.
```{r sma-is-deming}
delta_obs <- var(y_obs) / var(x_obs)
c(sma = b_sma,
deming_at_var_ratio = deming_slope(x_obs, y_obs, delta_obs),
gap = abs(b_sma - deming_slope(x_obs, y_obs, delta_obs)))
```
Again exact, to `r sprintf("%.1e", abs(b_sma - deming_slope(x_obs, y_obs, delta_obs)))`. So the standardised major axis is not an assumption-free method. It is a Deming fit that has quietly assumed the error variance ratio equals the total variance ratio of the data, which is to say it has assumed both variables are measured with the same relative precision. Nobody is asked to defend that assumption because nobody is asked to state it.
## When is that assumption right?
Set up a structural model with known parts. Let the true log body size be `x_t`, let the true relationship be `y_t = b * x_t`, and let each variable pick up its own independent error. Then ask which estimator recovers `b`.
The algebra gives a sharp answer. The observed variances are `var(x) = var(x_t) + var(e_x)` and `var(y) = b^2 * var(x_t) + var(e_y)`, so the standardised major axis estimates the square root of their ratio. That equals `b` exactly when `var(e_y) = b^2 * var(e_x)`, and not otherwise.
```{r sma-condition}
b_str <- 0.75
n_str <- 400
n_mc <- 2000
run_config <- function(sd_ex, sd_ey, seed) {
set.seed(seed)
out <- t(vapply(seq_len(n_mc), function(i) {
xt <- rnorm(n_str, 3, 0.8)
x <- xt + rnorm(n_str, 0, sd_ex)
y <- b_str * xt + rnorm(n_str, 0, sd_ey)
c(unname(coef(lm(y ~ x))[2]),
sma_slope(x, y),
deming_slope(x, y, sd_ey^2 / sd_ex^2))
}, numeric(3)))
colMeans(out)
}
cfg <- list(c(0.30, 0.225), c(0.30, 0.300), c(0.30, 0.100), c(0.30, 0.450))
tab <- do.call(rbind, lapply(seq_along(cfg), function(k) {
m <- run_config(cfg[[k]][1], cfg[[k]][2], 4380 + k)
data.frame(sd_ex = cfg[[k]][1], sd_ey = cfg[[k]][2],
err_ratio = round(cfg[[k]][2]^2 / cfg[[k]][1]^2, 3),
ols = round(m[1], 4), sma = round(m[2], 4),
deming_true = round(m[3], 4))
}))
tab
```
The critical ratio is `b^2`, which for a true exponent of `r sprintf("%.2f", b_str)` is `r sprintf("%.4f", b_str^2)`. The first row sits on it, and the standardised major axis returns `r sprintf("%.4f", tab$sma[1])` against a truth of `r sprintf("%.2f", b_str)`. Move off it and the estimate moves with it: equal error variances give `r sprintf("%.4f", tab$sma[2])`, a precise y variable gives `r sprintf("%.4f", tab$sma[3])`, and a noisy y variable gives `r sprintf("%.4f", tab$sma[4])`. Least squares sits at about `r sprintf("%.3f", mean(tab$ols))` throughout, attenuated by the x error in the usual way, and Deming with the true ratio recovers the exponent in all four rows.
So the standardised major axis is not a fix for measurement error. It is a fix for one specific measurement error ratio, and it is biased in a knowable direction everywhere else: too steep when y is noisy relative to x, too shallow when x is the noisy one.
```{r fig-sweep}
#| echo: false
#| fig-cap: "Mean estimated exponent across a sweep of error variance ratios, with the x error held fixed. The standardised major axis crosses the truth exactly once, at a ratio equal to the squared exponent."
#| fig-alt: "Line chart with error variance ratio on a log axis. The SMA curve rises steadily and crosses a horizontal true-value line at one point, while the OLS curve stays flat and well below it."
#| fig-width: 6.6
#| fig-height: 4
set.seed(4390)
sd_ex_fix <- 0.30
ratios <- c(0.08, 0.15, 0.30, 0.5625, 1.0, 1.8, 3.2)
swp <- do.call(rbind, lapply(ratios, function(rt) {
sd_ey <- sd_ex_fix * sqrt(rt)
o <- t(vapply(seq_len(700), function(i) {
xt <- rnorm(n_str, 3, 0.8)
x <- xt + rnorm(n_str, 0, sd_ex_fix)
y <- b_str * xt + rnorm(n_str, 0, sd_ey)
c(unname(coef(lm(y ~ x))[2]), sma_slope(x, y))
}, numeric(2)))
data.frame(ratio = rt, method = c("OLS", "SMA"), est = colMeans(o))
}))
ggplot(swp, aes(ratio, est, colour = method)) +
geom_hline(yintercept = b_str, colour = "#93a87f", linewidth = 0.5, linetype = "dashed") +
geom_line(linewidth = 0.9) + geom_point(size = 2) +
scale_x_log10() +
scale_colour_manual(values = c(OLS = "#c9b458", SMA = "#b5534e")) +
labs(x = "error variance ratio, var(e_y) / var(e_x)", y = "mean estimated exponent",
title = "Only one ratio makes the SMA unbiased",
subtitle = sprintf("dashed line: true exponent %.2f; the crossing sits at b squared = %.4f",
b_str, b_str^2)) +
theme_te() + theme(legend.position = "top")
```
## Equation error breaks the condition on its own
There is a second source of scatter that has nothing to do with instruments. Two species of the same body mass genuinely differ in metabolic rate, and no amount of careful measurement removes that. Warton and colleagues call this equation error, to distinguish it from measurement error, and it lands entirely in the y variable.
Notice what that does to the condition. Equation error adds to `var(e_y)` while contributing nothing to `var(e_x)`. If your x variable is measured well, as body mass usually is, the ratio sits far above `b^2` and the standardised major axis runs steep. Take the limiting case where x is measured perfectly:
```{r equation-error}
set.seed(3801)
xt <- rnorm(n_str, 3, 0.8)
x_clean <- xt
y_eqn <- b_str * xt + rnorm(n_str, 0, 0.30)
c(true_b = b_str,
ols = unname(coef(lm(y_eqn ~ x_clean))[2]),
sma = sma_slope(x_clean, y_eqn))
```
With no error in x at all, least squares is the correct estimator and returns `r sprintf("%.3f", unname(coef(lm(y_eqn ~ x_clean))[2]))`, while the standardised major axis returns `r sprintf("%.3f", sma_slope(x_clean, y_eqn))`. Switching to the symmetric method because "both variables are biological" has made the answer worse, not safer.
## Where to go next
The choice of line is a choice of error model, and the data cannot make it for you. Three practical positions:
If your x variable is measured much more precisely than the scatter in y, and the scatter is mostly biological, ordinary least squares estimates the relationship you want. If you are describing the joint spread of two traits rather than predicting one from the other, the standardised major axis is a reasonable descriptive summary, and its symmetry is a genuine advantage: it gives the same line whichever variable you put on the x axis. If you actually know something about the two error variances, from repeated measurements or from instrument specifications, Deming with that ratio dominates both.
What you should not do is pick the method that gives the exponent closest to the value you expected. The gap between the two lines is `1/r`, and it is available to be exploited in either direction.
## Honest limits
Everything above is about the point estimate. Intervals are a separate problem: the sampling distribution of the standardised major axis slope is skewed, and its confidence interval is not the usual symmetric Wald form. The condition `var(e_y) = b^2 * var(e_x)` is also not testable from a single bivariate sample, because the observed variances confound signal and error. That is the same wall the errors-in-variables literature runs into everywhere: a ratio has to come from outside the data.
## References
Warton, Wright, Falster, Westoby 2006 Biological Reviews 81(2):259-291 (10.1017/S1464793106007007)
Warton, Weber 2002 Biometrical Journal 44(2):161-174 (10.1002/1521-4036(200203)44:2<161::AID-BIMJ161>3.0.CO;2-N)
Xiao, White, Hooten, Durham 2011 Ecology 92(10):1887-1894 (10.1890/11-0538.1)
## Related tutorials
- [Errors in variables and Deming regression](../errors-in-variables-deming-regression/) - the general two-error line fit that the SMA is a special case of.
- [Allometry and log-log regression in R](../allometry-and-log-log-regression/) - getting onto the log scale and back off it again.
- [Measurement error and regression dilution](../measurement-error-regression-dilution/) - why the least squares slope attenuates, and by how much.
- [Testing isometry and comparing slopes](../testing-isometry-and-comparing-slopes/) - inference once the line is chosen.