Answer

Where does a curve level off? Estimating a plateau point

Inspired by a question on Cross Validated ·

regressionnonlinear regression

The short answer

A smooth curve has no single point where it starts to plateau, so you have to define one, and polynomials are the wrong tool for it. Fit a model with a built-in plateau instead: an asymptotic exponential, where the onset can be defined as the point by which 95% of the total drop is complete, or a linear-plateau (broken-stick) model with an estimated breakpoint. Both give the onset as a parameter with a standard error, not a judgement call.

The short answer

A common situation: a measure falls steeply at first and then levels off, and the research question is about where that levelling-off begins. The difficulty is that a smooth curve that approaches a floor never actually becomes flat. Its slope keeps shrinking but never reaches zero, so there is no natural point to find. You have to choose a definition of the onset, and then estimate it with a model that contains that definition as a parameter.

Why polynomial fits cannot answer this

Quadratic and cubic fits are tempting because they are easy, but they have no plateau in them. A quadratic has exactly one flat point, its vertex, and that is where it stops falling and starts rising again. If the data level off, the quadratic still curves back upward beyond the vertex, and the vertex location depends mainly on how far the x-axis happens to extend. A cubic can bend more, but its flat stretches are just as much a by-product of the polynomial form. Searching a polynomial for the place where the slope is nearly zero picks up these artefacts rather than a feature of the data.

A fixed slope threshold ("flat once the slope is below 0.01") has a second problem: the number depends on the units of both axes, so the same threshold means different things for different outcomes or time scales. A relative definition, such as the share of the total drop that is complete, is free of units and comparable across curves.

See it in R and Python

The code simulates 40 points from a curve that starts near 0.9 and falls towards a floor of 0.3, with a true 95%-of-the-drop point of 3.74. It then fits a quadratic, an asymptotic exponential with base R's self-starting SSasymp() model, and a broken-stick model found by trying every breakpoint on a fine grid. The asymptotic model is y = Asym + (R0 - Asym) * exp(-k * x), with k = exp(lrc). The remaining gap to the floor shrinks by the factor exp(-k * x), so 95% of the drop is done when that factor reaches 0.05, at x = log(20) / k. Its 95% interval is computed on the log scale, where the estimate is a simple shift of lrc.

R

set.seed(677612)
x <- seq(0, 10, length.out = 40)
y <- 0.3 + 0.6 * exp(-0.8 * x) + rnorm(40, sd = 0.04)   # true curve levels off at 0.3
# True point where 95% of the drop is done: log(20) / 0.8
round(log(20) / 0.8, 2)

# 1. Quadratic: its "flat point" is the vertex, where it turns back up
quad <- lm(y ~ x + I(x^2))
b <- coef(quad)
round(c(vertex = unname(-b[2] / (2 * b[3]))), 2)

# 2. Asymptotic exponential: y = Asym + (R0 - Asym) * exp(-exp(lrc) * x)
fit <- nls(y ~ SSasymp(x, Asym, R0, lrc))
est <- coef(fit); se <- sqrt(diag(vcov(fit)))
round(cbind(estimate = est, se = se), 3)
k <- exp(est["lrc"])
x95 <- log(20) / k                                 # 95% of the drop completed
ci95 <- exp(log(log(20)) - est["lrc"] + c(-1.96, 1.96) * se["lrc"])
round(c(k = unname(k), x95 = unname(x95), lower = ci95[1], upper = ci95[2]), 2)

# 3. Linear-plateau ("broken stick"): straight decline, then flat after c
grid <- seq(0.5, 9.5, by = 0.01)
rss <- sapply(grid, function(cc) deviance(lm(y ~ pmin(x, cc))))
c_hat <- grid[which.min(rss)]
round(c(breakpoint = c_hat), 2)

# Compare fits by residual standard error
round(c(quadratic = sigma(quad), asymptotic = sigma(fit),
        broken_stick = sqrt(min(rss) / (40 - 3))), 4)

Python

import numpy as np
from scipy.optimize import curve_fit

