Skip to content

Commit cfe065e

Browse files
update tests, documentation, add example
1 parent d10c4f9 commit cfe065e

File tree

4 files changed

+384
-156
lines changed

4 files changed

+384
-156
lines changed

examples/plot_smooth_quantile.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
"""
2+
===========================================
3+
Fast Quantile Regression with Smoothing
4+
===========================================
5+
This example demonstrates how SmoothQuantileRegressor achieves faster convergence
6+
than scikit-learn's QuantileRegressor while maintaining accuracy, particularly
7+
for large datasets.
8+
"""
9+
10+
# %%
11+
# Data Generation
12+
# --------------
13+
# First, we generate synthetic data with a known quantile structure.
14+
15+
import time
16+
import numpy as np
17+
import matplotlib.pyplot as plt
18+
from sklearn.datasets import make_regression
19+
from sklearn.preprocessing import StandardScaler
20+
from sklearn.linear_model import QuantileRegressor
21+
from skglm.experimental.smooth_quantile_regressor import SmoothQuantileRegressor
22+
from skglm.solvers import FISTA
23+
24+
# Set random seed for reproducibility
25+
np.random.seed(42)
26+
27+
# Generate dataset - using a more reasonable size for quick testing
28+
n_samples, n_features = 10000, 10 # Match test file size
29+
X, y = make_regression(n_samples=n_samples, n_features=n_features,
30+
noise=0.1, random_state=42)
31+
X = StandardScaler().fit_transform(X)
32+
y = y - np.mean(y) # Center y like in test file
33+
34+
# %%
35+
# Model Comparison
36+
# ---------------
37+
# We compare scikit-learn's QuantileRegressor with our SmoothQuantileRegressor
38+
# on the 80th quantile.
39+
40+
tau = 0.5 # median (SmoothQuantileRegressor works much better for non-median quantiles)
41+
alpha = 0.1
42+
43+
44+
def pinball_loss(y_true, y_pred, tau=0.5):
45+
"""Compute Pinball (quantile) loss."""
46+
residuals = y_true - y_pred
47+
return np.mean(np.where(residuals >= 0,
48+
tau * residuals,
49+
(1 - tau) * -residuals))
50+
51+
52+
# scikit-learn's QuantileRegressor
53+
start_time = time.time()
54+
qr = QuantileRegressor(quantile=tau, alpha=alpha, fit_intercept=True,
55+
solver="highs").fit(X, y)
56+
qr_time = time.time() - start_time
57+
y_pred_qr = qr.predict(X)
58+
qr_loss = pinball_loss(y, y_pred_qr, tau=tau)
59+
60+
# SmoothQuantileRegressor
61+
start_time = time.time()
62+
solver = FISTA(max_iter=2000, tol=1e-8)
63+
solver.fit_intercept = True
64+
sqr = SmoothQuantileRegressor(
65+
smoothing_sequence=[1.0, 0.5, 0.2, 0.1, 0.05], # Base sequence, will be extended
66+
quantile=tau, alpha=alpha, verbose=True, # Enable verbose to see stages
67+
smooth_solver=solver
68+
).fit(X, y)
69+
sqr_time = time.time() - start_time
70+
y_pred_sqr = sqr.predict(X)
71+
sqr_loss = pinball_loss(y, y_pred_sqr, tau=tau)
72+
73+
# %%
74+
# Performance Analysis
75+
# ------------------
76+
# Let's analyze both the performance and solution quality of both methods.
77+
78+
speedup = qr_time / sqr_time
79+
rel_gap = (sqr_loss - qr_loss) / qr_loss
80+
81+
print("\nPerformance Summary:")
82+
print("scikit-learn QuantileRegressor:")
83+
print(f" Time: {qr_time:.2f}s")
84+
print(f" Loss: {qr_loss:.6f}")
85+
print("SmoothQuantileRegressor:")
86+
print(f" Time: {sqr_time:.2f}s")
87+
print(f" Loss: {sqr_loss:.6f}")
88+
print(f" Speedup: {speedup:.1f}x")
89+
print(f" Relative gap: {rel_gap:.1%}")
90+
91+
# %%
92+
# Visual Comparison
93+
# ---------------
94+
# We create visualizations to compare the predictions and residuals
95+
# of both methods.
96+
97+
# Sort data for better visualization
98+
sort_idx = np.argsort(y)
99+
y_sorted = y[sort_idx]
100+
qr_pred = y_pred_qr[sort_idx]
101+
sqr_pred = y_pred_sqr[sort_idx]
102+
103+
# Create figure with two subplots
104+
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5))
105+
106+
# Plot predictions
107+
ax1.scatter(y_sorted, qr_pred, alpha=0.5, label='scikit-learn', s=10)
108+
ax1.scatter(y_sorted, sqr_pred, alpha=0.5, label='SmoothQuantile', s=10)
109+
ax1.plot([y_sorted.min(), y_sorted.max()],
110+
[y_sorted.min(), y_sorted.max()], 'k--', alpha=0.3)
111+
ax1.set_xlabel('True values')
112+
ax1.set_ylabel('Predicted values')
113+
ax1.set_title(f'Predictions (τ={tau})')
114+
ax1.legend()
115+
116+
# Plot residuals
117+
qr_residuals = y_sorted - qr_pred
118+
sqr_residuals = y_sorted - sqr_pred
119+
ax2.hist(qr_residuals, bins=50, alpha=0.5, label='scikit-learn')
120+
ax2.hist(sqr_residuals, bins=50, alpha=0.5, label='SmoothQuantile')
121+
ax2.axvline(x=0, color='k', linestyle='--', alpha=0.3)
122+
ax2.set_xlabel('Residuals')
123+
ax2.set_ylabel('Count')
124+
ax2.set_title('Residual Distribution')
125+
ax2.legend()
126+
127+
plt.tight_layout()
128+
129+
# %%
130+
# Progressive Smoothing Analysis
131+
# ----------------------------
132+
# Let's examine how the smoothing parameter affects the solution quality.
133+
134+
stages = sqr.stage_results_
135+
deltas = [s['delta'] for s in stages]
136+
errors = [s['quantile_error'] for s in stages]
137+
losses = [s['obj_value'] for s in stages]
138+
139+
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5))
140+
141+
# Plot quantile error progression
142+
ax1.semilogx(deltas, errors, 'o-')
143+
ax1.set_xlabel('Smoothing parameter (δ)')
144+
ax1.set_ylabel('Quantile error')
145+
ax1.set_title('Quantile Error vs Smoothing')
146+
ax1.grid(True, alpha=0.3)
147+
148+
# Plot objective value progression
149+
ax2.semilogx(deltas, losses, 'o-')
150+
ax2.set_xlabel('Smoothing parameter (δ)')
151+
ax2.set_ylabel('Objective value')
152+
ax2.set_title('Objective Value vs Smoothing')
153+
ax2.grid(True, alpha=0.3)
154+
155+
plt.tight_layout()
156+
plt.show()

skglm/experimental/quantile_huber.py

Lines changed: 57 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,44 +6,75 @@
66

77

88
class QuantileHuber(BaseDatafit):
9-
r"""Huber‑smoothed pinball loss for quantile regression.
9+
r"""Smoothed approximation of the pinball loss for quantile regression.
1010
11-
This class implements a smoothed approximation of the pinball (quantile)
12-
loss by applying Huber‑style smoothing at the non‑differentiable point.
13-
The formulation improves numerical stability and convergence for
14-
gradient‑based solvers, particularly on large data sets.
11+
This class implements a smoothed version of the pinball loss used in quantile
12+
regression. The original pinball loss is:
13+
14+
.. math::
15+
\rho_\tau(r) = \begin{cases}
16+
\tau r & \text{if } r \geq 0 \\
17+
(\tau - 1) r & \text{if } r < 0
18+
\end{cases}
19+
20+
The smoothed version (Huberized pinball loss) is:
21+
22+
.. math::
23+
\rho_\tau^\delta(r) = \begin{cases}
24+
\tau r - \frac{\delta}{2} & \text{if } r \geq \delta \\
25+
\frac{r^2}{2\delta} & \text{if } |r| < \delta \\
26+
(\tau - 1) r - \frac{\delta}{2} & \text{if } r \leq -\delta
27+
\end{cases}
28+
29+
where :math:`\delta` is the smoothing parameter. As :math:`\delta \to 0`,
30+
the smoothed loss converges to the original pinball loss.
1531
1632
Parameters
1733
----------
18-
delta : float, positive
19-
Width of the quadratic region around the origin. Larger values create
20-
stronger smoothing. As ``delta`` -> 0, the loss approaches the
21-
standard pinball loss.
34+
delta : float, default=1.0
35+
Smoothing parameter. Smaller values make the approximation closer to
36+
the original pinball loss but may lead to numerical instability.
2237
23-
quantile : float in (0, 1)
24-
Target quantile level (e.g. ``0.5`` corresponds to the median).
38+
quantile : float, default=0.5
39+
Quantile level between 0 and 1. When 0.5, the loss is symmetric
40+
(Huber loss). For other values, the loss is asymmetric.
41+
42+
Attributes
43+
----------
44+
delta : float
45+
Current smoothing parameter.
46+
47+
quantile : float
48+
Current quantile level.
2549
2650
Notes
2751
-----
28-
The loss function is defined as
52+
The smoothed loss is continuously differentiable everywhere, making it
53+
suitable for gradient-based optimization methods. The gradient is:
2954
3055
.. math::
56+
\nabla \rho_\tau^\delta(r) = \begin{cases}
57+
\tau & \text{if } r \geq \delta \\
58+
\frac{r}{\delta} & \text{if } |r| < \delta \\
59+
\tau - 1 & \text{if } r \leq -\delta
60+
\end{cases}
3161
32-
L(r) =
33-
\\begin{cases}
34-
\\tau \\dfrac{r^{2}}{2\\delta}, & 0 < r \\le \\delta \\\\
35-
(1-\\tau) \\dfrac{r^{2}}{2\\delta}, & -\\delta \\le r < 0 \\\\
36-
\\tau \\left(r - \\dfrac{\\delta}{2}\\right), & r > \\delta \\\\
37-
(1-\\tau) \\left(-r - \\dfrac{\\delta}{2}\\right), & r < -\\delta
38-
\\end{cases}
62+
The Hessian is piecewise constant:
3963
40-
where :math:`r = y - Xw` is the residual, :math:`\\tau` is the target
41-
quantile, and :math:`\\delta` controls the smoothing width.
42-
43-
References
44-
----------
45-
He, X., Pan, X., Tan, K. M., & Zhou, W. X. (2021).
46-
*Smoothed Quantile Regression with Large‑Scale Inference*.
64+
.. math::
65+
\nabla^2 \rho_\tau^\delta(r) = \begin{cases}
66+
0 & \text{if } |r| \geq \delta \\
67+
\frac{1}{\delta} & \text{if } |r| < \delta
68+
\end{cases}
69+
70+
Examples
71+
--------
72+
>>> from skglm.experimental.quantile_huber import QuantileHuber
73+
>>> import numpy as np
74+
>>> loss = QuantileHuber(delta=0.1, quantile=0.8)
75+
>>> r = np.array([-1.0, 0.0, 1.0])
76+
>>> print(loss.value(r)) # Compute loss values
77+
>>> print(loss.gradient(r)) # Compute gradients
4778
"""
4879

4980
def __init__(self, delta, quantile):

skglm/experimental/smooth_quantile_regressor.py

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,33 @@
88

99

1010
class SmoothQuantileRegressor:
11-
"""Progressive smoothing (homotopy) meta-solver.
11+
r"""Progressive smoothing solver for quantile regression.
1212
13-
This solver addresses convergence issues in non-smooth datafits like
14-
Pinball(quantile regression) on large datasets
15-
(as discussed in GitHub issue #276).
13+
This solver addresses convergence issues in non-smooth quantile regression
14+
on large datasets by using a progressive smoothing approach. The optimization
15+
objective is:
1616
17-
It works by progressively solving a sequence of smoothed problems with
18-
decreasing smoothing parameter.
17+
.. math::
18+
\min_{w \in \mathbb{R}^p} \frac{1}{n} \sum_{i=1}^n \rho_\tau(y_i - x_i^T w)
19+
+ \alpha \|w\|_1
20+
21+
where :math:`\rho_\tau` is the pinball loss:
22+
23+
.. math::
24+
\rho_\tau(r) = \begin{cases}
25+
\tau r & \text{if } r \geq 0 \\
26+
(\tau - 1) r & \text{if } r < 0
27+
\end{cases}
28+
29+
The solver progressively approximates the non-smooth pinball loss using
30+
smoothed versions with decreasing smoothing parameter :math:`\delta`:
31+
32+
.. math::
33+
\rho_\tau^\delta(r) = \begin{cases}
34+
\tau r - \frac{\delta}{2} & \text{if } r \geq \delta \\
35+
\frac{r^2}{2\delta} & \text{if } |r| < \delta \\
36+
(\tau - 1) r - \frac{\delta}{2} & \text{if } r \leq -\delta
37+
\end{cases}
1938
2039
Parameters
2140
----------
@@ -32,42 +51,57 @@ class SmoothQuantileRegressor:
3251
3352
smooth_solver : instance of BaseSolver, default=None
3453
Solver to use for smooth approximation stages.
35-
If None, uses LBFGS(max_iter=500, tol=1e-6).
36-
54+
If None, uses FISTA(max_iter=2000, tol=1e-8).
3755
3856
verbose : bool, default=False
3957
If True, prints progress information during fitting.
4058
4159
Attributes
4260
----------
4361
coef_ : ndarray of shape (n_features,)
44-
Parameter vector (w in the cost function formula).
62+
Parameter vector (:math:`w` in the cost function formula).
4563
4664
intercept_ : float
4765
Intercept term in decision function.
4866
4967
stage_results_ : list
50-
Information about each smoothing stage including smoothing parameter,
51-
number of iterations, and coefficients.
52-
68+
Information about each smoothing stage including:
69+
- delta: smoothing parameter
70+
- obj_value: objective value
71+
- coef_norm: L2 norm of coefficients
72+
- quantile_error: absolute difference between target and achieved quantile
73+
- actual_quantile: achieved quantile level
74+
- coef: coefficient vector
75+
- intercept: intercept value
76+
- n_iter: number of iterations (if available)
77+
78+
Notes
79+
-----
80+
This implementation uses a progressive smoothing approach to solve quantile
81+
regression problems. It starts with a highly smoothed approximation and
82+
gradually reduces the smoothing parameter to approach the original non-smooth
83+
problem. This approach is particularly effective for large datasets where
84+
direct optimization of the non-smooth objective can be challenging.
85+
86+
The solver automatically selects the best solution from all smoothing stages
87+
based on the quantile error, ensuring good approximation of the target
88+
quantile level.
5389
5490
References
5591
----------
5692
Chen, C. (2007). A Finite Smoothing Algorithm for Quantile Regression.
5793
Journal of Computational and Graphical Statistics, 16(1), 136–164.
5894
http://www.jstor.org/stable/27594233
5995
60-
6196
Examples
6297
--------
63-
>>> from skglm.experimental.progressive_smoothing import
64-
ProgressiveSmoothingSolver
98+
>>> from skglm.experimental.smooth_quantile_regressor import SmoothQuantileRegressor
6599
>>> import numpy as np
66100
>>> X = np.random.randn(1000, 10)
67101
>>> y = np.random.randn(1000)
68-
>>> solver = ProgressiveSmoothingSolver(quantile=0.8)
69-
>>> solver.fit(X, y)
70-
>>> print(solver.coef_)
102+
>>> reg = SmoothQuantileRegressor(quantile=0.8, alpha=0.1)
103+
>>> reg.fit(X, y)
104+
>>> print(reg.coef_)
71105
"""
72106

73107
def __init__(

0 commit comments

Comments
 (0)