Quantile Regression › Example: Small Numbers and Heteroscedastic Predictions

Michael BrenndoerferOctober 26, 202610 min read
Machine Learning from ScratchAll chapters
Part 5 of 15Linear and Generalized Linear Models
Chapter 47Quantile Regression › Example: Small Numbers and Heteroscedastic Predictions
Reading section 5 of 10 · Example: Small Numbers and Heteroscedastic Predictions · 10 minSection 5 of 10 · 10 min

Section 5 of 10 · 10 min read

Quantile Regression › Example: Small Numbers and Heteroscedastic PredictionsSection view · Sections in this chapter5 / 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

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.

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!

Newsletter

Stay up to date

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

Unsubscribe at any time. Privacy policy