Skip to content

Commit 77646d2

Browse files
fix linter again & adress pytest warning for future updates
1 parent 9fdf203 commit 77646d2

File tree

3 files changed

+84
-74
lines changed

3 files changed

+84
-74
lines changed

skglm/experimental/quantile_huber.py

Lines changed: 58 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -6,63 +6,64 @@
66

77

88
class QuantileHuber(BaseDatafit):
9-
"""Huber-smoothed Pinball loss for quantile regression.
10-
11-
This implements a smoothed approximation of the Pinball (quantile) loss
12-
by applying Huber-style smoothing at the non-differentiable point. T
13-
his formulation improves numerical stability and convergence
14-
for gradient-based solvers, particularly on larger datasets.
15-
16-
Parameters
17-
----------
18-
delta : float, positive
19-
Width of the quadratic region around the origin. Larger values create
20-
more smoothing. As delta approaches 0, this approaches the standard
21-
Pinball loss.
22-
23-
quantile : float, between 0 and 1
24-
The desired quantile level. For example, 0.5 corresponds to the median.
25-
26-
Notes
27-
-----
28-
The loss function is defined as:
29-
30-
.. math::
31-
L(r) = \begin{cases}
32-
\tau \frac{r^2}{2\delta} & \text{if } 0 < r \leq \delta \\
33-
(1-\tau) \frac{r^2}{2\delta} & \text{if } -\delta \leq r < 0 \\
34-
\tau (r - \frac{\delta}{2}) & \text{if } r > \delta \\
35-
(1-\tau) (-r - \frac{\delta}{2}) & \text{if } r < -\delta
36-
\end{cases}
37-
38-
where :math:`r = y - Xw` is the residual, :math:`\tau` is the target
39-
quantile, and :math:`\delta` controls the smoothing region width.
40-
41-
The gradient is given by:
42-
43-
.. math::
44-
\nabla L(r) = \begin{cases}
45-
\tau \frac{r}{\delta} & \text{if } 0 < r \leq \delta \\
46-
(1-\tau) \frac{r}{\delta} & \text{if } -\delta \leq r < 0 \\
47-
\tau & \text{if } r > \delta \\
48-
-(1-\tau) & \text{if } r < -\delta
49-
\end{cases}
50-
51-
This formulation provides twice-differentiable smoothing while maintaining
52-
quantile estimation properties. The approach is similar to convolution
53-
smoothing with a uniform kernel.
54-
55-
Special cases:
56-
- When :math:`\\tau = 0.5`, this reduces to the symmetric Huber
57-
loss used for median regression.
58-
- As :math:`\\delta \\to 0`, it converges to the standard
59-
Pinball loss.
60-
61-
References
62-
----------
63-
He, X., Pan, X., Tan, K. M., & Zhou, W. X. (2021).
64-
"Smoothed Quantile Regression with Large-Scale Inference
65-
"""
9+
r"""Huber-smoothed Pinball loss for quantile regression.
10+
11+
This implements a smoothed approximation of the Pinball (quantile) loss
12+
by applying Huber-style smoothing at the non-differentiable point. T
13+
his formulation improves numerical stability and convergence
14+
for gradient-based solvers, particularly on larger datasets.
15+
16+
Parameters
17+
----------
18+
delta : float, positive
19+
Width of the quadratic region around the origin. Larger values
20+
create more smoothing. As delta approaches 0, this approaches the
21+
standard Pinball loss.
22+
23+
quantile : float, between 0 and 1
24+
The desired quantile level. For example, 0.5 corresponds to the
25+
median.
26+
27+
Notes
28+
-----
29+
The loss function is defined as:
30+
31+
.. math::
32+
L(r) = \begin{cases}
33+
\tau \frac{r^2}{2\delta} & \text{if } 0 < r \leq \delta \\
34+
(1-\tau) \frac{r^2}{2\delta} & \text{if } -\delta \leq r < 0 \\
35+
\tau (r - \frac{\delta}{2}) & \text{if } r > \delta \\
36+
(1-\tau) (-r - \frac{\delta}{2}) & \text{if } r < -\delta
37+
\end{cases}
38+
39+
where :math:`r = y - Xw` is the residual, :math:`\tau` is the target
40+
quantile, and :math:`\delta` controls the smoothing region width.
41+
42+
The gradient is given by:
43+
44+
.. math::
45+
\nabla L(r) = \begin{cases}
46+
\tau \frac{r}{\delta} & \text{if } 0 < r \leq \delta \\
47+
(1-\tau) \frac{r}{\delta} & \text{if } -\delta \leq r < 0 \\
48+
\tau & \text{if } r > \delta \\
49+
-(1-\tau) & \text{if } r < -\delta
50+
\end{cases}
51+
52+
This formulation provides twice-differentiable smoothing while
53+
maintaining quantile estimation properties. The approach is similar to
54+
convolution smoothing with a uniform kernel.
55+
56+
Special cases:
57+
- When :math:`\\tau = 0.5`, this reduces to the symmetric Huber
58+
loss used for median regression.
59+
- As :math:`\\delta \\to 0`, it converges to the standard
60+
Pinball loss.
61+
62+
References
63+
----------
64+
He, X., Pan, X., Tan, K. M., & Zhou, W. X. (2021).
65+
"Smoothed Quantile Regression with Large-Scale Inference
66+
"""
6667

