Quantile Regression › Example: Small Numbers and Heteroscedastic Predictions
Machine Learning from ScratchAll chapters 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
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 and ask for the median, . The counting rule says we need a value with at most observations strictly below it and at least at or below it. The value has two observations below it and three at or below it , so it satisfies the rule. Let us confirm by evaluating the average pinball loss at three candidates.
- At the residuals are . With , , so the losses are and their average is .
- At the residuals are , losses , average .
- At the residuals are , losses , average .
These values compare the same average loss at three candidate medians.
Here the pinball loss at is , and the residual is the signed distance from each observation to the candidate value .
where:
- : the residual , the signed distance from each observation to the candidate value
- : the pinball loss at , which equals
- : the candidate value being evaluated as an estimate of the sample median
The minimum is at , which is the sample median. Moving 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, , fitted to four observations, where is the single slope to be estimated and is the predictor:
| ratio | weight | ||
|---|---|---|---|
| 1 | 2 | 2.00 | 1 |
| 2 | 3 | 1.50 | 2 |
| 3 | 9 | 3.00 | 3 |
| 4 | 13 | 3.25 | 4 |
Because all and the residual in observation is , the pinball loss is positively homogeneous of degree one: for , . Writing with , the objective becomes
where:
- : the ratio of response to predictor for the -th observation
- : the -th predictor value, which also serves as the weight
- : the single slope parameter being estimated
- : the pinball loss at level , which is positively homogeneous of degree one
This is a weighted pinball loss in the single unknown , and its minimizer is the weighted -quantile of the ratios with weights . Order the ratios and accumulate weight: with weight 2, then with weight 1 (cumulative 3), then with weight 3 (cumulative 6), then with weight 4 (cumulative 10). For the target cumulative weight is . The cumulative weight jumps from (after ) to (after ), so it crosses the target of at the ratio ; by the weighted-median convention we take the smallest ratio whose cumulative weight is at least the target, giving .
Checking the loss: at the fitted values are and the residuals are , giving an average loss of . At the residuals are , average loss . At the residuals are , average loss . Both neighbours are worse, confirming the minimum. Note also that this weighted-median minimizer applies because all slopes share a single parameter multiplying positive weights ; the objective is a weighted pinball loss in whose minimizer is the weighted -quantile of the ratios.
PythonIn [5]Expand
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]Expand
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: , , .
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]Expand
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]Expand

This shortcut is worth remembering. For a single slope, no intercept, and positive predictors, the objective reduces to a weighted -quantile of the ratios with weights . At 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 their ordering gives a widening nominal 10th-to-90th-percentile band:
| intercept | slope | |
|---|---|---|
| 0.1 | 1.00 | 0.60 |
| 0.5 | 2.00 | 1.10 |
| 0.9 | 3.60 | 2.00 |
The fitted curves are , , and , where:
- : the fitted conditional -quantile at predictor value
- : the intercept of the fitted line at level
- : the slope of the fitted line at level
Evaluate them at three values of and record the width of the central 80 percent band, which is defined as , where is the fitted conditional -quantile and is the resulting band width in response units:
- At : the three values are , , , so .
- At : the values are , , , so .
- At : the values are , , , so .
The width is not constant: because both bounding curves are linear, the width is exactly linear in . Subtracting the two fitted lines and term by term,
which reproduces , , and at (for example, ). The slope difference is the rate at which the conditional spread grows: each additional unit of 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 and . The prediction is , so the residual is . Because we use the lower branch: . Overpredicting this response at a high quantile is cheap because the lower branch of has slope (in magnitude ), so a negative residual of size is penalized by only ; 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 at the same and score it against the line, whose prediction is . The residual is , so . The two residuals, and , are not of comparable size, so this comparison mainly illustrates the pricing rule: the pinball loss charges per unit on the positive side and per unit on the negative side, with both rates evaluated at the relevant .
PythonIn [9]Expand
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| 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 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!