Skip to content

Commit 38e4b37

Browse files
committed
quantile regression first pass
1 parent f7bf90a commit 38e4b37

7 files changed

Lines changed: 590 additions & 0 deletions

File tree

ya_glm/backends/scipy/__init__.py

Whitespace-only changes.
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# import numpy as np
2+
from ya_glm.lla.WeightedLassoSolver import WeightedLassoSolver
3+
from ya_glm.lla.utils import safe_concat
4+
5+
from ya_glm.backends.fista.glm_solver import get_glm_loss as get_glm_loss_fista
6+
from .glm_solver import solve_glm
7+
8+
9+
class WL1SolverGlm(WeightedLassoSolver):
10+
def __init__(self, X, y, loss_func, loss_kws={},
11+
fit_intercept=True,
12+
opt_kws={}):
13+
14+
self.glm_loss = get_glm_loss_fista(X=X, y=y,
15+
loss_func=loss_func,
16+
loss_kws=loss_kws,
17+
fit_intercept=fit_intercept,
18+
precomp_lip=None)
19+
20+
self.loss_func = loss_func
21+
self.loss_kws = loss_kws
22+
23+
self.opt_kws = opt_kws
24+
25+
def solve(self, L1_weights, opt_init=None, opt_init_upv=None):
26+
"""
27+
Parameters
28+
----------
29+
L1_weights: array-like
30+
Weights for lasso penalty.
31+
32+
opt_init: None, array-like
33+
Optional initializaiton for the coefficient.
34+
35+
opt_init_upv: None, array-like
36+
Optional initializaiton for the intercept.
37+
38+
Output
39+
------
40+
solution, upv_solution, other_data
41+
"""
42+
43+
coef, intercept, opt_data = \
44+
solve_glm(X=self.glm_loss.X,
45+
y=self.glm_loss.y,
46+
loss_func=self.loss_func,
47+
loss_kws=self.loss_kws,
48+
fit_intercept=self.glm_loss.fit_intercept,
49+
lasso_pen=1,
50+
lasso_weights=L1_weights,
51+
coef_init=opt_init,
52+
intercept_init=opt_init_upv,
53+
# groups=self.groups,
54+
# L1to2=self.L1to2,
55+
# nuc=self.nuc,
56+
**self.opt_kws)
57+
58+
return coef, intercept, opt_data
59+
60+
def loss(self, value, upv=None):
61+
"""
62+
Returns the loss function
63+
64+
loss(y) or loss(y, u)
65+
66+
Parameters
67+
----------
68+
value: array-like
69+
The value of the coefficient.
70+
71+
upv: None, array-like
72+
The intercept.
73+
74+
Output
75+
------
76+
loss: float
77+
"""
78+
if self.glm_loss.fit_intercept:
79+
#return self.glm_loss.eval(np.concatenate([[upv], value]))
80+
return self.glm_loss.eval(safe_concat(upv, value))
81+
82+
else:
83+
return self.glm_loss.eval(value)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from ya_glm.backends.scipy.quantile_lin_prog import solve_lin_prog
2+
from ya_glm.backends.scipy.quantile_quad import solve_quad_prog
3+
4+
5+
def solve_glm(X, y,
6+
loss_func='quantile',
7+
loss_kws={'quantile': 0.5},
8+
fit_intercept=True,
9+
10+
lasso_pen=None,
11+
lasso_weights=None,
12+
13+
ridge_pen=None,
14+
ridge_weights=None,
15+
tikhonov=None,
16+
17+
coef_init=None,
18+
intercept_init=None,
19+
20+
solver='default',
21+
tol=None,
22+
options=None
23+
):
24+
25+
quantile = loss_kws['quantile']
26+
27+
if lasso_weights is not None and lasso_pen is None:
28+
lasso_pen = 1
29+
30+
if (ridge_weights is not None or tikhonov is not None) \
31+
and ridge_pen is None:
32+
ridge_pen = 1
33+
34+
kws = {'X': X,
35+
'y': y,
36+
'fit_intercept': fit_intercept,
37+
'quantile': quantile,
38+
'lasso_pen': lasso_pen,
39+
'lasso_weights': lasso_weights,
40+
'sample_weights': None, # TODO: add
41+
'tol': tol}
42+
43+
if ridge_pen is None:
44+
if solver == 'default':
45+
solver = 'highs'
46+
47+
return solve_lin_prog(solver=solver,
48+
**kws)
49+
50+
else:
51+
if solver == 'default':
52+
solver = None
53+
54+
return solve_quad_prog(ridge_pen=ridge_pen,
55+
ridge_weights=ridge_weights,
56+
tikhonov=tikhonov,
57+
coef_init=coef_init,
58+
intercept_init=intercept_init,
59+
solver=solver,
60+
options=options,
61+
**kws)
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
2+
import warnings
3+
from time import time
4+
5+
import numpy as np
6+
from scipy.optimize import linprog
7+
from scipy.optimize import OptimizeWarning
8+
from scipy.linalg import LinAlgWarning
9+
10+
11+
def solve_lin_prog(X, y, fit_intercept=True, quantile=0.5, lasso_pen=1,
12+
sample_weights=None,
13+
lasso_weights=None,
14+
tol=None,
15+
solver='highs'):
16+
"""
17+
Solves the L1 penalized quantile regression problem using scipy's linprog solver. The code is adapted from https://github.com/benchopt/benchmark_quantile_regression and https://github.com/scikit-learn/scikit-learn/blob/0d064cfd4eda6dd4f7c8711a4870d2f02fda52fb/sklearn/linear_model/_quantile.py#L195-L209
18+
19+
Parameters
20+
----------
21+
X: array-like, shape (n_samples, n_features)
22+
The training covariate data.
23+
24+
y: array-like, shape (n_samples, )
25+
The training response data.
26+
27+
fit_intercept: bool
28+
Whether or not to fit an intercept.
29+
30+
quantile: float
31+
Which quantile.
32+
33+
lasso_pen: float
34+
The multiplicated penalty strength parameter.
35+
36+
sample_weights: None, array-like shape (n_features, )
37+
Sample weights
38+
39+
lasso_weights: None, array-like shape (n_features, )
40+
Feature weights for the L1 norm.
41+
42+
tol: None, float
43+
Tolerance for stopping criteria.
44+
45+
solver: str
46+
Which linprog solver to use, see scipy.optimize.linprog
47+
48+
Output
49+
------
50+
coef, intercept, opt_out
51+
"""
52+
start_time = time()
53+
54+
A_eq, b_eq, c, n_params = \
55+
get_lin_prog_data(X=X, y=y,
56+
fit_intercept=fit_intercept,
57+
quantile=quantile,
58+
lasso_pen=lasso_pen,
59+
sample_weights=sample_weights,
60+
lasso_weights=lasso_weights)
61+
62+
if 'highs' in solver:
63+
options = {'primal_feasibility_tolerance': tol}
64+
else:
65+
options = {'tol': tol}
66+
67+
warnings.filterwarnings('ignore', category=OptimizeWarning)
68+
warnings.filterwarnings('ignore', category=LinAlgWarning)
69+
70+
result = linprog(
71+
c=c,
72+
A_eq=A_eq,
73+
b_eq=b_eq,
74+
method=solver,
75+
options=options
76+
)
77+
78+
coef, intercept = get_coef_inter(solution=result.x,
79+
n_params=n_params,
80+
fit_intercept=fit_intercept)
81+
82+
opt_out = scipy_result_to_dict(result)
83+
opt_out['runtime'] = time() - start_time
84+
85+
return coef, intercept, opt_out
86+
87+
88+
def get_coef_inter(solution, n_params, fit_intercept):
89+
# positive slack - negative slack
90+
# solution is an array with (params_pos, params_neg, u, v)
91+
params = solution[:n_params] - solution[n_params:2 * n_params]
92+
93+
if fit_intercept:
94+
coef = params[1:]
95+
intercept = params[0]
96+
else:
97+
coef = params
98+
intercept = None
99+
100+
return coef, intercept
101+
102+
103+
def scipy_result_to_dict(result):
104+
return {'opt_val': result.fun,
105+
'success': result.success,
106+
'status': result.status,
107+
'nit': result.nit,
108+
'message': result.message}
109+
110+
111+
def get_lin_prog_data(X, y, fit_intercept=True, quantile=0.5, lasso_pen=1,
112+
sample_weights=None,
113+
lasso_weights=None):
114+
"""
115+
116+
Output
117+
------
118+
A_eq, b_eq, c, n_params
119+
"""
120+
121+
n_samples, n_features = X.shape
122+
123+
# TODO: perhaps filter zero sample weights as in https://github.com/scikit-learn/scikit-learn/blob/0d064cfd4eda6dd4f7c8711a4870d2f02fda52fb/sklearn/linear_model/_quantile.py#L195-L209
124+
125+
# format sample weights vec
126+
if sample_weights is None:
127+
sample_weights = np.ones(n_samples) / n_samples
128+
else:
129+
sample_weights = np.array(sample_weights).copy() / n_samples
130+
131+
# format the L1_vec
132+
if lasso_weights is None:
133+
L1_vec = np.ones(n_features)
134+
135+
else:
136+
assert len(lasso_weights) == n_features
137+
L1_vec = np.array(lasso_weights)
138+
139+
if fit_intercept:
140+
n_params = n_features + 1
141+
L1_vec = np.concatenate([[0], L1_vec, # 0 = do not penalize intercept
142+
[0], L1_vec])
143+
else:
144+
n_params = n_features
145+
L1_vec = np.concatenate([L1_vec, L1_vec])
146+
147+
# the linear programming formulation of quantile regression
148+
# follows https://stats.stackexchange.com/questions/384909/
149+
150+
c = np.concatenate([
151+
L1_vec * lasso_pen,
152+
sample_weights * quantile,
153+
sample_weights * (1 - quantile),
154+
])
155+
156+
if fit_intercept:
157+
158+
A_eq = np.concatenate([
159+
np.ones((n_samples, 1)),
160+
X,
161+
-np.ones((n_samples, 1)),
162+
-X,
163+
np.eye(n_samples),
164+
-np.eye(n_samples),
165+
], axis=1)
166+
167+
else:
168+
A_eq = np.concatenate([
169+
X,
170+
-X,
171+
np.eye(n_samples),
172+
-np.eye(n_samples),
173+
], axis=1)
174+
175+
return A_eq, y, c, n_params

0 commit comments

Comments
 (0)