6768
def __init__(self, delta, quantile):
6869
if not 0 < quantile < 1:

skglm/experimental/smooth_quantile_regressor.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010
class SmoothQuantileRegressor:
1111
"""Progressive smoothing (homotopy) meta-solver.
1212
13-
This solver addresses convergence issues in non-smooth datafits like Pinball
14-
(quantile regression) on large datasets (as discussed in GitHub issue #276).
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).
1516
1617
It works by progressively solving a sequence of smoothed problems with
1718
decreasing smoothing parameter.
@@ -59,7 +60,8 @@ class SmoothQuantileRegressor:
5960
6061
Examples
6162
--------
62-
>>> from skglm.experimental.progressive_smoothing import ProgressiveSmoothingSolver
63+
>>> from skglm.experimental.progressive_smoothing import
64+
ProgressiveSmoothingSolver
6365
>>> import numpy as np
6466
>>> X = np.random.randn(1000, 10)
6567
>>> y = np.random.randn(1000)
@@ -81,7 +83,8 @@ def __init__(
8183
# if user stops above min_delta, append finer deltas
8284
min_delta = 1e-3
8385
if base_seq[-1] > min_delta:
84-
extra = np.geomspace(base_seq[-1], min_delta, num=5, endpoint=False)[1:]
86+
extra = np.geomspace(base_seq[-1], min_delta, num=5,
87+
endpoint=False)[1:]
8588
base_seq = base_seq + list(extra)
8689
self.smoothing_sequence = base_seq
8790
self.quantile = float(quantile)
@@ -224,7 +227,8 @@ def fit(self, X, y):
224227
self.intercept_ = best_intercept
225228

226229
if self.verbose:
227-
print(f"[Final] Using smoothed solution with delta={best_delta:.3g}")
230+
print(f"[Final] Using smoothed solution with delta"
231+
f"={best_delta:.3g}")
228232
print(f" Best quantile error: {best_quantile_error:.3f}")
229233

230234
self.stage_results_ = stage_results

skglm/experimental/tests/test_smooth_quantile_regressor.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
from sklearn.linear_model import QuantileRegressor
55

66
from skglm import GeneralizedLinearEstimator
7-
from skglm.experimental.smooth_quantile_regressor import SmoothQuantileRegressor
7+
from skglm.experimental.smooth_quantile_regressor import (
8+
SmoothQuantileRegressor,
9+
)
810
from skglm.experimental.pdcd_ws import PDCD_WS
911
from skglm.experimental.quantile_regression import Pinball
1012
from skglm.penalties import L1
@@ -38,10 +40,13 @@ def test_issue276_regression():
3840
n_samples_small, n_samples_large = 100, 1000
3941
n_features = 10
4042

41-
# Create two datasets - small should work with PDCD_WS, large exhibits the issue
42-
X_small, y_small = make_regression(n_samples=n_samples_small, n_features=n_features,
43+
# Create two datasets, small should work with PDCD_WS,
44+
# large exhibits the issue
45+
X_small, y_small = make_regression(n_samples=n_samples_small,
46+
n_features=n_features,
4347
noise=0.1, random_state=42)
44-
X_large, y_large = make_regression(n_samples=n_samples_large, n_features=n_features,
48+
X_large, y_large = make_regression(n_samples=n_samples_large,
49+
n_features=n_features,
4550
noise=0.1, random_state=42)
4651

4752
X_small = StandardScaler().fit_transform(X_small)
@@ -58,7 +63,8 @@ def test_issue276_regression():
5863
solver="highs").fit(X_large, y_large)
5964

6065
# Verify PDCD_WS works fine on small dataset
61-
pdcd_solver = PDCD_WS(max_iter=500, max_epochs=500, tol=1e-4, verbose=False)
66+
pdcd_solver = PDCD_WS(max_iter=500, max_epochs=500, tol=1e-4,
67+
verbose=False)
6268
pdcd_solver.fit_intercept = True
6369

6470
estimator_small = GeneralizedLinearEstimator(
@@ -71,7 +77,8 @@ def test_issue276_regression():
7177
pdcd_small_loss = pinball_loss(y_small, y_pred_pdcd_small, tau=tau)
7278

7379
# Apply PDCD_WS to large dataset (should exhibit issue #276)
74-
pdcd_solver = PDCD_WS(max_iter=500, max_epochs=200, tol=1e-4, verbose=False)
80+
pdcd_solver = PDCD_WS(max_iter=500, max_epochs=200, tol=1e-4,
81+
verbose=False)
7582
pdcd_solver.fit_intercept = True
7683
estimator_large = GeneralizedLinearEstimator(
7784
datafit=Pinball(tau),
@@ -122,8 +129,6 @@ def test_issue276_regression():
122129
assert len(sqr.stage_results_) > 0, "Missing stage results" \
123130
"in SmoothQuantileRegressor"
124131

125-
return rel_gap_pdcd_small, rel_gap_pdcd_large, rel_gap_sqr_large
126-
127132

128133
def test_smooth_quantile_regressor_non_median():
129134
"""
@@ -135,7 +140,8 @@ def test_smooth_quantile_regressor_non_median():
135140
"""
136141
np.random.seed(42)
137142

138-
X, y = make_regression(n_samples=1000, n_features=10, noise=0.1, random_state=42)
143+
X, y = make_regression(n_samples=1000, n_features=10, noise=0.1,
144+
random_state=42)
139145
X = StandardScaler().fit_transform(X)
140146

141147
tau = 0.8
@@ -149,7 +155,8 @@ def test_smooth_quantile_regressor_non_median():
149155

150156
# SmoothQuantileRegressor solution
151157
sqr = SmoothQuantileRegressor(
152-
smoothing_sequence=[1.0, 0.5, 0.2, 0.1, 0.05, 0.02, 0.01, 0.005, 0.001],
158+
smoothing_sequence=[1.0, 0.5, 0.2, 0.1, 0.05,
159+
0.02, 0.01, 0.005, 0.001],
153160
quantile=tau,
154161
alpha=alpha,
155162
verbose=False,
@@ -170,5 +177,3 @@ def test_smooth_quantile_regressor_non_median():
170177
n_neg = np.sum(residuals < 0)
171178
assert abs(n_pos / (n_pos + n_neg) - tau) < 0.1, \
172179
f"Residual distribution doesn't match target quantile {tau}"
173-
174-
return rel_gap

0 commit comments

Comments
 (0)