Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings
This repository was archived by the owner on Jul 24, 2024. It is now read-only.

Commit de9b92b

Browse files
eric-valenteStephen Hoover
authored and
Stephen Hoover
committed
Exposing the lower and upper limits V.2 (#41)
Follow the behavior of the R wrapper for allowing users to specify lower and upper limits on coefficients.
1 parent 87ca3af commit de9b92b

File tree

4 files changed

+88
-15
lines changed

4 files changed

+88
-15
lines changed

‎glmnet/linear.py‎

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ class ElasticNet(BaseEstimator):
4444
will be on the scale of the original data regardless of the value
4545
of standardize.
4646
47+
lower_limits : array, (shape n_features,) default -infinity
48+
Array of lower limits for each coefficient, must be non-positive.
49+
Can be a single value (which is then replicated), else an array
50+
corresponding to the number of features.
51+
52+
upper_limits : array, (shape n_features,) default +infinity
53+
Array of upper limits for each coefficient, must be positive.
54+
See lower_limits.
55+
4756
fit_intercept : bool, default True
4857
Include an intercept term in the model.
4958
@@ -128,6 +137,7 @@ class ElasticNet(BaseEstimator):
128137

129138
def __init__(self, alpha=1, n_lambda=100, min_lambda_ratio=1e-4,
130139
lambda_path=None, standardize=True, fit_intercept=True,
140+
lower_limits=-np.inf, upper_limits=np.inf,
131141
cut_point=1.0, n_splits=3, scoring=None, n_jobs=1, tol=1e-7,
132142
max_iter=100000, random_state=None, max_features=None, verbose=False):
133143

@@ -136,6 +146,8 @@ def __init__(self, alpha=1, n_lambda=100, min_lambda_ratio=1e-4,
136146
self.min_lambda_ratio = min_lambda_ratio
137147
self.lambda_path = lambda_path
138148
self.standardize = standardize
149+
self.lower_limits = lower_limits
150+
self.upper_limits = upper_limits
139151
self.fit_intercept = fit_intercept
140152
self.cut_point = cut_point
141153
self.n_splits = n_splits
@@ -146,7 +158,6 @@ def __init__(self, alpha=1, n_lambda=100, min_lambda_ratio=1e-4,
146158
self.random_state = random_state
147159
self.max_features = max_features
148160
self.verbose = verbose
149-
150161
self.cv = None
151162

152163
def fit(self, X, y, sample_weight=None, relative_penalties=None):
@@ -185,16 +196,33 @@ def fit(self, X, y, sample_weight=None, relative_penalties=None):
185196
self : object
186197
Returns self.
187198
"""
199+
200+
X, y = check_X_y(X, y, accept_sparse='csr', ensure_min_samples=2)
201+
if sample_weight is None:
202+
sample_weight = np.ones(X.shape[0])
203+
204+
if not np.isscalar(self.lower_limits):
205+
self.lower_limits = np.asarray(self.lower_limits)
206+
if len(self.lower_limits) != X.shape[1]:
207+
raise ValueError("lower_limits must equal number of features")
208+
209+
if not np.isscalar(self.upper_limits):
210+
self.upper_limits = np.asarray(self.upper_limits)
211+
if len(self.upper_limits) != X.shape[1]:
212+
raise ValueError("upper_limits must equal number of features")
213+
214+
if any(self.lower_limits > 0) if isinstance(self.lower_limits, np.ndarray) else self.lower_limits > 0:
215+
raise ValueError("lower_limits must be non-positive")
216+
217+
if any(self.upper_limits < 0) if isinstance(self.upper_limits, np.ndarray) else self.upper_limits < 0:
218+
raise ValueError("upper_limits must be positive")
219+
188220
if self.alpha > 1 or self.alpha < 0:
189221
raise ValueError("alpha must be between 0 and 1")
190222

191223
if self.n_splits > 0 and self.n_splits < 3:
192224
raise ValueError("n_splits must be at least 3")
193225

194-
X, y = check_X_y(X, y, accept_sparse='csr', ensure_min_samples=2)
195-
if sample_weight is None:
196-
sample_weight = np.ones(X.shape[0])
197-
198226
self._fit(X, y, sample_weight, relative_penalties)
199227

200228
if self.n_splits >= 3:
@@ -247,8 +275,8 @@ def _fit(self, X, y, sample_weight, relative_penalties):
247275
order='F')
248276

249277
coef_bounds = np.empty((2, X.shape[1]), dtype=np.float64, order='F')
250-
coef_bounds[0, :] = -np.inf
251-
coef_bounds[1, :] = np.inf
278+
coef_bounds[0, :] = self.lower_limits
279+
coef_bounds[1, :] = self.upper_limits
252280

253281
if X.shape[1] > X.shape[0]:
254282
# the glmnet docs suggest using a different algorithm for the case

‎glmnet/logistic.py‎

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,15 @@ class LogitNet(BaseEstimator):
4848
4949
fit_intercept : bool, default True
5050
Include an intercept term in the model.
51+
52+
lower_limits : array, (shape n_features,) default -infinity
53+
Array of lower limits for each coefficient, must be non-positive.
54+
Can be a single value (which is then replicated), else an array
55+
corresponding to the number of features.
56+
57+
upper_limits : array, (shape n_features,) default +infinity
58+
Array of upper limits for each coefficient, must be positive.
59+
See lower_limits.
5160
5261
cut_point : float, default 1
5362
The cut point to use for selecting lambda_best.
@@ -133,6 +142,7 @@ class LogitNet(BaseEstimator):
133142

134143
def __init__(self, alpha=1, n_lambda=100, min_lambda_ratio=1e-4,
135144
lambda_path=None, standardize=True, fit_intercept=True,
145+
lower_limits=-np.inf, upper_limits=np.inf,
136146
cut_point=1.0, n_splits=3, scoring=None, n_jobs=1, tol=1e-7,
137147
max_iter=100000, random_state=None, max_features=None, verbose=False):
138148

@@ -141,6 +151,8 @@ def __init__(self, alpha=1, n_lambda=100, min_lambda_ratio=1e-4,
141151
self.min_lambda_ratio = min_lambda_ratio
142152
self.lambda_path = lambda_path
143153
self.standardize = standardize
154+
self.lower_limits = lower_limits
155+
self.upper_limits = upper_limits
144156
self.fit_intercept = fit_intercept
145157
self.cut_point = cut_point
146158
self.n_splits = n_splits
@@ -151,7 +163,6 @@ def __init__(self, alpha=1, n_lambda=100, min_lambda_ratio=1e-4,
151163
self.random_state = random_state
152164
self.max_features = max_features
153165
self.verbose = verbose
154-
155166
self.cv = None
156167

157168
def fit(self, X, y, sample_weight=None, relative_penalties=None):
@@ -190,12 +201,28 @@ def fit(self, X, y, sample_weight=None, relative_penalties=None):
190201
self : object
191202
Returns self.
192203
"""
193-
if self.alpha > 1 or self.alpha < 0:
194-
raise ValueError("alpha must be between 0 and 1")
195-
196204
X, y = check_X_y(X, y, accept_sparse='csr', ensure_min_samples=2)
197205
if sample_weight is None:
198-
sample_weight = np.ones(X.shape[0])
206+
sample_weight = np.ones(X.shape[0])
207+
208+
if not np.isscalar(self.lower_limits):
209+
self.lower_limits = np.asarray(self.lower_limits)
210+
if len(self.lower_limits) != X.shape[1]:
211+
raise ValueError("lower_limits must equal number of features")
212+
213+
if not np.isscalar(self.upper_limits):
214+
self.upper_limits = np.asarray(self.upper_limits)
215+
if len(self.upper_limits) != X.shape[1]:
216+
raise ValueError("upper_limits must equal number of features")
217+
218+
if any(self.lower_limits > 0) if isinstance(self.lower_limits, np.ndarray) else self.lower_limits > 0:
219+
raise ValueError("lower_limits must be non-positive")
220+
221+
if any(self.upper_limits < 0) if isinstance(self.upper_limits, np.ndarray) else self.upper_limits < 0:
222+
raise ValueError("upper_limits must be positive")
223+
224+
if self.alpha > 1 or self.alpha < 0:
225+
raise ValueError("alpha must be between 0 and 1")
199226

200227
# fit the model
201228
self._fit(X, y, sample_weight, relative_penalties)
@@ -293,9 +320,9 @@ def _fit(self, X, y, sample_weight=None, relative_penalties=None):
293320
relative_penalties = np.ones(X.shape[1], dtype=np.float64,
294321
order='F')
295322

296-
coef_bounds = np.empty((2, X.shape[1]), dtype=np.float64, order='F')
297-
coef_bounds[0, :] = -np.inf
298-
coef_bounds[1, :] = np.inf
323+
coef_bounds = np.empty((2, X.shape[1]), dtype=np.float64, order='F')
324+
coef_bounds[0, :] = self.lower_limits
325+
coef_bounds[1, :] = self.upper_limits
299326

300327
if n_classes == 2:
301328
# binomial, tell glmnet there is only one class

‎glmnet/tests/test_linear.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,15 @@ def test_alphas(self):
102102
m = m.fit(x, y)
103103
self.check_r2_score(y, m.predict(x), 0.90, alpha=alpha)
104104

105+
def test_coef_limits(self):
106+
x, y = self.inputs[0]
107+
lower_limits = np.repeat(-1, x.shape[1])
108+
upper_limits = 0
109+
m = ElasticNet(lower_limits=lower_limits, upper_limits=upper_limits, random_state=5934, alpha=0)
110+
m = m.fit(x, y)
111+
assert(np.all(m.coef_ >= -1))
112+
assert(np.all(m.coef_ <= 0))
113+
105114
def test_n_splits(self):
106115
x, y = self.inputs[0]
107116
for n in self.n_splits:

‎glmnet/tests/test_logistic.py‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,15 @@ def test_alphas(self):
9292
m = m.fit(x, y)
9393
check_accuracy(y, m.predict(x), 0.85, alpha=alpha)
9494

95+
def test_coef_limits(self):
96+
x, y = self.binomial[0]
97+
lower_limits = np.repeat(-1, x.shape[1])
98+
upper_limits = 0
99+
m = LogitNet(lower_limits=lower_limits, upper_limits=upper_limits, random_state=69265, alpha=0)
100+
m = m.fit(x, y)
101+
assert(np.all(m.coef_ >= -1))
102+
assert(np.all(m.coef_ <= 0))
103+
95104
def test_relative_penalties(self):
96105
x, y = self.binomial[0]
97106
p = x.shape[1]

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /