Skip to content

WIP - FEAT - Quantile Huber & Progressive Smoothing #312

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

floriankozikowski
Copy link
Contributor

Context of the PR

This PR implements a smooth quantile regression estimator using a Huberized loss with progressive smoothing. The goal is to provide a faster alternative to scikit-learn's QuantileRegressor while maintaining similar accuracy.
(closes #276 )
(Also it aims to simplify earlier approaches done in PR #306 )

Contributions of the PR

Added QuantileHuber loss in skglm/experimental/quantile_huber.py

Added SmoothQuantileRegressor class in skglm/experimental/smooth_quantile_regressor.py:

  • Uses FISTA solver with L1 regularization
  • Implements progressive smoothing from delta_init to delta_final
  • Includes intercept updates using gradient steps

Added example in examples/plot_smooth_quantile.py

Checks before merging PR

  • added documentation for any new feature
  • added unit tests
  • edited the what's new (if applicable)

@floriankozikowski floriankozikowski changed the title first try at simple quantile huber WIP - FEAT - Quantile Huber & Progressive Smoothing May 23, 2025
return np.mean(residuals * (quantile - (residuals < 0)))


def create_data(n_samples=1000, n_features=10, noise=0.1):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

avoid this: this is literally just wrapping make_regression

plt.tight_layout()
plt.show()


Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to wrap in if name == main for example plots

res += self._loss_scalar(residual)
return res / n_samples

def _loss_scalar(self, residual):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loss_sample may be a clearer name

grad_j += -X[i, j] * self._grad_scalar(residual)
return grad_j / n_samples

def _grad_scalar(self, residual):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

having gradient_scalar and _grad_scalar is a massive risk of confusion in the future; _grad_per_sample ?

return grad_j / n_samples

def _grad_scalar(self, residual):
"""Calculate gradient for a single residual."""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a single sample


def fit(self, X, y):
"""Fit using progressive smoothing: delta_init --> delta_final."""
X, y = check_X_y(X, y)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to check: GeneralizedLinearEstimator will do it


for i, delta in enumerate(deltas):
datafit = QuantileHuber(quantile=self.quantile, delta=delta)
penalty = L1(alpha=self.alpha)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

those can be taken out of the for loop

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(initialize datafit, penalty, solver and est outside of the loop; then in the loop only update the delta parameter of GLE.datafit)

solver=solver
)

if i > 0:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this way you won't need this (if est is fixed outside the loop and uses warm_start=True)


return self

def predict(self, X):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can store est as self.est and use self.est.predict to leverage existing code

@mathurinm
Copy link
Collaborator

Ok as discussed separately, you need to implement the maths computation to make the solver work with Fista solver and AndersonCD solver; then it should be easy to support the intercept as these solvers rely on update_intercept_step (which is just a coordinate descent step on the intercept, which has a lipschitz constant equal to that of a feature which would be filled with 1s)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

PDCD_WS solver seems unstable for Pinball Loss.
2 participants