Quantile Regression

Michael BrenndoerferOctober 26, 202666 min read

Learn quantile regression from first principles: pinball loss, linear programming, conditional quantiles, crossing diagnostics, and Python implementation.

Machine Learning from ScratchAll chapters
Part 5 of 15Linear and Generalized Linear Models
Chapter 47Quantile Regression
Reading the full chapter · 10 sections · 66 min10 sections · 66 min

All 10 sections · 66 min read

Quantile RegressionFull chapter · Sections in this chapter1 / 10 ↓
Reading tools Definitions, links & notes

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Notes & comments

Quantile Regression

A courier company wants to promise a delivery time. A forecast of the average duration does not tell it what fraction of parcels will arrive late. Quoting a much longer time may reduce late deliveries but make the service less attractive. A conditional 90th percentile addresses the operational question: what duration has at least 90 percent of deliveries at or below it for a route with these predictors? An estimated percentile still needs validation before it becomes a service promise.

Ordinary least squares answers a different question: it fits a linear conditional-mean model when that mean is correctly specified. Fitting the coefficients does not require constant conditional variance. A constant-width prediction band is an additional modeling choice, not a restriction imposed by least squares. Quantile regression instead fits a conditional quantile at a chosen level τ\tau. For a continuous conditional distribution with a unique quantile, the true 0.9 quantile has 90 percent of probability at or below it. Fitted linear quantiles need not achieve that exact conditional fraction in a finite sample.

The mechanism is a change of loss function. Squared error ρ(u)=u2\rho(u) = u^2 is minimized by the conditional mean. The pinball loss (an asymmetric absolute-value loss) is minimized by the conditional quantile. Once we make that swap, the familiar linear model machinery (design matrices, gradients, regularization) mostly carries over, but the geometry changes in important ways. The objective becomes piecewise linear and non-differentiable at zero, so we solve a linear program rather than a linear system. The fitted curves are also no longer guaranteed to be ordered across τ\tau.

This chapter is the last stop in our tour of Part V: Linear and Generalized Linear Models. Everything we built for multiple linear regression, the design matrix, the intercept column, the loss-plus-optimizer framing from gradient descent and numerical optimization, reappears here with a different loss. What we gain is a model that speaks directly about risk, spread, and tail behavior, which is exactly the language we will need when we move to time-to-event data in Part VI.

Advantages

The first advantage is a direct description of different parts of the conditional distribution without choosing a complete distributional family. A mean alone does not determine spread or shape, even for symmetric, light-tailed distributions. Fitting levels such as τ=0.05,0.25,0.5,0.75,0.95\tau = 0.05, 0.25, 0.5, 0.75, 0.95 estimates several conditional cutoffs; the gaps between them describe spread. The linear functional form remains an assumption at each level. Heteroscedasticity can therefore become a feature we estimate rather than something hidden by a single-width summary.

The second advantage is robustness in the response direction. Because the pinball loss grows linearly rather than quadratically, its response score is bounded at fixed predictors, unlike the least-squares score. This does not bound every finite-sample coefficient change; a sample tail quantile can still be determined by one extreme response. At the median, quantile regression is least absolute deviations regression, a maximum likelihood estimate for the regression coefficients under independent Laplace errors centered at zero with a common fixed positive scale, which we first met in maximum likelihood estimation. At a fixed predictor vector, the estimating score is bounded as the response varies; extreme predictors can still have high leverage. For the pinball loss the residual slopes are τ\tau on the positive side and τ−1\tau - 1 on the negative side, with magnitudes τ\tau and 1−τ1 - \tau respectively, so no slope ever exceeds 11 in magnitude.

A useful population property is equivariance under continuous strictly increasing transformations. For such a function hh, quantiles obey Qh(Y)(τ)=h(QY(τ))Q_{h(Y)}(\tau)=h(Q_Y(\tau)); log and exponential transformations on their respective domains satisfy this condition. If the conditional quantile of log⁡Y\log Y is modeled correctly, exponentiating its estimate targets the corresponding quantile of positive YY, without a conditional-mean retransformation correction. This does not say that two restricted linear fits, one on YY and one on log⁡Y\log Y, agree exactly after transformation in a finite sample.

Finally, quantile regression is a natural tool for asymmetric decision problems. Inventory levels, staffing, safety margins, and service-level agreements can involve quantile targets when their costs or probability constraints have the appropriate form. A blood-pressure reference chart that flags the top 5 percent of children by age is a quantile curve, not a mean curve. When underage and overage costs are positive and linear in the size of the error, expected pinball loss at the cost-implied τ\tau matches that decision problem up to a positive scale factor.

Disadvantages

The main practical annoyance is quantile crossing. In the population the quantile function is nondecreasing in τ\tau, so Q(0.4∣x)≤Q(0.6∣x)Q(0.4 \mid x) \le Q(0.6 \mid x) for every xx. However, each τ\tau is estimated by its own optimization, so nothing in the procedure forces the fitted curve at τ=0.4\tau = 0.4 to lie below the fitted curve at τ=0.6\tau = 0.6. In finite samples the curves can cross; the risk depends on sample size, the design, the predictor domain, and functional-form misspecification. At a fixed predictor value, narrowing a nested pair of target levels cannot increase its population quantile gap, leaving less separation for estimation error to overcome. A crossing fitted value is not just undesirable. It cannot be the ordered quantile function of a conditional distribution and can reverse the endpoints of a prediction band. We will see how to diagnose this, and how rearrangement repairs it, later in the chapter.

The second limitation is that efficiency at the center is worse than least squares when least squares is appropriate. For nn independent, identically distributed Gaussian observations with positive variance σ2\sigma^2, the sample mean has variance σ2/n\sigma^2/n while the sample median has asymptotic variance πσ2/(2n)\pi\sigma^2/(2n), so their relative efficiency (the ratio of variances) is (σ2/n)/(πσ2/(2n))=2/π≈0.64(\sigma^2/n)/(\pi\sigma^2/(2n)) = 2/\pi \approx 0.64 for the median, meaning we need roughly 1/(2/π)=π/2≈1.571/(2/\pi) = \pi/2 \approx 1.57 times as much data for the same precision. The same 2/π2/\pi asymptotic relative-efficiency factor applies to correctly specified median regression with independent homoscedastic Gaussian errors and a regular design. Tail precision depends on the conditional density and design as well as τ\tau: normal tails have low density and can be difficult to estimate, but uncertainty need not increase monotonically toward the tails for every distribution.

Third, the objective is not smooth, so the fitted coefficients need not change smoothly as the data change; jumps can occur when an optimum switches between non-unique solutions. The general multivariable problem has no least-squares-style matrix formula, although special cases have quantile shortcuts. Ordinary differentiation is insufficient at zero residuals; we use a linear program. Its runtime depends on the sample size, predictor count, matrix representation, conditioning, and solver settings, and the piecewise-linear structure can produce non-unique optima when the data are degenerate (duplicate covariate patterns, for example), which complicates inference. Common regular asymptotic standard-error estimates use resampling or estimates of the conditional density; they do not automatically follow the OLS formula σ2(X⊤X)−1\sigma^2 (X^\top X)^{-1}.

Fourth, and most often confused, a fitted quantile band is not a confidence interval. The band [Q^(0.05∣x),Q^(0.95∣x)][\hat Q(0.05 \mid x), \hat Q(0.95 \mid x)] targets coverage of individual responses YY (here Q^(τ∣x)\hat Q(\tau \mid x) is the estimated conditional τ\tau-quantile), a statement about the spread of the data; a confidence interval is a statement about the uncertainty of an estimated parameter such as a regression coefficient. The first can be wide because the data are noisy; the second can be narrow because the sample is large. Both need to be reported, and they answer different questions.

Formula

A regression model for the mean answers the question "what is the average response at this xx?" A regression model for a quantile answers a sharper question: "what cutoff has at most a fraction τ\tau of responses strictly below it and at least a fraction τ\tau at or below it at this xx?" To make that question precise we need three ingredients: a definition of a conditional quantile, a loss function whose minimizer is that quantile, and a finite-sample optimization problem we can solve. The first two are population statements; the third is what turns the idea into an algorithm.

We begin with notation. Let YY be a response variable and let the full predictor vector be X=(X0,X1,…,Xp)X = (X_0,X_1,\dots,X_p), with X0=1X_0=1 for the intercept. There are pp nonconstant predictors and p+1p+1 entries in the design vector. The cumulative distribution function of YY is FY(y)=P(Y≤y)F_Y(y) = P(Y \le y), and the τ\tau-quantile of YY, for τ∈(0,1)\tau \in (0,1), is

QY(τ)=inf⁡{y:FY(y)≥τ}.Q_Y(\tau) = \inf\{y : F_Y(y) \ge \tau\}.

The infimum handles distributions with flat regions or jumps; where FYF_Y is continuous and strictly increasing it agrees with the ordinary inverse; otherwise it remains a generalized inverse. Conditioning on the predictors gives the target:

QY∣X=x(τ)=inf⁡{y:FY∣X=x(y)≥τ},Q_{Y \mid X=x}(\tau) = \inf\{y : F_{Y\mid X=x}(y) \ge \tau\},

where:

  • FY∣X=xF_{Y\mid X=x}: the conditional cumulative distribution function of YY given X=xX = x
  • FY∣X=x(y)F_{Y\mid X=x}(y): the value of that conditional CDF evaluated at a candidate response value yy
  • inf⁡\inf: the infimum, the greatest lower bound of the set of response values satisfying the inequality
  • QY∣X=x(τ)Q_{Y \mid X=x}(\tau): the conditional τ\tau-quantile of YY at a given predictor value xx, the smallest response value whose conditional CDF reaches at least τ\tau

For intuition, consider the case where Y∣X=xY \mid X = x is normal with mean μ(x)\mu(x) and standard deviation σ(x)\sigma(x). Then

QY∣X=x(τ)=μ(x)+σ(x) Φ−1(τ),Q_{Y \mid X = x}(\tau) = \mu(x) + \sigma(x)\,\Phi^{-1}(\tau),

where:

  • Φ−1(τ)\Phi^{-1}(\tau): the quantile function of the standard normal distribution, evaluated at probability τ\tau, which is the same zz-value used for confidence intervals in the z-test
  • μ(x)\mu(x): the conditional mean of YY given X=xX = x
  • σ(x)\sigma(x): the conditional standard deviation of YY given X=xX = x

At τ=0.5\tau = 0.5 we get μ(x)\mu(x), because Φ−1(0.5)=0\Phi^{-1}(0.5) = 0. At τ=0.95\tau = 0.95 we get μ(x)+1.645σ(x)\mu(x) + 1.645\sigma(x). So if we assume normality, the conditional quantile function is just the mean function shifted by a multiple of the conditional standard deviation. Ordinary least squares estimates μ(x)\mu(x) and, when it assumes constant variance, treats σ\sigma as a single number. Quantile regression estimates QY∣X=x(τ)Q_{Y\mid X=x}(\tau) directly, which lets μ\mu and σ\sigma both vary with xx, and lets the shape vary too.

A quantile is a local summary of a distribution: it depends on where probability mass accumulates near the cut, not just on a global average. That local character explains two features that will reappear throughout the chapter. First, different quantiles can respond to the predictors in different ways, so a single slope is not enough. Second, precision depends on the density near the selected cutoff and on the predictor design. Low normal-tail density makes extreme levels difficult in our Gaussian example, but there is no universal ordering of quantile variances. The pinball loss below encodes exactly this local, asymmetric view of the data.

Conditional quantiles and pinball loss

To estimate a quantile we need a loss function minimized at that quantile. In the following expectation calculations assume E[Y2]<∞\mathbb E[Y^2]<\infty for squared error and E∣Y∣<∞\mathbb E|Y|<\infty for pinball loss. For the density-based derivatives, assume the distribution has a continuous density near the candidate cutoff. Start with the familiar case. Minimizing the expected squared error over the candidate value qq,

