Skip to content

Commit 100ada6

Browse files
committed
disallow negative coefficients
1 parent 49e1aa6 commit 100ada6

File tree

4 files changed

+45
-15
lines changed

4 files changed

+45
-15
lines changed

openenergyid/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Open Energy ID Python SDK."""
22

3-
__version__ = "0.1.8"
3+
__version__ = "0.1.9"
44

55
from .enums import Granularity
66
from .models import TimeSeries

openenergyid/mvlr/main.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def find_best_mvlr(
1818
granularity=granularity,
1919
allow_negative_predictions=data.allow_negative_predictions,
2020
single_use_exog_prefixes=data.single_use_exog_prefixes,
21+
exogs__disallow_negative_coefficient=data.get_disallowed_negative_coefficients(),
2122
)
2223
mvlr.do_analysis()
2324
if mvlr.validate(

openenergyid/mvlr/models.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ class IndependentVariableInput(BaseModel):
5151
"Eg. `HDD_16.5` will be Heating Degree Days with a base temperature of 16.5°C, "
5252
"`CDD_0` will be Cooling Degree Days with a base temperature of 0°C.",
5353
)
54+
allow_negative_coefficient: bool = Field(
55+
default=True,
56+
alias="allowNegativeCoefficient",
57+
description="Whether the coefficient can be negative.",
58+
)
5459

5560

5661
class MultiVariableRegressionInput(BaseModel):
@@ -123,6 +128,17 @@ def data_frame(self) -> pd.DataFrame:
123128

124129
return frame
125130

131+
def get_disallowed_negative_coefficients(self) -> List[str]:
132+
"""Get independent variables that are not allowed to have a negative coefficient."""
133+
result = []
134+
for iv in self.independent_variables: # pylint: disable=not-an-iterable
135+
if iv.name == COLUMN_TEMPERATUREEQUIVALENT and iv.variants is not None:
136+
if not iv.allow_negative_coefficient:
137+
result.extend(iv.variants)
138+
elif not iv.allow_negative_coefficient:
139+
result.append(iv.name)
140+
return result
141+
126142

127143
######################
128144
# MVLR Result Models #

openenergyid/mvlr/mvlr.py

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def __init__(
4141
allow_negative_predictions: bool = False,
4242
granularity: Granularity = None,
4343
single_use_exog_prefixes: list[str] = None,
44+
exogs__disallow_negative_coefficient: list[str] = None,
4445
):
4546
"""Parameters
4647
----------
@@ -72,6 +73,8 @@ def __init__(
7273
will be used as an independent variable.
7374
Once the best fit using a variable with a given prefix is found, the other variables with the same
7475
prefix will not be used as independent variables.
76+
exogs__disallow_negative_coefficient : list of str, default=None
77+
List of variable names for which the coefficient is not allowed to be negative.
7578
"""
7679
self.data = data.copy()
7780
if y not in self.data.columns:
@@ -87,6 +90,7 @@ def __init__(
8790
self.allow_negative_predictions = allow_negative_predictions
8891
self.granularity = granularity
8992
self.single_use_exog_prefixes = single_use_exog_prefixes
93+
self.exogs__disallow_negative_coefficient = exogs__disallow_negative_coefficient
9094
self._fit = None
9195
self._list_of_fits = []
9296
self.list_of_cverrors = []
@@ -161,6 +165,15 @@ def _do_analysis_no_cross_validation(self):
161165
ref_fit.model.formula.rhs_termlist + [term],
162166
)
163167
fit = fm.ols(model_desc, data=self.data).fit()
168+
169+
# Check if the coefficient of the variable is allowed to be negative
170+
if (
171+
self.exogs__disallow_negative_coefficient is not None
172+
and x in self.exogs__disallow_negative_coefficient
173+
and fit.params[x] < 0
174+
):
175+
continue
176+
164177
if fit.bic < best_bic:
165178
best_bic = fit.bic
166179
best_fit = fit
@@ -174,20 +187,20 @@ def _do_analysis_no_cross_validation(self):
174187
ref_fit.model.formula.rhs_termlist,
175188
):
176189
break
177-
else:
178-
self._list_of_fits.append(best_fit)
179-
all_model_terms_dict.pop(best_x)
180-
181-
# Check if `best_x` starts with a prefix that should only be used once
182-
# If so, remove all other variables with the same prefix from the list of candidates
183-
if self.single_use_exog_prefixes:
184-
for prefix in self.single_use_exog_prefixes:
185-
if best_x.startswith(prefix):
186-
all_model_terms_dict = {
187-
k: v
188-
for k, v in all_model_terms_dict.items()
189-
if not k.startswith(prefix)
190-
}
190+
191+
self._list_of_fits.append(best_fit)
192+
all_model_terms_dict.pop(best_x)
193+
194+
# Check if `best_x` starts with a prefix that should only be used once
195+
# If so, remove all other variables with the same prefix from the list of candidates
196+
if self.single_use_exog_prefixes:
197+
for prefix in self.single_use_exog_prefixes:
198+
if best_x.startswith(prefix):
199+
all_model_terms_dict = {
200+
k: v
201+
for k, v in all_model_terms_dict.items()
202+
if not k.startswith(prefix)
203+
}
191204

192205
self._fit = self._list_of_fits[-1]
193206

0 commit comments

Comments
 (0)