rng = np.random.default_rng(677612)
x = np.linspace(0, 10, 40)
y = 0.3 + 0.6 * np.exp(-0.8 * x) + rng.normal(0, 0.04, 40)  # true curve levels off at 0.3
print("true x95:", round(np.log(20) / 0.8, 2))

# 1. Quadratic: its "flat point" is the vertex, where it turns back up
b2, b1, b0 = np.polyfit(x, y, 2)
print("vertex:", round(-b1 / (2 * b2), 2))
quad_rss = np.sum((y - np.polyval([b2, b1, b0], x)) ** 2)

# 2. Asymptotic exponential: y = Asym + (R0 - Asym) * exp(-exp(lrc) * x)
def asymp(x, Asym, R0, lrc):
    return Asym + (R0 - Asym) * np.exp(-np.exp(lrc) * x)

est, cov = curve_fit(asymp, x, y, p0=[y[-5:].mean(), y[0], np.log(0.5)])
se = np.sqrt(np.diag(cov))
for name, e, s in zip(["Asym", "R0", "lrc"], est, se):
    print(f"{name:5s} {e:7.3f} {s:6.3f}")
x95 = np.log(20) / np.exp(est[2])                  # 95% of the drop completed
ci95 = np.exp(np.log(np.log(20)) - est[2] + np.array([-1.96, 1.96]) * se[2])
print("k:", round(np.exp(est[2]), 3), "x95:", round(x95, 2), "95% CI:", ci95.round(2))
asym_rss = np.sum((y - asymp(x, *est)) ** 2)

# 3. Linear-plateau ("broken stick"): straight decline, then flat after c
grid = np.round(np.arange(0.5, 9.5 + 1e-9, 0.01), 2)
def rss_at(c):
    X = np.column_stack([np.ones_like(x), np.minimum(x, c)])
    beta = np.linalg.lstsq(X, y, rcond=None)[0]
    return np.sum((y - X @ beta) ** 2)
rss = np.array([rss_at(c) for c in grid])
print("breakpoint:", grid[rss.argmin()])

# Compare fits by residual standard error
print("RSE quadratic, asymptotic, broken stick:",
      np.round(np.sqrt(np.array([quad_rss, asym_rss, rss.min()]) / (40 - 3)), 4))
[1] 3.74
vertex 
  6.88 
     estimate    se
Asym    0.287 0.011
R0      0.831 0.031
lrc    -0.431 0.112
    k   x95 lower upper 
 0.65  4.61  3.70  5.74 
breakpoint 
      2.94 
   quadratic   asymptotic broken_stick 
      0.0564       0.0435       0.0535

All figures come from one seeded run of the R code. Python's random numbers differ from R's, so its estimates differ slightly (95% point 4.18, 95% CI [3.49, 5.02]; breakpoint 2.68; vertex 6.93), but they show the same pattern, including the asymptotic model having the smallest residual standard error.

Reading the results

The choice of 95% is itself a convention. Using 90% gives log(10) / k, and 99% gives log(100) / k. For the exponential model there is a neat link to the slope-threshold idea: the slope also shrinks by the factor exp(-k * x), so the 95%-of-the-drop point is exactly where the slope has fallen to 5% of its starting value. That turns the vague idea of a nearly flat slope into a relative, unit-free rule.

Practical tips

For background on fitting and checking regression models, see the DASS blog, and Wikipedia's articles on nonlinear regression and segmented regression.

How to report a nonlinear regression in APA style (7th edition)

In the method section, give the model, the fitting method and your definition of the onset: "We fitted an asymptotic exponential model, y = Asym + (R0 − Asym)exp(−kx), by nonlinear least squares. The onset of the plateau was defined as the value of x by which 95% of the total decline was complete, log(20)/k, with a Wald 95% confidence interval computed on the log scale."

In the results, using the figures from the example: "The outcome declined from 0.831 (SE = 0.031) towards an asymptote of 0.287 (SE = 0.011), with rate constant k = 0.65. Ninety-five percent of the decline was complete by x = 4.61, 95% CI [3.70, 5.74]. The model fitted better than a quadratic (residual standard errors 0.0435 and 0.0564)." With many curves, put the parameter estimates in a table and state the onset definition in a table note.

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.