Lsq(q)=E[(Y−q)2],L_{\text{sq}}(q) = \mathbb{E}\left[(Y - q)^2\right],

leads to the mean. Differentiating with respect to qq (the finite second moment allows the locally integrable derivative to pass through the expectation) gives

ddqLsq(q)=−2 E[Y−q]=−2(E[Y]−q),\frac{d}{dq} L_{\text{sq}}(q) = -2\,\mathbb{E}[Y - q] = -2\left(\mathbb{E}[Y] - q\right),

which is zero exactly when q=E[Y]q = \mathbb{E}[Y]. Setting the derivative to zero and checking that the second derivative, 2>02 > 0, confirms a minimum. Squared error is married to the mean.

Now replace squared error with the asymmetric absolute-value loss known as the pinball loss, or check loss. Here u=y−qu = y - q is the signed residual between an observation yy and a candidate value qq, and τ∈(0,1)\tau \in (0,1) is the target quantile level:

ρτ(u)={τ u,u≥0,(τ−1) u,u<0.\rho_\tau(u) = \begin{cases} \tau\, u, & u \ge 0,\\[2pt] (\tau - 1)\, u, & u < 0. \end{cases}

where:

  • uu: the residual, the signed difference between the observation and the candidate value, u=y−qu = y - q
  • τ\tau: the target quantile level in (0,1)(0,1)
  • ρτ(u)\rho_\tau(u): the pinball loss at level τ\tau for residual uu
  • qq: the candidate value at which the pinball loss is being evaluated

For τ>0.5\tau > 0.5, the positive-residual branch is steeper: its slope magnitude is τ\tau, compared with 1−τ1-\tau on the negative side. A compact way to write the same function is

ρτ(u)=u(τ−1{u<0}),\rho_\tau(u) = u\left(\tau - \mathbf{1}\{u < 0\}\right),

where:

  • uu: the residual, the signed difference between the observation and the candidate value, u=y−qu = y - q
  • τ\tau: the target quantile level in (0,1)(0,1)
  • 1{⋅}\mathbf{1}\{\cdot\}: the indicator function, equal to 1 when its argument is true and 0 otherwise
  • qq: the candidate value at which the pinball loss is being evaluated, so that the loss is a function of u=y−qu = y - q and of τ\tau
  • ρτ(u)\rho_\tau(u): the pinball loss at level τ\tau for residual uu

Check the two branches: if u≥0u \ge 0 the indicator is zero and we get τu\tau u; if u<0u < 0 the indicator is one and we get (τ−1)u(\tau - 1)u, which is positive because τ−1<0\tau - 1 < 0 and u<0u < 0. At τ=0.5\tau = 0.5 both slopes have magnitude 0.50.5, and ρ0.5(u)=∣u∣/2\rho_{0.5}(u) = |u|/2, so the pinball loss is just half the absolute error.

The asymmetry is the whole point. For a high quantile, say τ=0.9\tau = 0.9, a positive residual (an observation above the fitted value) is penalized by slope τ=0.9\tau = 0.9, while a negative residual is penalized by slope 1−τ=0.11 - \tau = 0.1. For one unrestricted sample cutoff that minimizes this loss, at most 90 percent of observations lie strictly below it and at least 90 percent lie at or below it. For a low quantile the push goes the other way. This is how a single loss function encodes a target probability level. The left branch has slope τ−1\tau - 1 (magnitude 1−τ1 - \tau) and the right branch has slope τ\tau. These magnitudes are equal only when τ=0.5\tau = 0.5. Any other τ\tau makes the two arms differ in slope, and the loss is asymmetric.

To see that this loss is minimized by the τ\tau-quantile, let f(y)f(y) be the probability density function of YY and write the expected loss at a candidate value qq, where qq is the candidate value being scored, f(y)f(y) is the density of YY, and ρτ\rho_\tau is the pinball loss defined above. We obtain

L(q)=E[ρτ(Y−q)]=∫−∞q(τ−1)(y−q)f(y) dy+∫q∞τ(y−q)f(y) dy.L(q) = \mathbb{E}\left[\rho_\tau(Y - q)\right] = \int_{-\infty}^{q} (\tau - 1)(y - q) f(y)\, dy + \int_{q}^{\infty} \tau (y - q) f(y)\, dy.

The first integral collects the observations below qq, where the residual y−qy - q is negative and the loss uses the (τ−1)(\tau-1) branch; the second collects observations above qq.

where:

  • qq: the candidate value being scored as a quantile
  • f(y)f(y): the probability density function of YY
  • ρτ\rho_\tau: the pinball loss at level τ\tau, defined above
  • L(q)L(q): the expected pinball loss at the candidate value qq

Now differentiate with respect to qq. The finite first moment makes the expected loss finite; the residual-loss derivatives are bounded by 1 in magnitude, which justifies differentiation at continuity points of the CDF. Both integrands vanish at the boundary y=qy = q because each factor contains (y−q)(y-q), so the Leibniz rule gives no boundary contribution and we only need the derivative of the integrands.

dLdq=∫−∞q(1−τ)f(y) dy  −  ∫q∞τf(y) dy.\frac{dL}{dq} = \int_{-\infty}^{q} (1 - \tau) f(y)\, dy \; - \; \int_{q}^{\infty} \tau f(y)\, dy.

The derivative of (τ−1)(y−q)(\tau-1)(y-q) with respect to qq is 1−τ1-\tau, and that of τ(y−q)\tau(y-q) is −τ-\tau. At either moving integration endpoint y=qy=q, the factor y−qy-q is zero, so each boundary contribution vanishes. Recognizing the remaining integrals as probabilities gives:

where:

  • F(q)F(q): the cumulative distribution function of YY evaluated at qq
dLdq=(1−τ)F(q)−τ(1−F(q))=(1−τ)F(q)−τ+τF(q)=F(q)−τ.\begin{aligned} \frac{dL}{dq} &= (1 - \tau) F(q) - \tau\left(1 - F(q)\right) \\ &= (1 - \tau) F(q) - \tau + \tau F(q) \\ &= F(q) - \tau. \end{aligned}

The two τF(q)\tau F(q) terms combine: (1−τ)F(q)+τF(q)=F(q)(1-\tau)F(q) + \tau F(q) = F(q), leaving F(q)−τF(q) - \tau. Setting this to zero gives F(q)=τF(q)=\tau. When the CDF is strictly increasing at this cutoff, it selects the unique QY(τ)Q_Y(\tau); a flat region can give multiple minimizing values. The second derivative is f(q)≥0f(q) \ge 0, so LL is convex and this stationary point is a global minimum. The pinball loss is therefore the loss whose minimizer is the quantile, exactly as squared error is the loss whose minimizer is the mean.

For a finite sample, define F^n(q)=n−1∑i1{yi≤q}\hat F_n(q)=n^{-1}\sum_i\mathbf 1\{y_i\le q\} and let F^n(q−)\hat F_n(q^-) be the fraction strictly below qq. The subgradient interval of the average pinball loss, as a function of qq, is [F^n(q−)−τ,F^n(q)−τ][\hat F_n(q^-)-\tau,\hat F_n(q)-\tau]. A minimum occurs exactly when this interval contains zero, giving the counting rule:

#{yi<q}≤nτ≤#{yi≤q}.\#\{y_i < q\} \le n\tau \le \#\{y_i \le q\}.

In words: the number of observations strictly below the estimate is at most nτn\tau, and the number at or below it is at least nτn\tau; dividing by nn, at most a fraction τ\tau of the data lie strictly below the estimate and at least a fraction τ\tau lie at or below it. Ties at exactly qq are what allow both inequalities to hold when nτn\tau is not an integer, so the two sides can disagree by the mass pinned at qq.

The figure below makes the asymmetry visible. Each curve is a pinball loss for a different τ\tau. The two constant residual slopes, together with the kink at zero, give the quantile counting condition derived above.

PythonIn [3]
CollapseExpand
u_grid = np.linspace(-2.5, 2.5, 401)
pinball_levels = (0.1, 0.5, 0.9)
pinball_curves = {
    level: np.where(u_grid >= 0, level * u_grid, (level - 1) * u_grid)
    for level in pinball_levels
}
squared_comparison = 0.5 * u_grid**2
PythonIn [4]
CollapseExpand
import matplotlib.pyplot as plt
from book_plot_style import PALETTE, polish_axes, use_book_style

use_book_style()

fig, ax = plt.subplots(figsize=(6.0, 4.8))
for tau_level, color, line_style in zip(
    pinball_levels,
    [PALETTE["primary"], PALETTE["amber"], PALETTE["teal"]],
    ["-", "-.", ":"],
):
    ax.plot(
        u_grid,
        pinball_curves[tau_level],
        color=color,
        linewidth=1.1,
        linestyle=line_style,
        label=rf"$\tau = {tau_level}$",
    )

ax.plot(
    u_grid,
    squared_comparison,
    color=PALETTE["muted"],
    linewidth=1.6,
    linestyle="--",
    label="squared error (scaled)",
)
ax.axvline(0.0, color=PALETTE["axis"], linewidth=0.8, alpha=0.4)
ax.set_title("Pinball loss versus squared error")
ax.set_xlabel(r"Residual $u = y - q$")
ax.set_ylabel(r"Loss $\rho_\tau(u)$")
ax.set_ylim(-0.6, 2.6)
ax.legend(loc="upper center", bbox_to_anchor=(0.5, -0.18), ncol=2)
polish_axes(ax)
plt.show()
VisualizationOut [4]
CollapseExpand
Two straight-line branches meeting at the origin. The tau equals 0.1 curve is shallow on the right and steep on the left; the tau equals 0.9 curve is the mirror image; the tau equals 0.5 curve is a symmetric V shape, with a parabola plotted for comparison.
Pinball loss at three quantile levels with scaled squared error for contrast. The asymmetry grows as $\tau$ departs from 0.5, and its two constant slopes and zero kink produce the quantile counting condition. For tau above 0.5 the positive branch is steeper, while for tau below 0.5 the negative branch dominates.

Linear quantile regression as an optimization problem

Now we restrict attention to linear quantile functions. For a chosen τ\tau, we model

QY∣X=x(τ)=x⊤β(τ)=β0(τ)+β1(τ)x1+⋯+βp(τ)xp.Q_{Y \mid X = x}(\tau) = x^\top \beta(\tau) = \beta_0(\tau) + \beta_1(\tau) x_1 + \cdots + \beta_p(\tau) x_p.

The convention that x0=1x_0 = 1 absorbs the intercept β0(τ)\beta_0(\tau), so that x⊤β(τ)=∑j=0pβj(τ)xjx^\top \beta(\tau) = \sum_{j=0}^{p} \beta_j(\tau) x_j with a (p+1)(p+1)-dimensional parameter vector.

Here:

  • β(τ)\beta(\tau): the coefficient vector at quantile level τ\tau, of length p+1p + 1, indexed from j=0j = 0 to j=pj = p
  • β0(τ)\beta_0(\tau): the intercept at level τ\tau, multiplying the constant x0=1x_0 = 1
  • βj(τ)\beta_j(\tau): the coefficient on predictor xjx_j at level τ\tau, for j=1,…,pj = 1, \dots, p
  • xjx_j: the jj-th nonconstant predictor, for j=1,…,pj = 1, \dots, p
  • x1,…,xpx_1, \dots, x_p: the pp observed predictors, to which the constant x0=1x_0 = 1 is prepended
  • x∈Rp+1x \in \mathbb{R}^{p+1}: the full predictor vector with the prepended constant, so that x⊤β(τ)x^\top \beta(\tau) is the fitted τ\tau-quantile

The notation emphasizes that the coefficients depend on τ\tau: each quantile level gets its own line. The estimate is the minimizer of the average pinball loss over the sample,

β^(τ)=arg⁡min⁡β∈Rp+11n∑i=1nρτ(yi−xi⊤β).\hat\beta(\tau) = \arg\min_{\beta \in \mathbb{R}^{p+1}} \frac{1}{n} \sum_{i=1}^{n} \rho_\tau\left(y_i - x_i^\top \beta\right).

Compare this to the least-squares objective, which is a smooth quadratic in β\beta and, for a full-column-rank design, admits the mathematical solution (X⊤X)−1X⊤y(X^\top X)^{-1} X^\top y that we derived in ordinary least squares. Here the objective is a sum of piecewise linear functions of β\beta. It is convex (a sum of convex functions is convex) but it has kinks wherever a residual yi−xi⊤βy_i - x_i^\top\beta is exactly zero, and some optima have zero residuals. Flat sets of optimal coefficients can also contain points with no zero residuals. At a kink the derivative does not exist, so "set the gradient to zero and solve" does not work. We need the machinery of convex optimization.

The standard route is to convert the problem into a linear program, which we can then hand to a mature solver. The trick is to split each residual into its positive and negative parts. Define, for every observation, the positive and negative parts of the residual:

ui=max⁡(yi−xi⊤β, 0),vi=max⁡(xi⊤β−yi, 0),u_i = \max\left(y_i - x_i^\top \beta,\, 0\right), \qquad v_i = \max\left(x_i^\top \beta - y_i,\, 0\right),

where:

  • yiy_i: the observed response for the ii-th observation
  • xi⊤βx_i^\top \beta: the fitted quantile at the ii-th predictor vector
  • uiu_i: the positive part of the residual, nonzero only when the observation lies above the fit
  • viv_i: the negative part of the residual, nonzero only when the observation lies below the fit

Both are non-negative, and their difference reconstructs the signed residual: ui−vi=yi−xi⊤βu_i - v_i = y_i - x_i^\top\beta. In the linear program we drop the max definitions and treat ui≥0u_i \ge 0 and vi≥0v_i \ge 0 as the unknowns; the optimal solution automatically sets at most one of the pair strictly positive, since subtracting t=min⁡(ui,vi)>0t=\min(u_i,v_i)>0 from both preserves their difference and lowers the cost by [τ+(1−τ)]t=t[\tau+(1-\tau)]t=t. Now check that the pinball loss can be written as a weighted sum of them. Note that if yi−xi⊤β>0y_i - x_i^\top\beta > 0 then ui=yi−xi⊤βu_i = y_i - x_i^\top\beta and vi=0v_i = 0, while if yi−xi⊤β<0y_i - x_i^\top\beta < 0 then ui=0u_i = 0 and vi=xi⊤β−yiv_i = x_i^\top\beta - y_i. If the residual yi−xi⊤βy_i - x_i^\top\beta is positive then uiu_i equals it, vi=0v_i = 0, and

τui+(1−τ)vi=τ(yi−xi⊤β)=ρτ(yi−xi⊤β).\tau u_i + (1-\tau) v_i = \tau\left(y_i - x_i^\top\beta\right) = \rho_\tau\left(y_i - x_i^\top \beta\right).

If the residual is negative then ui=0u_i = 0, vi=xi⊤β−yiv_i = x_i^\top\beta - y_i, and

τui+(1−τ)vi=(1−τ)(xi⊤β−yi)=(τ−1)(yi−xi⊤β)=ρτ(yi−xi⊤β),\tau u_i + (1-\tau) v_i = (1-\tau)\left(x_i^\top\beta - y_i\right) = (\tau - 1)\left(y_i - x_i^\top\beta\right) = \rho_\tau\left(y_i - x_i^\top \beta\right),

using (1−τ)=−(τ−1)(1-\tau) = -(\tau-1) in the middle step. Either way the identity holds, and the identity is linear in the unknowns β,u,v\beta, u, v. That is precisely why the problem becomes a linear program: we minimize a linear function subject to linear constraints and non-negativity.

Written out in full, for a fixed τ\tau we solve

min⁡β, u, v  ∑i=1n[τui+(1−τ)vi]subject to{xi⊤β+ui−vi=yi,i=1,…,n,ui≥0,vi≥0,i=1,…,n,\min_{\beta,\, u,\, v} \; \sum_{i=1}^{n} \left[\tau u_i + (1-\tau) v_i\right] \quad \text{subject to} \quad \begin{cases} x_i^\top \beta + u_i - v_i = y_i, & i = 1, \dots, n,\\ u_i \ge 0, \quad v_i \ge 0, & i = 1, \dots, n, \end{cases}

where:

  • β\beta: the coefficient vector, unrestricted in sign
  • ui,viu_i, v_i: the positive and negative parts of the ii-th residual, both constrained to be non-negative
  • τ\tau: the target quantile level
  • xix_i: the ii-th predictor vector, including the intercept entry
  • yiy_i: the ii-th observed response

To pass this to a solver we stack everything into one vector. Let XX be the n×(p+1)n \times (p+1) design matrix whose ii-th row is xi⊤x_i^\top, let InI_n denote the n×nn \times n identity matrix, let 1n\mathbf{1}_n be a column of nn ones, let 0p+10_{p+1} be a column of p+1p+1 zeros, and let zz be the stacked unknown vector defined below. Collect the unknowns as z=(β,u,v)∈R(p+1)+2nz = (\beta, u, v) \in \mathbb{R}^{(p+1) + 2n}. Then the program reads

min⁡z  c⊤z\min_{z} \; c^\top z

subject to the equality constraint and the variable-bound restrictions

[XIn−In]z=y,zj free for j=1,…,p+1,zj≥0 for j=p+2,…,(p+1)+2n,\begin{bmatrix} X & I_n & -I_n \end{bmatrix} z = y, \qquad z_j \ \text{free for } j = 1,\dots,p+1, \quad z_j \ge 0 \ \text{for } j = p+2,\dots,(p+1)+2n,

where:

  • z=(β,u,v)z = (\beta, u, v): the stacked vector of unknowns, of length (p+1)+2n(p+1) + 2n
  • c=(0p+1, τ1n, (1−τ)1n)c = \left(0_{p+1},\ \tau \mathbf{1}_n,\ (1-\tau)\mathbf{1}_n\right): the linear cost vector
  • XX: the n×(p+1)n \times (p+1) design matrix
  • InI_n: the n×nn \times n identity matrix
  • yy: the observed response vector

The constraint [XIn−In]z=y\begin{bmatrix} X & I_n & -I_n \end{bmatrix} z = y is the residual decomposition xi⊤β+ui−vi=yix_i^\top\beta + u_i - v_i = y_i in matrix form, and zj≥0z_j \ge 0 enforces non-negativity of the split residuals.

Reading the block structure: the first block XβX\beta produces the fitted values, adding uu and subtracting vv reconciles them with the observed yy, and the cost vector puts weight τ\tau on positive residuals (observations above the fitted line, that is, under-predictions) and weight 1−τ1-\tau on negative residuals (observations below the fitted line, that is, over-predictions). The single equation per observation is exactly the residual decomposition we derived. The problem has 2n+p+12n + p + 1 unknowns and nn equality constraints. That is larger than a least-squares solve. The two identity blocks contain only 2n2n nonzero entries; at fixed predictor count the block matrix becomes sparse as nn grows, and a sparse representation lets solvers exploit this structure.

The dual shows how the fitted score limits sensitivity to extreme responses at fixed predictors. The dual of the program above is

max⁡a∈Rn  y⊤asubject toX⊤a=0,ai∈[τ−1, τ]  for all i,\max_{a \in \mathbb{R}^n} \; y^\top a \quad \text{subject to} \quad X^\top a = 0, \qquad a_i \in [\tau - 1,\ \tau] \ \text{ for all } i,

where:

  • a∈Rna \in \mathbb{R}^n: the dual variable vector, one multiplier per observation
  • aia_i: the dual multiplier for the ii-th observation, confined to the interval [τ−1,τ][\tau - 1, \tau]
  • yy: the observed response vector
  • XX: the n×(p+1)n \times (p+1) design matrix

Each observation gets a multiplier aia_i that is capped between τ−1\tau - 1 and τ\tau. At the solution, ai=τa_i = \tau for observations with positive residual (points above the fitted line) and ai=τ−1a_i = \tau - 1 for observations with negative residual (points below the fitted line), with aia_i taking any value in [τ−1,τ][\tau-1,\tau] when the residual is exactly zero. The constraint X⊤a=0X^\top a = 0 is the first-order condition: the fitted line is positioned so that the signed, capped residuals balance against every column of the design matrix. Every multiplier is confined to the interval [τ−1,τ][\tau - 1, \tau], whose bound on the magnitude is max⁡(τ,1−τ)≤1\max(\tau, 1 - \tau) \le 1. Because aia_i is bounded independently of yiy_i, an extreme response cannot push its own multiplier beyond this cap. The equilibrium condition X⊤a=0X^\top a = 0 balances the capped scores against the design columns; it does not guarantee a bound on finite-sample coefficient changes.

For the intercept column, X0=1nX_0=\mathbf 1_n, stationarity gives ∑iai=0\sum_i a_i=0. Let ri=yi−xi⊤βr_i=y_i-x_i^\top\beta, N−=#{i:ri<0}N_-=\#\{i:r_i<0\} and N0=#{i:ri=0}N_0=\#\{i:r_i=0\}. At nonzero residuals the dual values are fixed at τ\tau or τ−1\tau-1; at zero residuals they may take any value in [τ−1,τ][\tau-1,\tau]. The available zero-residual values balance the intercept precisely when

N−≤nτ≤N−+N0.N_-\le n\tau\le N_-+N_0.

Here N−N_- counts negative residuals, N0N_0 counts ties at the fitted value, and nτn\tau is the target count. This is the same counting inequality derived for a sample quantile. It does not require exactly a fraction τ\tau of residuals to be negative. The full vector condition also needs valid zero-residual multipliers that satisfy X⊤a=0X^\top a=0. We will read those multipliers from the LP result rather than treating an arbitrary endpoint of each subgradient interval as an ordinary gradient.

Mathematical properties

These properties are not decorative. They tell us what quantile regression preserves, what it gives up, and where its finite-sample behavior can differ from the population ideal.

  • Monotone equivariance. Population conditional quantiles commute with continuous strictly increasing transformations on the response domain. A selected empirical order statistic has the same property. A general linear quantile-regression estimator need not commute with nonlinear transformations because the transformed function may leave the linear model family.
  • Median reduction. At τ=0.5\tau = 0.5 the pinball loss is half the absolute error, so the estimator is least absolute deviations regression. For independent Laplace errors centered at zero with a common fixed positive scale, its regression coefficients are maximum likelihood estimates.
  • No ordering guarantee across τ\tau. Because each τ\tau is optimized separately, β^(τ1)\hat\beta(\tau_1) and β^(τ2)\hat\beta(\tau_2) for τ1<τ2\tau_1 < \tau_2 are not constrained to produce ordered fitted values at any given xx. Population quantiles are ordered. Appropriate consistency assumptions can recover that order in the limit, but separate finite-sample fits impose no ordering constraint.
  • Asymptotic normality. For independent identically distributed rows, a correctly specified linear conditional quantile, bounded predictors with nonsingular second-moment matrix, and a positive continuous conditional density with adequate local smoothness near that quantile, the usual regular quantile-regression limit applies. Define the density-weighted design moment DD and the plain design moment JJ by
D=E[fY∣X(Qτ(x)∣x)xx⊤],J=E[xx⊤],D = \mathbb{E}\left[f_{Y\mid X}\left(Q_\tau(x) \mid x\right) x x^\top\right], \qquad J = \mathbb{E}\left[x x^\top\right],

where:

  • fY∣X(⋅∣x)f_{Y\mid X}(\cdot \mid x): the conditional density of YY given X=xX = x, evaluated at the quantile Qτ(x)Q_\tau(x)
  • Qτ(x)=x⊤β(τ)Q_\tau(x) = x^\top\beta(\tau): the true conditional τ\tau-quantile function
  • xx: a predictor vector drawn from the population distribution (the expectation is taken over XX)
  • DD: the density-weighted design moment; density affects precision through the full sandwich covariance, together with the predictor design and τ\tau
  • JJ: the unweighted design moment E[xx⊤]\mathbb{E}[x x^\top]

Then n(β^(τ)−β(τ))→dN(0, τ(1−τ) D−1JD−1)\sqrt{n}\left(\hat\beta(\tau) - \beta(\tau)\right) \xrightarrow{d} \mathcal{N}\left(0,\ \tau(1-\tau)\, D^{-1} J D^{-1}\right) as n→∞n \to \infty, where τ(1−τ)\tau(1-\tau) is the variance of the pinball-loss score, D−1JD−1D^{-1} J D^{-1} is the sandwich matrix (bread times meat times bread), and DD and JJ are defined immediately above. The density term DD records the local information near the quantile. At fixed τ\tau and predictor distribution, multiplying all conditional densities at the quantile by the same factor cc divides the asymptotic covariance by c2c^2; more general precision comparisons depend on the whole density-weighted design. In distributions such as the normal example, low tail density can increase this covariance; the formula does not imply that every distribution has larger variance at more extreme levels.

  • Under these regular population conditions, the influence function is bounded as yy varies at a fixed predictor vector. It need not be bounded as xx varies: high-leverage predictors multiply the capped scores. This population statement does not bound every finite-sample coefficient change.

Example: Small Numbers and Heteroscedastic Predictions

The formulas are compact, but the logic is easiest to trust when we can compute a few quantiles by hand. We start with a single median, then a slope, and finally the heteroscedastic case that motivates the entire method. The numbers are small enough to check on paper.

A single quantile from a handful of numbers

Take five response values y=(2,3,5,8,10)y = (2, 3, 5, 8, 10) and ask for the median, τ=0.5\tau = 0.5. The counting rule says we need a value qq with at most 0.5×5=2.50.5 \times 5 = 2.5 observations strictly below it and at least 2.52.5 at or below it. The value q=5q = 5 has two observations below it (2,3)(2, 3) and three at or below it (2,3,5)(2, 3, 5), so it satisfies the rule. Let us confirm by evaluating the average pinball loss at three candidates.

  • At q=3q = 3 the residuals are u=(−1,0,2,5,7)u = (-1, 0, 2, 5, 7). With τ=0.5\tau = 0.5, ρ0.5(u)=0.5∣u∣\rho_{0.5}(u) = 0.5|u|, so the losses are (0.5,0,1.0,2.5,3.5)(0.5, 0, 1.0, 2.5, 3.5) and their average is 7.5/5=1.57.5/5 = 1.5.
  • At q=5q = 5 the residuals are u=(−3,−2,0,3,5)u = (-3, -2, 0, 3, 5), losses (1.5,1,0,1.5,2.5)(1.5, 1, 0, 1.5, 2.5), average 6.5/5=1.36.5/5 = 1.3.
  • At q=6q = 6 the residuals are u=(−4,−3,−1,2,4)u = (-4, -3, -1, 2, 4), losses (2,1.5,0.5,1,2)(2, 1.5, 0.5, 1, 2), average 7/5=1.47/5 = 1.4.

These values compare the same average loss at three candidate medians.

Here the pinball loss at τ=0.5\tau = 0.5 is ρ0.5(u)=0.5∣u∣\rho_{0.5}(u) = 0.5|u|, and the residual is the signed distance from each observation to the candidate value qq.

where:

  • uu: the residual yi−qy_i - q, the signed distance from each observation to the candidate value qq
  • ρ0.5(u)\rho_{0.5}(u): the pinball loss at τ=0.5\tau = 0.5, which equals 0.5∣u∣0.5|u|
  • qq: the candidate value being evaluated as an estimate of the sample median

The minimum is at q=5q = 5, which is the sample median. Moving qq up from 5 adds loss on the three points below and removes loss on the two above; because there are more points below than above, that trade is unfavorable. The balance point is exactly where the counts cross, which is the counting rule.

A slope through the origin and the weighted-median shortcut

Now consider a one-parameter model with no intercept, Q(τ∣x)=βxQ(\tau \mid x) = \beta x, fitted to four observations, where β\beta is the single slope to be estimated and xx is the predictor:

Ratios and predictor weights used in the weighted-median slope example.
xix_iyiy_iratio ri=yi/xir_i = y_i / x_iweight xix_i
122.001
231.502
393.003
4133.254

Because all xi>0x_i > 0 and the residual in observation ii is yi−βxi=xi(ri−β)y_i - \beta x_i = x_i(r_i - \beta), the pinball loss is positively homogeneous of degree one: for c>0c > 0, ρτ(cu)=c ρτ(u)\rho_\tau(cu) = c\,\rho_\tau(u). Writing yi−βxi=xi(ri−β)y_i - \beta x_i = x_i(r_i - \beta) with xi>0x_i > 0, the objective becomes

∑i=14ρτ(yi−βxi)=∑i=14xi ρτ(ri−β).\sum_{i=1}^{4} \rho_\tau\left(y_i - \beta x_i\right) = \sum_{i=1}^{4} x_i\, \rho_\tau\left(r_i - \beta\right).

where:

  • ri=yi/xir_i = y_i / x_i: the ratio of response to predictor for the ii-th observation
  • xix_i: the ii-th predictor value, which also serves as the weight
  • β\beta: the single slope parameter being estimated
  • ρτ\rho_\tau: the pinball loss at level τ\tau, which is positively homogeneous of degree one

This is a weighted pinball loss in the single unknown β\beta, and its minimizer is the weighted τ\tau-quantile of the ratios rir_i with weights xix_i. Order the ratios and accumulate weight: 1.501.50 with weight 2, then 2.002.00 with weight 1 (cumulative 3), then 3.003.00 with weight 3 (cumulative 6), then 3.253.25 with weight 4 (cumulative 10). For τ=0.5\tau = 0.5 the target cumulative weight is 0.5×10=50.5 \times 10 = 5. The cumulative weight jumps from 33 (after 2.002.00) to 66 (after 3.003.00), so it crosses the target of 55 at the ratio 3.003.00; by the weighted-median convention we take the smallest ratio whose cumulative weight is at least the target, giving β^(0.5)=3\hat\beta(0.5) = 3.

Checking the loss: at β=3\beta = 3 the fitted values are (3,6,9,12)(3, 6, 9, 12) and the residuals are (−1,−3,0,1)(-1, -3, 0, 1), giving an average loss of 0.5(1+3+0+1)/4=0.6250.5(1 + 3 + 0 + 1)/4 = 0.625. At β=2.75\beta = 2.75 the residuals are (−0.75,−2.5,0.75,2)(-0.75, -2.5, 0.75, 2), average loss 0.5(0.75+2.5+0.75+2)/4=0.750.5(0.75 + 2.5 + 0.75 + 2)/4 = 0.75. At β=3.25\beta = 3.25 the residuals are (−1.25,−3.5,−0.75,0)(-1.25, -3.5, -0.75, 0), average loss 0.5(1.25+3.5+0.75+0)/4=0.68750.5(1.25 + 3.5 + 0.75 + 0)/4 = 0.6875. Both neighbours are worse, confirming the minimum. Note also that this weighted-median minimizer applies because all slopes share a single parameter β\beta multiplying positive weights xix_i; the objective is a weighted pinball loss in β\beta whose minimizer is the weighted τ\tau-quantile of the ratios.

PythonIn [5]
CollapseExpand
hand_x = np.array([1.0, 2.0, 3.0, 4.0])
hand_y = np.array([2.0, 3.0, 9.0, 13.0])

ratios = hand_y / hand_x
order = np.argsort(ratios)
cum_weight = np.cumsum(hand_x[order]) / hand_x.sum()
beta_weighted_median = ratios[order][
    np.searchsorted(cum_weight, 0.5, side="left")
]


def pinball_average(y, yhat, tau):
    resid = y - yhat
    return np.mean(np.where(resid >= 0, tau * resid, (tau - 1.0) * resid))


print(f"weighted median of ratios: {beta_weighted_median:.4f}")
for beta in (2.75, 3.00, 3.25):
    print(
        f"  beta = {beta:.2f}   average pinball loss = {pinball_average(hand_y, beta * hand_x, 0.5):.4f}"
    )
ConsoleOut [6]
CollapseExpand
weighted median of ratios: 3.0000
  beta = 2.75   average pinball loss = 0.7500
  beta = 3.00   average pinball loss = 0.6250
  beta = 3.25   average pinball loss = 0.6875

The printed losses reproduce the hand arithmetic exactly: 0.750.75, 0.6250.625, 0.68750.6875.

The shortcut can also be read directly from the objective. Rather than comparing only three candidate slopes, evaluate the average pinball loss over a grid and mark the minimizer.

PythonIn [7]
CollapseExpand
beta_grid_med = np.linspace(1.5, 4.5, 301)
loss_med = np.array(
    [pinball_average(hand_y, b * hand_x, 0.5) for b in beta_grid_med]
)
loss_at_weighted_median = pinball_average(
    hand_y, beta_weighted_median * hand_x, 0.5
)
VisualizationOut [8]
CollapseExpand
Average pinball loss versus slope beta, lowest near beta equals three.
Average pinball loss for the no-intercept slope model as a function of the candidate slope. The curve is piecewise linear and reaches its minimum at the weighted median of the ratios, beta = 3, confirming that the pinball objective is minimized at a weighted quantile of the ratios. The straight segments meet at kinks as residual signs change; this example has a unique minimum.

This shortcut is worth remembering. For a single slope, no intercept, and positive predictors, the objective reduces to a weighted τ\tau-quantile of the ratios ri=yi/xir_i=y_i/x_i with weights xix_i. At τ=0.5\tau=0.5 this is a weighted median. That is why quantile regression sometimes feels like a ranking operation rather than a fitting operation.

Heteroscedastic worked predictions

Now the case that matters. Take three illustrative quantile lines with the coefficients below. These are chosen for hand arithmetic, not claimed solver output from the later simulated dataset. Over 0≤x≤100\le x\le10 their ordering gives a widening nominal 10th-to-90th-percentile band:

Illustrative coefficients for three quantile lines used in the hand width calculation.
τ\tauintercept β0(τ)\beta_0(\tau)slope β1(τ)\beta_1(\tau)
0.11.000.60
0.52.001.10
0.93.602.00

The fitted curves are Q(0.1∣x)=1.0+0.6xQ(0.1 \mid x) = 1.0 + 0.6x, Q(0.5∣x)=2.0+1.1xQ(0.5 \mid x) = 2.0 + 1.1x, and Q(0.9∣x)=3.6+2.0xQ(0.9 \mid x) = 3.6 + 2.0x, where:

  • Q(τ∣x)Q(\tau \mid x): the fitted conditional τ\tau-quantile at predictor value xx
  • β0(τ)\beta_0(\tau): the intercept of the fitted line at level τ\tau
  • β1(τ)\beta_1(\tau): the slope of the fitted line at level τ\tau

Evaluate them at three values of xx and record the width of the central 80 percent band, which is defined as W(x)=Q(0.9∣x)−Q(0.1∣x)W(x) = Q(0.9 \mid x) - Q(0.1 \mid x), where Q(τ∣x)Q(\tau \mid x) is the fitted conditional τ\tau-quantile and W(x)W(x) is the resulting band width in response units:

  • At x=1x = 1: the three values are 1.61.6, 3.13.1, 5.65.6, so W(1)=5.6−1.6=4.0W(1) = 5.6 - 1.6 = 4.0.
  • At x=5x = 5: the values are 4.04.0, 7.57.5, 13.613.6, so W(5)=13.6−4.0=9.6W(5) = 13.6 - 4.0 = 9.6.
  • At x=10x = 10: the values are 7.07.0, 13.013.0, 23.623.6, so W(10)=23.6−7.0=16.6W(10) = 23.6 - 7.0 = 16.6.

The width is not constant: because both bounding curves are linear, the width is exactly linear in xx. Subtracting the two fitted lines Q^(0.9∣x)=β0(0.9)+β1(0.9) x\hat Q(0.9 \mid x) = \beta_0(0.9) + \beta_1(0.9)\,x and Q^(0.1∣x)=β0(0.1)+β1(0.1) x\hat Q(0.1 \mid x) = \beta_0(0.1) + \beta_1(0.1)\,x term by term,

W(x)=[β0(0.9)−β0(0.1)]+[β1(0.9)−β1(0.1)]x=(3.60−1.00)+(2.00−0.60) x=2.60+1.40 x,\begin{aligned} W(x) &= \left[\beta_0(0.9) - \beta_0(0.1)\right] + \left[\beta_1(0.9) - \beta_1(0.1)\right] x \\ &= (3.60 - 1.00) + (2.00 - 0.60)\,x \\ &= 2.60 + 1.40\,x, \end{aligned}

which reproduces 4.04.0, 9.69.6, and 16.616.6 at x=1,5,10x = 1, 5, 10 (for example, W(5)=2.60+1.40×5=9.60W(5) = 2.60 + 1.40 \times 5 = 9.60). The slope difference 1.401.40 is the rate at which the conditional spread grows: each additional unit of xx widens the 10-to-90 range by 1.40 units. An additional constant-variance normal residual model would produce a constant-width band. Least squares itself does not force that variance model.

We can also price individual predictions with the pinball loss. Suppose a new observation arrives with x=5x = 5 and y=13.0y = 13.0. The τ=0.9\tau = 0.9 prediction is 13.613.6, so the residual is u=13.0−13.6=−0.6u = 13.0 - 13.6 = -0.6. Because u<0u < 0 we use the lower branch: ρ0.9(−0.6)=(0.9−1)(−0.6)=(−0.1)(−0.6)=0.06\rho_{0.9}(-0.6) = (0.9 - 1)(-0.6) = (-0.1)(-0.6) = 0.06. Overpredicting this response at a high quantile is cheap because the lower branch of ρτ\rho_\tau has slope −(1−τ)=−0.1-(1-\tau) = -0.1 (in magnitude 0.10.1), so a negative residual of size 0.60.6 is penalized by only 0.1×0.6=0.060.1 \times 0.6 = 0.06; a true 0.9 quantile of a continuous distribution has 90 percent of its probability below it. Our illustrative coefficients carry no empirical calibration guarantee; the small penalty here follows from the chosen loss. Now take y=7.0y = 7.0 at the same xx and score it against the τ=0.1\tau = 0.1 line, whose prediction is 4.04.0. The residual is 3.0>03.0 > 0, so ρ0.1(3.0)=0.1×3.0=0.30\rho_{0.1}(3.0) = 0.1 \times 3.0 = 0.30. The two residuals, −0.6-0.6 and +3.0+3.0, are not of comparable size, so this comparison mainly illustrates the pricing rule: the pinball loss charges τ\tau per unit on the positive side and 1−τ1 - \tau per unit on the negative side, with both rates evaluated at the relevant τ\tau.

PythonIn [9]
CollapseExpand
hand_coefs = {0.1: (1.00, 0.60), 0.5: (2.00, 1.10), 0.9: (3.60, 2.00)}
rows = []
for x0 in (1.0, 5.0, 10.0):
    q_lo = hand_coefs[0.1][0] + hand_coefs[0.1][1] * x0
    q_mid = hand_coefs[0.5][0] + hand_coefs[0.5][1] * x0
    q_hi = hand_coefs[0.9][0] + hand_coefs[0.9][1] * x0
    rows.append(
        {
            "x": x0,
            "Q(0.1|x)": q_lo,
            "Q(0.5|x)": q_mid,
            "Q(0.9|x)": q_hi,
            "width": q_hi - q_lo,
        }
    )
width_check = pd.DataFrame(rows).round(2)
width_check["formula width"] = (2.60 + 1.40 * width_check["x"]).round(2)
width_check
Out[10]:
x Q(0.1|x) Q(0.5|x) Q(0.9|x) width formula width
0 1.0 1.6 3.1 5.6 4.0 4.0
1 5.0 4.0 7.5 13.6 9.6 9.6
2 10.0 7.0 13.0 23.6 16.6 16.6

The two width columns agree, which is the arithmetic identity W(x)=2.60+1.40xW(x) = 2.60 + 1.40x restated in code. With the mechanics settled, we can now build the estimator properly and fit it to a realistic sample.

Implementation in NumPy, SciPy, and scikit-learn

We build the linear program from first principles with NumPy and SciPy, because the point of this chapter is to see the mechanism rather than to call it. Only after our own solver reproduces the reference fit do we bring in scikit-learn's QuantileRegressor, which solves an equivalent unpenalized problem with the HiGHS backend when configured with the settings used below.

We start with a synthetic dataset that is deliberately heteroscedastic: the response is linear in xx, and the noise scales up with xx. This is a best case for quantile regression, since the conditional quantiles really are linear, so any coverage shortfall we observe later comes from estimation noise rather than misspecification.

PythonIn [11]
CollapseExpand
rng = default_rng(20240517)

n_total = 900
x_all = rng.uniform(0.0, 10.0, size=n_total)
sigma_all = 0.6 + 0.45 * x_all  # spread grows with x
y_all = 2.0 + 1.1 * x_all + sigma_all * rng.standard_normal(n_total)

perm = rng.permutation(n_total)
train_rows, test_rows = perm[:600], perm[600:]


def add_intercept(x):
    return np.column_stack([np.ones_like(x), x])


X_tr, y_tr = add_intercept(x_all[train_rows]), y_all[train_rows]
X_te, y_te = add_intercept(x_all[test_rows]), y_all[test_rows]
ConsoleOut [12]
CollapseExpand
training rows: 600, test rows: 300, parameters: 2
train noise scale ranges from 0.60 to 5.07

Two functions carry the whole method: the pinball loss for scoring, and the linear program for fitting. We write the constraint matrix in the block form [XIn−In]\begin{bmatrix} X & I_n & -I_n\end{bmatrix} derived above, and let SciPy's HiGHS interface choose its supported LP algorithm.

PythonIn [13]
CollapseExpand
def pinball_loss(y, yhat, tau):
    """Average sample pinball loss for supplied predictions; its population counterpart targets the conditional tau-quantile."""
    resid = y - yhat
    return float(
        np.mean(np.where(resid >= 0.0, tau * resid, (tau - 1.0) * resid))
    )


def fit_quantile_lp(X, y, tau, return_result=False):
    """Solve min sum(tau*u + (1-tau)*v) s.t. X beta + u - v = y, u, v >= 0."""
    n, p = X.shape
    cost = np.concatenate(
        [np.zeros(p), tau * np.ones(n), (1.0 - tau) * np.ones(n)]
    )
    A_eq = np.hstack([X, np.eye(n), -np.eye(n)])
    bounds = [(None, None)] * p + [(0.0, None)] * (2 * n)
    result = linprog(cost, A_eq=A_eq, b_eq=y, bounds=bounds, method="highs")
    if not result.success:
        raise RuntimeError(f"linear program failed: {result.message}")
    beta = result.x[:p]
    return (beta, result) if return_result else beta

We fit seven quantile levels on the training data. Each call solves one linear program. Running more levels adds their individual solve costs; those solve costs can differ across levels.

PythonIn [14]
CollapseExpand
taus = np.array([0.05, 0.10, 0.25, 0.50, 0.75, 0.90, 0.95])
betas = {tau: fit_quantile_lp(X_tr, y_tr, tau) for tau in taus}

coef_table = pd.DataFrame(
    {
        "intercept": [betas[t][0] for t in taus],
        "slope": [betas[t][1] for t in taus],
    },
    index=pd.Index([f"{t:g}" for t in taus], name="tau"),
).round(3)
coef_table
Out[15]:
intercept slope
tau
0.05 1.126 0.362
0.1 1.106 0.570
0.25 1.504 0.831
0.5 2.074 1.058
0.75 2.485 1.385
0.9 2.443 1.794
0.95 2.815 1.906

Read the table as a description of a stretching distribution. The true data-generating quantiles are Qτ(x)=2+1.1x+(0.6+0.45x)Φ−1(τ)Q_\tau(x)=2+1.1x+(0.6+0.45x)\Phi^{-1}(\tau), so their slopes are 1.1+0.45Φ−1(τ)1.1+0.45\Phi^{-1}(\tau). At levels 0.05 and 0.95 these are approximately 0.360 and 1.840. The fitted slopes are estimates and their intercepts need not be monotone; we check the resulting predictions for crossings rather than inferring order from their coefficient table. One fitted sample also does not estimate the sampling variability of its coefficients.

The coefficient table can be read more directly by comparing its slope column with the true slope function. The next figure does that at the fitted levels, using the already computed coefficient vectors.

PythonIn [16]
CollapseExpand
tau_slope_grid = np.linspace(0.05, 0.95, 201)
true_slope_curve = 1.1 + 0.45 * norm.ppf(tau_slope_grid)
fitted_slopes = np.array([betas[t][1] for t in taus])
true_slopes_at_fits = 1.1 + 0.45 * norm.ppf(taus)
VisualizationOut [17]
CollapseExpand
Estimated slope points against tau lie close to a smooth increasing true slope curve.
Fitted quantile slopes at seven levels compared with the true slope function implied by the heteroscedastic Gaussian generator. The slopes rise with tau, and the fitted points lie close to the true curve, showing that separate level fits recover the growing conditional spread as the noise scales up with the predictor. Each level is estimated by its own linear program, yet the estimates follow the same underlying pattern.

The helper normally returns only coefficients. Setting return_result=True also returns SciPy's successful LP result so we can inspect its actual equality-constraint dual multipliers. We check the counting inequality with a stated numerical zero tolerance, the full stationarity condition, the dual bounds, and the primal-dual objective gap.

PythonIn [18]
CollapseExpand
check_rows = []
for tau in (0.10, 0.50, 0.90):
    beta, lp_result = fit_quantile_lp(X_tr, y_tr, tau, return_result=True)
    resid = y_tr - X_tr @ beta
    zero_tol = 1e-7 * max(1.0, float(np.max(np.abs(y_tr))))
    n_negative = int(np.sum(resid < -zero_tol))
    n_zero = int(np.sum(np.abs(resid) <= zero_tol))
    dual = lp_result.eqlin.marginals
    stationarity = float(np.max(np.abs(X_tr.T @ dual)))
    dual_bounds_ok = bool(
        np.all(dual >= tau - 1 - 1e-8) and np.all(dual <= tau + 1e-8)
    )
    count_ok = bool(n_negative <= len(y_tr) * tau <= n_negative + n_zero)
    objective_gap = float(abs(lp_result.fun - y_tr @ dual) / len(y_tr))
    assert (
        count_ok
        and dual_bounds_ok
        and stationarity < 1e-7
        and objective_gap < 1e-7
    )
    check_rows.append(
        {
            "tau": tau,
            "negative residuals": n_negative,
            "near-zero residuals": n_zero,
            "count inequality": count_ok,
            "max stationarity residual": stationarity,
            "mean primal-dual gap": objective_gap,
        }
    )
pd.DataFrame(check_rows)
Out[19]:
tau negative residuals near-zero residuals count inequality max stationarity residual mean primal-dual gap
0 0.1 59 2 True 5.224987e-13 1.042129e-15
1 0.5 299 2 True 4.574119e-14 3.789561e-16
2 0.9 539 2 True 4.218847e-15 8.526513e-16

The zero tolerance classifies residuals near a fitted hyperplane; it is not a license to assign them an arbitrary gradient. The returned dual values satisfy the permitted intervals and balance all design columns. Primal feasibility from the LP, dual feasibility, and agreement of their objectives provide an optimality check to the stated solver precision, while leaving coefficient uniqueness as a separate question.

Now the reference implementation. scikit-learn's QuantileRegressor minimizes the same objective with an optional ℓ1\ell_1 penalty on the coefficients; setting alpha=0 removes the penalty, and fit_intercept=False prevents it from adding a second intercept column to the one we already built.

PythonIn [20]
CollapseExpand
reference = QuantileRegressor(
    quantile=0.5, alpha=0.0, fit_intercept=False, solver="highs"
)
reference.fit(X_tr, y_tr)

gap = np.max(np.abs(reference.coef_ - betas[0.5]))
ConsoleOut [21]
CollapseExpand
our intercept, slope:      2.073922,  1.057594
scikit-learn intercept, slope:  2.073922,  1.057594
largest absolute difference: 8.88e-16

The two fits agree to solver precision for this sample. They minimize the same unpenalized objective. When its optimum is non-unique, different solvers can return different optimal coefficient vectors even without numerical error.

With the model in hand, we evaluate it on the held-out 300 observations. We report the pinball loss at each level and, more importantly for this chapter, the empirical coverage of two nominal prediction bands. The test-share-below-fit column is a per-level marginal calibration check; the two nominal bands aggregate that check over τ=0.05\tau=0.05 and 0.950.95 and over τ=0.25\tau=0.25 and 0.750.75 respectively.

PythonIn [22]
CollapseExpand
grid_x = np.linspace(0.0, 10.0, 201)
G = add_intercept(grid_x)

pred_te = {tau: X_te @ betas[tau] for tau in taus}
grid_pred = {tau: G @ betas[tau] for tau in taus}

eval_table = pd.DataFrame(
    {
        "tau": taus,
        "test pinball loss": [pinball_loss(y_te, pred_te[t], t) for t in taus],
        "test share below fit": [
            float(np.mean(y_te <= pred_te[t])) for t in taus
        ],
    }
).round(4)
eval_table
Out[23]:
tau test pinball loss test share below fit
0 0.05 0.3182 0.0767
1 0.10 0.5520 0.1200
2 0.25 0.9933 0.2767
3 0.50 1.2191 0.4733
4 0.75 0.9508 0.7567
5 0.90 0.5269 0.9167
6 0.95 0.3153 0.9467

The third column is a per-level marginal calibration check: for accurate quantiles in a continuous model its population target is τ\tau. It is not a histogram of a fully estimated probability integral transform. Deviations are sampling noise plus any estimation error in β(τ)\beta(\tau). Our linear form is correctly specified here, so there is no model-form approximation error; the deviations happen to be small in this sample. Turning those numbers into band coverage:

PythonIn [24]
CollapseExpand
cover_90 = float(np.mean((y_te >= pred_te[0.05]) & (y_te <= pred_te[0.95])))
cover_50 = float(np.mean((y_te >= pred_te[0.25]) & (y_te <= pred_te[0.75])))

ols_beta, *_ = np.linalg.lstsq(X_tr, y_tr, rcond=None)
ols_sigma = float(np.sqrt(np.mean((y_tr - X_tr @ ols_beta) ** 2)))
ols_halfwidth = float(norm.ppf(0.95) * ols_sigma)
ConsoleOut [25]
CollapseExpand
nominal 90% band  -> empirical coverage 0.870
nominal 50% band  -> empirical coverage 0.480
OLS constant 90% half-width (normal assumption): 5.25

The 90 percent band covers close to 90 percent of held-out responses and the 50 percent band close to 50 percent. This is a good outcome for this sample. The model form matches the data-generating process, and the independent holdout provides an honest check; neither fact guarantees that a fitted band will attain nominal coverage in a finite sample. The last line foreshadows the contrast we will draw visually: the OLS band has one constant half-width, while ours varies with xx.

Key Parameters

The quantile level τ\tau is the parameter that defines the model, not a tuning knob in the usual sense. It selects which feature of the conditional distribution we estimate, and different values of τ\tau produce different coefficient vectors that are not required to be ordered. Choosing τ\tau is a decision-theoretic act: pick the level that appears in your cost structure or your reporting requirement, such as 0.95 for a service-level target or 0.05 for a lower reference limit.

For fit_intercept=True, QuantileRegressor adds α∑j=1p∣βj∣\alpha\sum_{j=1}^p|\beta_j| to the mean pinball loss and leaves its separately fitted intercept unpenalized. If we instead supply a constant column with fit_intercept=False, that column is treated as an ordinary coefficient and is penalized when alpha>0. Our comparison uses alpha=0, so no coefficient is penalized. This is the ℓ1\ell_1 regularization studied in lasso regularization. It shrinks coefficients toward zero and can zero out predictors entirely, which is useful when the number of predictors is large relative to the sample. Because the penalty acts on coefficient magnitudes, features must be on comparable scales for it to be meaningful; standardize first, as described in standardization.

The solver argument selects the underlying optimizer. "highs" is the default; practical solve time depends on the row and feature counts, sparsity, conditioning, and available hardware. The "highs-ds" and "highs-ipm" methods choose the dual simplex and interior-point variants, which can behave differently on degenerate designs. The fit_intercept flag should be set to False whenever we supply our own intercept column, to avoid fitting a redundant second intercept.

Key Methods

fit(X, y) builds and solves the linear program and stores the coefficient vector in coef_, with an intercept in intercept_ when fit_intercept=True. predict(X) evaluates the fitted linear quantile function at new rows; there is no link function and no inverse link, because the fitted conditional quantile is evaluated directly on the response scale used in training.

We compute the pinball loss ourselves rather than relying on a built-in score. The pinball loss is not a proportion of variance explained, and interpreting it requires knowing τ\tau. Reporting an R2R^2-like number at an arbitrary quantile invites confusion. The loss is in the units of yy, so a value of 0.300.30 at τ=0.1\tau = 0.1 means the average weighted residual magnitude is 0.300.30 response units, not 3030 percent of anything. For inference on the coefficients (standard errors, confidence intervals, joint tests across τ\tau), the usual practice is a bootstrap over observations, refitting every τ\tau within each replicate so that the correlation between levels is preserved.

Visualizing Quantile Regression

A table of coefficients is a weak instrument for understanding a quantile fit. The signature picture is the fan: the data, with the fitted quantile curves drawn over them. Where the fitted fan opens up, the selected quantile gaps increase. Parallel fitted levels have constant gaps, though finitely many levels cannot establish the shape of the entire conditional distribution.

PythonIn [26]
CollapseExpand
from book_plot_style import PALETTE, polish_axes, use_book_style

use_book_style()

fig, ax = plt.subplots(figsize=(6.0, 4.0))
ax.scatter(
    x_all[train_rows],
    y_tr,
    s=9,
    color=PALETTE["muted"],
    alpha=0.30,
    label="training data",
)
ax.fill_between(
    grid_x,
    grid_pred[0.05],
    grid_pred[0.95],
    color=PALETTE["primary"],
    alpha=0.18,
    label="5 to 95 percent band",
)
ax.fill_between(
    grid_x,
    grid_pred[0.25],
    grid_pred[0.75],
    color=PALETTE["amber"],
    alpha=0.28,
    label="25 to 75 percent band",
)
ax.plot(
    grid_x,
    grid_pred[0.50],
    color=PALETTE["muted"],
    linewidth=1.1,
    label=r"median, $\tau=0.5$",
)
ax.set_title("Fitted quantile curves form a widening fan")
ax.set_xlabel("Predictor $x$")
ax.set_ylabel("Response $y$")
ax.legend(loc="upper left")
polish_axes(ax)
plt.show()
VisualizationOut [26]
CollapseExpand
Scatter of six hundred points rising from left to right with increasing vertical spread, overlaid with a fan of shaded bands that are narrow near x equals zero and wide near x equals ten, plus a central solid median line.
Fitted quantile curves on the training data. The shading marks the central 50 and 90 percent bands, and the solid line is the median fit. The bands widen with $x$ because the response spread widens with $x$, so quantile regression captures the heteroscedasticity directly, whereas the separate constant-variance plug-in reference used below has one band width. The fan shape makes the change in conditional spread visible without fitting a separate variance model.

The response generator uses symmetric Gaussian noise, so its true conditional mean and median coincide. Any difference between their fitted lines is a finite-sample result. In the executed fit, the 90 percent band width grows from about 1.69 at x=0x=0 to 17.13 at x=10x=10; the 50 percent width grows from about 0.98 to 6.52. Those estimates are separate from the illustrative coefficients in the hand calculation.

The width of the bands is linear in xx, and it is worth isolating that fact. Plotting width against xx makes the comparison with a constant-variance model concrete.

PythonIn [27]
CollapseExpand
fitted_width_90 = grid_pred[0.95] - grid_pred[0.05]
fitted_width_50 = grid_pred[0.75] - grid_pred[0.25]
ols_plugin_width = 2.0 * ols_halfwidth
PythonIn [28]
CollapseExpand
from book_plot_style import PALETTE, polish_axes, use_book_style

use_book_style()

fig, ax = plt.subplots(figsize=(6.0, 4.0))
ax.plot(
    grid_x,
    fitted_width_90,
    color=PALETTE["primary"],
    linewidth=1.1,
    label="quantile band width, 5 to 95",
)
ax.plot(
    grid_x,
    fitted_width_50,
    color=PALETTE["amber"],
    linewidth=1.1,
    linestyle=":",
    label="quantile band width, 25 to 75",
)
ax.axhline(
    ols_plugin_width,
    color=PALETTE["muted"],
    linewidth=1.8,
    linestyle="--",
    label="OLS band width (constant variance)",
)
ax.set_title("Fitted quantile widths and a constant-width reference")
ax.set_xlabel("Predictor $x$")
ax.set_ylabel("Band width in response units")
ax.legend(loc="upper left")
polish_axes(ax)
plt.show()
VisualizationOut [28]
CollapseExpand
Three lines plotted against x: a steeply rising line for the 90 percent quantile band width reaching about seventeen, a rising 50 percent width reaching about 6.52, and a flat dashed plug-in reference at about 10.49.
Width of the fitted bands as a function of the predictor. The fitted 5 to 95 percent width grows from about 1.69 to 17.13 units, and the 25 to 75 percent width grows from about 0.98 to 6.52, while a separate constant-variance normal plug-in reference stays fixed at about 10.49. The widening widths show the estimated conditional spread growing with $x$, in contrast with the flat reference line. These are computed fitted summaries, not finite-sample coverage guarantees.

The dashed line uses one residual standard deviation for every predictor value. For this heteroscedastic generator it is too wide relative to the true conditional band at small xx and too narrow at large xx. An overly wide band includes more ordinary responses; an overly narrow one excludes more. The fitted quantile widths vary with xx, but their finite-sample accuracy still needs checking. An OLS mean model could instead be paired with a separate conditional-variance model.

Quantile crossing, interval coverage, and limitations

Each τ\tau was fit by a separate linear program, and none of those programs knew about the others. Nothing in our procedure enforces Q^(τ1∣x)≤Q^(τ2∣x)\hat Q(\tau_1 \mid x) \le \hat Q(\tau_2 \mid x) for τ1<τ2\tau_1 < \tau_2. Even this 600-row fit can have adjacent-level crossings near the edge of the predictor range. The displayed 50 and 90 percent band endpoints are separate comparisons; their ordering does not establish order among every fitted level.

The algebra of a crossing is simple. Two fitted lines

Q^(τ1∣x)=β^0(τ1)+β^1(τ1)x,Q^(τ2∣x)=β^0(τ2)+β^1(τ2)x\hat Q(\tau_1 \mid x) = \hat\beta_0(\tau_1) + \hat\beta_1(\tau_1) x, \qquad \hat Q(\tau_2 \mid x) = \hat\beta_0(\tau_2) + \hat\beta_1(\tau_2) x

intersect where the two right-hand sides are equal. Collecting terms in xx gives

[β^0(τ2)−β^0(τ1)]+[β^1(τ2)−β^1(τ1)]x=0,\left[\hat\beta_0(\tau_2) - \hat\beta_0(\tau_1)\right] + \left[\hat\beta_1(\tau_2) - \hat\beta_1(\tau_1)\right] x = 0,

so the crossing point is

x⋆=β^0(τ1)−β^0(τ2)β^1(τ2)−β^1(τ1),x^\star = \frac{\hat\beta_0(\tau_1) - \hat\beta_0(\tau_2)}{\hat\beta_1(\tau_2) - \hat\beta_1(\tau_1)},

where:

  • x⋆x^\star: the value of the predictor at which the two fitted quantile lines intersect
  • τ1<τ2\tau_1 < \tau_2: the two quantile levels whose curves are compared
  • β^0(τk)\hat\beta_0(\tau_k): the estimated intercept at level τk\tau_k
  • β^1(τk)\hat\beta_1(\tau_k): the estimated slope at level τk\tau_k

provided the slopes differ. If the slopes are equal the lines are parallel and either never cross or coincide. For an example, suppose two independently fitted lines returned Q^(0.4∣x)=3.0+1.0x\hat Q(0.4 \mid x) = 3.0 + 1.0x and Q^(0.6∣x)=3.4+0.9x\hat Q(0.6 \mid x) = 3.4 + 0.9x. Then x⋆=(3.0−3.4)/(0.9−1.0)=(−0.4)/(−0.1)=4x^\star = (3.0 - 3.4)/(0.9 - 1.0) = (-0.4)/(-0.1) = 4, and, consistently, substituting x=4x = 4 into both lines gives 3.0+1.0×4=3.4+0.9×4=7.03.0 + 1.0 \times 4 = 3.4 + 0.9 \times 4 = 7.0. To the left of x=4x = 4 the 40 percent curve sits below the 60 percent curve, as it must; to the right the ordering reverses, which is impossible for a valid conditional distribution since Q(0.4∣x)≤Q(0.6∣x)Q(0.4 \mid x) \le Q(0.6 \mid x) must hold for every xx.

The practical remedy is rearrangement. At any fixed xx, take the raw fitted values across all estimated levels and sort them into increasing order. Formally, if we have fitted curves at levels τ1<⋯<τK\tau_1 < \dots < \tau_K, let Q^raw(τk∣x)\hat Q_{\text{raw}}(\tau_k \mid x) denote the raw fitted value at level τk\tau_k. Define the rearranged value Q^re(τk∣x)\hat Q_{\text{re}}(\tau_k \mid x) as the kk-th smallest of the raw fitted values {Q^raw(τ1∣x),…,Q^raw(τK∣x)}\{\hat Q_{\text{raw}}(\tau_1 \mid x), \dots, \hat Q_{\text{raw}}(\tau_K \mid x)\} at that xx, so that Q^re(τ1∣x)≤⋯≤Q^re(τK∣x)\hat Q_{\text{re}}(\tau_1 \mid x) \le \dots \le \hat Q_{\text{re}}(\tau_K \mid x). In words, Q^re(τk∣x)\hat Q_{\text{re}}(\tau_k \mid x) is the value that a sorted version of the raw fits places in kk-th position.

where:

  • Q^raw(τk∣x)\hat Q_{\text{raw}}(\tau_k \mid x): the raw, independently fitted value at level τk\tau_k and covariate xx, before any reordering
  • Q^re(τk∣x)\hat Q_{\text{re}}(\tau_k \mid x): the rearranged value at level τk\tau_k and covariate xx, obtained by sorting the raw fits across levels at that xx
  • KK: the number of estimated quantile levels

Sorting happens separately at each fixed xx, so rearrangement changes only the assignment of raw values to levels, not the set of values themselves. Sorting is monotone by construction, so the rearranged curves cannot cross. Chernozhukov, Fernández-Val and Galichon (2010) establish finite-sample improvements in function-distance measures for rearrangement toward a monotone target. This does not promise a better estimate at every individual level or a finite-sample coverage guarantee. Here sorting is used to repair the order of our finite set of fitted values. Rearrangement is a patch, not a cure: the underlying problem is that independent estimation ignores the coupling between levels, and a joint estimator with explicit noncrossing constraints avoids crossings on the predictor domain covered by those constraints, at the cost of a larger coupled optimization problem.

PythonIn [29]
CollapseExpand
small_taus = np.array([0.20, 0.40, 0.50, 0.60, 0.80])
crossing_rows, small_fits = [], {}
for n_small in (25, 50, 100, 200):
    B_small = np.column_stack(
        [fit_quantile_lp(X_tr[:n_small], y_tr[:n_small], t) for t in small_taus]
    )
    P_small = G @ B_small
    small_fits[n_small] = P_small
    crossing_rows.append(
        {
            "training rows": n_small,
            "crossing adjacent-level pairs": int(
                np.sum(np.diff(P_small, axis=1) < 0)
            ),
        }
    )
crossing_table = pd.DataFrame(crossing_rows)
crossing_table
Out[30]:
training rows crossing adjacent-level pairs
0 25 85
1 50 24
2 100 5
3 200 7

Each row counts the (grid point, adjacent level) pairs where the fitted curve at the lower level sits above the fitted curve at the higher level. These are diagnostics for four nested subsets, not a proof that crossings decrease whenever sample size increases. Finite-sample uncertainty and misspecification can both produce crossings. Rearranging the 25-row fit removes every crossing by construction:

PythonIn [31]
CollapseExpand
raw_25 = small_fits[25]
sorted_25 = np.sort(raw_25, axis=1)
print(
    f"crossings before rearrangement: {int(np.sum(np.diff(raw_25, axis=1) < 0))}"
)
print(
    f"crossings after  rearrangement: {int(np.sum(np.diff(sorted_25, axis=1) < 0))}"
)
print(
    f"average absolute change from sorting: {np.mean(np.abs(sorted_25 - raw_25)):.3f} response units"
)
ConsoleOut [32]
CollapseExpand
crossings before rearrangement: 85
crossings after  rearrangement: 0
average absolute change from sorting: 0.017 response units

The second count is zero for a reason that requires no data at all: sorting guarantees it. The third number tells us the price of the repair. When the raw curves barely cross, rearrangement changes almost nothing; when they cross badly, it moves the fitted values and the resulting loss changes accordingly.

PythonIn [33]
CollapseExpand
line_lo = 3.0 + 1.0 * grid_x
line_hi = 3.4 + 0.9 * grid_x
cross_x = (3.0 - 3.4) / (0.9 - 1.0)
cross_y = 3.0 + cross_x
PythonIn [34]
CollapseExpand
from book_plot_style import PALETTE, polish_axes, use_book_style

use_book_style()

fig, ax = plt.subplots(figsize=(6.0, 4.0))

ax.plot(
    grid_x,
    line_lo,
    color=PALETTE["primary"],
    linewidth=1.1,
    label=r"$\hat Q(0.4 \mid x)$",
)
ax.plot(
    grid_x,
    line_hi,
    color=PALETTE["amber"],
    linewidth=1.1,
    linestyle="--",
    label=r"$\hat Q(0.6 \mid x)$",
)
ax.scatter(
    [cross_x],
    [cross_y],
    s=70,
    color=PALETTE["muted"],
    zorder=5,
    label=r"crossing at $x^\star = 4$",
)
ax.set_title("Independently fitted quantile lines can cross")
ax.set_xlabel("Predictor $x$")
ax.set_ylabel("Fitted quantile")
ax.legend(loc="upper left")
polish_axes(ax)
plt.show()
VisualizationOut [34]
CollapseExpand
Two straight lines, one for tau 0.4 and one for tau 0.6, intersecting at x equals 4, with the lower quantile rising above the upper one to the right of the crossing, marked with a dot.
Two independently fitted quantile lines that cross at $x = 4$, a configuration no genuine conditional distribution can produce. To the left of the crossing the 40 percent curve sits below the 60 percent curve, and to the right the ordering reverses, with the 40 percent curve above the 60 percent curve. Rearrangement at each $x$ sorts the fitted values across levels and removes such crossings by construction, though it changes the fitted values themselves.

Now to coverage, which is the other place nominal statements can mislead. A band labelled "90 percent" is a claim about new observations: if we repeatedly draw xx and yy from the population and check whether yy falls inside [Q^(0.05∣x),Q^(0.95∣x)]\left[\hat Q(0.05 \mid x), \hat Q(0.95 \mid x)\right], we would like the long-run frequency to be 0.90. Our held-out estimate was close to nominal, but that is a result to be measured, not assumed. The per-level calibration plot makes the check visual. For each level τ\tau, plot the empirical fraction of held-out responses at or below the fitted τ\tau-quantile against τ\tau itself. A calibrated fit lies on the diagonal.

PythonIn [35]
CollapseExpand
from book_plot_style import PALETTE, polish_axes, use_book_style

use_book_style()

fig, ax = plt.subplots(figsize=(6.0, 4.0))
ax.plot(
    [0.0, 1.0],
    [0.0, 1.0],
    color=PALETTE["muted"],
    linewidth=1.2,
    linestyle="--",
    label="perfect calibration",
)
ax.plot(
    taus,
    eval_table["test share below fit"],
    marker="o",
    color=PALETTE["primary"],
    linewidth=1.0,
    markersize=6,
    label="fitted quantile curves",
)
ax.set_title("Held-out calibration of the quantile curves")
ax.set_xlabel(r"Nominal level $\tau$")
ax.set_ylabel("Share of test responses below the fitted quantile")
ax.legend(loc="upper left")
polish_axes(ax)
plt.show()
VisualizationOut [35]
CollapseExpand
Scatter of seven points close to a dashed forty-five degree diagonal line, with nominal level on the horizontal axis and observed share below the fitted quantile on the vertical axis, ranging from zero to one.
Calibration of the fitted quantile curves on held-out data. Each point compares a nominal level with the empirical fraction of test responses falling at or below the corresponding fitted quantile. Points near the diagonal indicate approximate marginal calibration in this sample. Departure from the diagonal does not identify its cause; assess sampling uncertainty and check conditional fit and crossings separately. This is a marginal check and does not guarantee calibration conditional on every predictor value.

The plotted fractions are close to the diagonal in this held-out sample. The measured central 90 percent coverage is 0.870 and central 50 percent coverage is 0.480. Neither measurement supplies a finite-sample guarantee or establishes calibration conditional on every xx: overcoverage in one predictor region can cancel undercoverage elsewhere. Misspecification, limited tail information, crossing and distribution changes can all affect performance. Standard split-conformal calibration can provide marginal finite-sample coverage under exchangeability when its calibration data are reserved appropriately; it does not automatically give conditional coverage at every predictor value.

A quantile band describes the variability of individual responses. A confidence band for a quantile curve concerns uncertainty about that fitted function. Under regularity conditions estimation uncertainty can decrease with sample size while response variability remains. A restricted linear quantile fit may also be misspecified. A marginal calibration plot can expose some errors, but it need not reveal every conditional error or produce a particular S-shape.

Practical Implications

This section connects the fitted quantile model to practical decisions and deployment, including diagnostics.

Where Quantile Regression Helps

Quantile regression is appropriate when the decision or scientific question concerns a conditional quantile rather than a conditional mean. Service-level agreements, inventory buffers, and staffing decisions can involve quantile targets under suitable loss or probability constraints. Value-at-risk, pediatric growth charts, clinical reference limits, and wage distributions also provide quantile questions. For a single-period inventory decision with linear underage cost cu>0c_u>0 and overage cost co>0c_o>0, minimizing expected cost targets level τ=cu/(cu+co)\tau=c_u/(c_u+c_o). A high level is appropriate when underage is much more expensive. Other decisions need their own loss and assumptions. The method is especially informative when conditional spread changes with the predictors, because separate fitted levels can describe a fan rather than impose a constant-width band. Its response score is bounded at fixed predictors, whereas the least-squares score grows with the residual. This does not prevent a finite-sample tail fit from following one extreme response, and high-leverage predictors remain a concern.

The fit is descriptive unless additional assumptions support causal or deployment claims. A quantile regression on wages can compare associations at different conditional levels, but interpreting a coefficient as a policy effect at a selected quantile requires identification assumptions beyond a correctly specified linear quantile. Likewise, a fitted 95th percentile is not by itself a 95 percent service guarantee; coverage is an empirical property of predictions on new data. When a full predictive distribution is required, a distributional or probabilistic model may be more direct. When only the conditional mean is needed, least squares directly targets it. With a correctly specified regular linear model and independent homoscedastic Gaussian errors, it is asymptotically more efficient than median regression.

Best Practices for Fitting and Checking

Choose the quantile levels before inspecting results and tie them to decision thresholds or reporting requirements. A central 90 percent band uses τ=0.05\tau = 0.05 and 0.950.95; a central 50 percent band uses 0.250.25 and 0.750.75. Fit each level independently, but retain the full coefficient vector for every level rather than only a summary. With scikit-learn’s QuantileRegressor, set fit_intercept=False when the design matrix already contains an intercept column, and use alpha=0 for an unpenalized fit. scikit-learn recommends the HiGHS methods, with solver="highs" as the default; check convergence, feasibility, and runtime on the actual design, since "highs-ds" and "highs-ipm" can behave differently on degenerate or poorly conditioned designs. If alpha>0, standardize predictors first because the penalty acts on raw coefficient magnitudes.

Evaluate the fitted levels on data held out of estimation. Compare each nominal τ\tau with the empirical share of held-out responses at or below the fitted quantile, and count crossings on a dense grid of predictor values rather than only at the observed rows. If crossings appear, rearrangement sorts the fitted values across levels at each predictor value and removes them by construction, but report both raw and rearranged results because the repair changes the fitted values. For coefficient uncertainty and joint statements across levels, use a paired bootstrap over observations and refit every selected level within each replicate so that dependence between levels is preserved. Clustered or otherwise dependent observations require a resampling scheme that respects that dependence. As a diagnostic, compare the median fit with least absolute deviations and, when the errors look symmetric and homoscedastic, with ordinary least squares; a discrepancy can reflect sampling variation, asymmetric errors, misspecification, or numerical problems and should be investigated rather than treated as proof that one estimator is better.

Data Requirements and Preprocessing

The design requirements largely mirror those of linear regression. Include an intercept column unless the application justifies a no-intercept model, encode categorical predictors in the same way as for multiple linear regression, and check multicollinearity because the interpretability of individual coefficients depends on the same identifiability conditions. Missing-data handling should be decided before fitting; complete-case analysis is valid only under restrictive assumptions, and imputation that is meant to reflect sampling uncertainty should be carried out within the resampling or cross-validation loop.

Scaling is mainly a regularization issue. When alpha=0, rescaling a predictor changes its coefficient but not the fitted quantile function, apart from numerical conditioning. When alpha>0, the ℓ1\ell_1 penalty is applied to the coefficients on the scale supplied to the solver, so standardize using training data only and map coefficients back to original units if those units are needed for interpretation. Continuous strictly increasing transformations of the response commute with population quantiles, but refitting a restricted linear model after transforming YY can produce a different finite-sample fit; a transformation should not be chosen solely to force a desired coverage result.

Common Pitfalls

A fitted quantile band is not a confidence interval. The interval between Q^(0.05∣x)\hat Q(0.05 \mid x) and Q^(0.95∣x)\hat Q(0.95 \mid x) describes the spread of individual responses, whereas a confidence interval describes uncertainty about an estimated parameter or curve. The first can be wide because the data are noisy. The second can be narrow because the sample is large. Reporting one while implying the other is a common source of confusion. A related mistake is treating a nominal label as a guarantee: a “90 percent” band is a claim about held-out coverage, and it should be checked on data that played no role in fitting rather than assumed from the label.

Tail levels require particular care. For n=500n=500 independent observations and a continuous response, the expected number below the true τ=0.01\tau=0.01 quantile is nτ=5n\tau=5, but a quantile regression fit is not based on only those five observations; its precision also depends on the conditional density and the design. Choosing a less extreme level, increasing the sample, or reporting uncertainty explicitly is preferable to presenting a tail estimate as precise when the data are sparse. Another frequent error is assuming that coefficients, or fitted values, are automatically ordered across τ\tau. Independent optimization does not enforce that ordering, coefficient ordering alone needs the predictor domain: ordered coefficients are sufficient when every design entry is nonnegative, but negative predictors can reverse their contribution, and ordering every coefficient is not necessary on a bounded domain, and crossings should be diagnosed and either repaired by rearrangement or avoided with a joint constrained estimator. Finally, selecting levels after inspecting test-set performance, or reporting only the most favorable level, undermines the calibration check.

Computational Considerations

Each quantile level is a separate linear program with (p+1)+2n(p+1)+2n unknowns and nn equality constraints, where nn is the number of observations and pp is the number of nonconstant predictors. At fixed predictor count, the block constraint matrix becomes sparse as the sample grows, although our helper allocates it densely. The optimization problem is larger than a least-squares solve and has no single OLS-style normal-equation solve with a fixed linear map from responses to coefficients. SciPy’s HiGHS interface solves each level independently, and because the levels share no fitted state they can be fitted in parallel. Runtime depends on the design and solver settings. For many levels, parallelizing across τ\tau is usually simpler than attempting a single monolithic optimization.

The dense formulation becomes the practical bottleneck as nn grows. Our dense nn-row matrix contains n(p+1+2n)n(p+1+2n) entries, so its memory grows quadratically in nn at fixed pp; the feasible size depends on available memory. For larger data, sparse matrix representations, specialized quantile-regression solvers, or a smaller set of levels are more appropriate. Reducing the number of levels lowers computation but does not fix poor conditioning or degeneracy in a given level. Solver tolerances should be checked against the first-order conditions, especially when duplicate covariate patterns or many zero residuals make the optimal coefficient vector non-unique.

Evaluation, Reporting, and Deployment

Evaluate a quantile fit with the pinball loss at each level and with held-out calibration. The pinball loss is in the units of the response and depends on τ\tau, so it should not be interpreted like R2R^2 or compared across levels as if a single scale measured overall fit. Report empirical coverage for any nominal band, preferably with an assessment of sampling uncertainty, and inspect calibration within relevant subgroups or predictor regions when conditional coverage matters. A marginal calibration plot can reveal systematic error, but it does not guarantee calibration at every predictor value.

In deployment, preserve the training preprocessing pipeline and the definitions of the selected levels, log predictions and outcomes, and monitor coverage and pinball loss as new data arrive. Distribution shift can invalidate a previously calibrated band, so refitting or recalibration should be triggered by evidence of drift rather than by a fixed schedule alone. If marginal finite-sample coverage is required, a conformal procedure with a separate calibration set can provide that guarantee under exchangeability, but it addresses a different target from correctness of each conditional quantile. If the goal is a full predictive distribution, distributional regression is more direct; if the goal is only a conditional mean, least squares targets that quantity directly, with the Gaussian-efficiency comparison described earlier. Quantile regression is most useful when the tail, the spread, or robustness to extreme responses is the point.

Summary

Quantile regression replaces squared error with the pinball loss ρτ(u)=u(τ−1{u<0})\rho_\tau(u) = u(\tau - \mathbf{1}\{u < 0\}), whose population minimizer is the τ\tau-quantile rather than the mean. Restricting the conditional quantile to be linear in the predictors, QY∣X=x(τ)=x⊤β(τ)Q_{Y \mid X=x}(\tau) = x^\top \beta(\tau), turns estimation into minimizing an average of piecewise linear terms. That objective is convex but non-differentiable, so we reformulate it as a linear program with 2n+p+12n + p + 1 unknowns, splitting each residual into non-negative positive and negative parts. The dual of that program exposes its bounded response score at fixed predictors: every observation's multiplier is capped between τ−1\tau - 1 and τ\tau, and the first-order condition X⊤a=0X^\top a = 0 reduces, at the intercept, to the counting rule that the negative-residual count must bracket nτn\tau after including zero residuals.

The payoff is a description of several parts of the conditional distribution rather than one mean. Fitting several levels produces a fan whose width is a local measure of spread, which lets us estimate variation in conditional spread directly. A conditional-mean model can also be combined with a separate variance model. The costs include the median-regression efficiency penalty under the regular Gaussian conditions described earlier, potentially low precision when conditional density near a cutoff is sparse, depending also on τ\tau and the predictor design, no general OLS-style fixed linear map from responses to coefficients, and quantile curves that can cross because each level is estimated independently. Crossing is diagnosed by counting order violations on a grid and repaired by rearrangement, which sorts fitted values across levels at each covariate point and removes crossings by construction.

The most consequential caution is that a nominal band is not a guarantee. Reporting "90 percent" is a claim about held-out coverage, and the only honest way to support it is to measure coverage on data that were not used to fit the model, exactly as we did with the per-level calibration plot. A misspecified form, sparse information near an extreme quantile, or a new target population can invalidate a nominal label. Quantile regression alone supplies no general finite-sample coverage guarantee; methods such as split conformal prediction require separate calibration data and assumptions such as exchangeability. Part XII currently lists planned chapters on calibration and prediction uncertainty. Within those limits, quantile regression complements the conditional-mean models in Part V by targeting selected quantiles without specifying a full response distribution, in the same language we will need when the response becomes a time to an event in the chapters that follow.

Quiz

Ready to test your understanding? Take this quick quiz to reinforce what you've learned about quantile regression.

Quantile Regression Quiz

Question 1 of 70 of 7 completed
Which loss family, parameterized by tau, targets the conditional tau-quantile for any chosen tau in (0, 1) across general conditional response distributions with finite first moments?
Join the discussionAsk a question or share what you learned.

Comments

Add to the conversation

A question, a correction, or an example from your own work.

No comments yet. Be the first to share your thoughts!

A different angle
All writing
Machine Learning

Generalized Additive Models

Learn generalized additive models from scratch: spline bases, difference penalties, link functions, identifiability, cross-validated smoothing, and NumPy fits.

45 min read
Machine Learning · Vol. 2

Logistic Regression: Math & Python

Covers logistic regression covering mathematical foundations, the logistic function, optimization algorithms, and practical implementation.

46 min read
Machine Learning · Vol. 2

Poisson Regression: Count Data Modeling with Math & Python

Covers Poisson regression for count data analysis. Topics include mathematical foundations, maximum likelihood estimation, rate ratio interpretation.

49 min read

About the author

Reference

Citation details

Cite or share this article.

BIBTEXAcademic
@misc{brenndoerfer2026quantileregression, author = {Michael Brenndoerfer}, title = {Quantile Regression}, year = {2026}, url = {https://mbrenndoerfer.com/writing/quantile-regression-from-scratch}, organization = {mbrenndoerfer.com}, note = {Accessed: 2026-10-28} }
APAAcademic
Michael Brenndoerfer (2026). Quantile Regression. Retrieved from https://mbrenndoerfer.com/writing/quantile-regression-from-scratch
MLAAcademic
Michael Brenndoerfer. "Quantile Regression." 2026. Web. October 28, 2026. <https://mbrenndoerfer.com/writing/quantile-regression-from-scratch>.
CHICAGOAcademic
Michael Brenndoerfer. "Quantile Regression." Accessed October 28, 2026. https://mbrenndoerfer.com/writing/quantile-regression-from-scratch.
HARVARDAcademic
Michael Brenndoerfer (2026) 'Quantile Regression'. Available at: https://mbrenndoerfer.com/writing/quantile-regression-from-scratch (Accessed: October 28, 2026).
SimpleBasic
Michael Brenndoerfer (2026). Quantile Regression. https://mbrenndoerfer.com/writing/quantile-regression-from-scratch
Newsletter

Stay up to date

Get new articles, book updates, and notes on AI and technology.

Unsubscribe at any time. Privacy policy