-
Notifications
You must be signed in to change notification settings - Fork 197
[ENH] Add OnlineSlidingWindow and OnlineExponentialForgetting meta-strategies #1098
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
Open
utsab345
wants to merge
2
commits into
sktime:main
Choose a base branch
from
utsab345:fix/online-sliding-window-exponential-forgetting
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,230 @@ | ||
| """Meta-strategy for online learning: exponential forgetting.""" | ||
|
|
||
| __author__ = ["utsab345"] | ||
| __all__ = ["OnlineExponentialForgetting"] | ||
|
|
||
| import numpy as np | ||
| import pandas as pd | ||
|
|
||
| from skpro.regression.base import _DelegatedProbaRegressor | ||
|
|
||
|
|
||
| class OnlineExponentialForgetting(_DelegatedProbaRegressor): | ||
| """Online regression strategy by exponential forgetting of old data. | ||
|
|
||
| In ``fit``, fits the regressor on all provided data and assigns each | ||
| observation a weight of 1. | ||
|
|
||
| In ``update``, decays the weight of all previously seen observations | ||
| by ``alpha``. New observations enter with weight 1. Observations whose | ||
| weight drops below ``min_weight`` are pruned. The regressor is then | ||
| refitted on the surviving data. | ||
|
|
||
| Acts as a soft sliding window where the effective window length is | ||
| approximately ``log(min_weight) / log(alpha)`` updates. | ||
|
|
||
| Caveat: data indices are reset to RangeIndex internally, even if some indices | ||
| passed in ``fit`` and ``update`` overlap. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| estimator : skpro regressor, descendant of BaseProbaRegressor | ||
| regressor to be refitted on data surviving the weight threshold, blueprint | ||
| alpha : float, default=0.95 | ||
| decay factor applied to all existing observation weights at each update. | ||
| Must be in (0, 1). Smaller values forget old data faster. | ||
| min_weight : float, default=1e-3 | ||
| minimum weight threshold. Observations with weight below this | ||
| value are pruned before refitting. Must be in (0, 1]. | ||
|
|
||
| Attributes | ||
| ---------- | ||
| estimator_ : skpro regressor, descendant of BaseProbaRegressor | ||
| clone of the regressor passed in the constructor, fitted on surviving data | ||
| """ | ||
|
|
||
| _tags = {"capability:update": True} | ||
|
|
||
| def __init__(self, estimator, alpha=0.95, min_weight=1e-3): | ||
| self.estimator = estimator | ||
| self.alpha = alpha | ||
| self.min_weight = min_weight | ||
|
|
||
| super().__init__() | ||
|
|
||
| tags_to_clone = [ | ||
| "capability:missing", | ||
| "capability:survival", | ||
| ] | ||
| self.clone_tags(estimator, tags_to_clone) | ||
|
|
||
| def _fit(self, X, y, C=None): | ||
| """Fit regressor to training data. | ||
|
|
||
| Writes to self: | ||
| Sets fitted model attributes ending in "_". | ||
|
|
||
| Parameters | ||
| ---------- | ||
| X : pandas DataFrame | ||
| feature instances to fit regressor to | ||
| y : pandas DataFrame, must be same length as X | ||
| labels to fit regressor to | ||
| C : pd.DataFrame, optional (default=None) | ||
| censoring information for survival analysis, | ||
| should have same column name as y, same length as X and y | ||
| should have entries 0 and 1 (float or int) | ||
| 0 = uncensored, 1 = (right) censored | ||
| if None, all observations are assumed to be uncensored | ||
| Can be passed to any probabilistic regressor, | ||
| but is ignored if capability:survival tag is False. | ||
|
|
||
| Returns | ||
| ------- | ||
| self : reference to self | ||
| """ | ||
| estimator = self.estimator.clone() | ||
| estimator.fit(X=X, y=y, C=C) | ||
| self.estimator_ = estimator | ||
|
|
||
| # remember data and initialise weights | ||
| self._X = X | ||
| self._y = y | ||
| self._C = C | ||
| self._weights = pd.Series(np.ones(len(X)), index=X.index) | ||
|
|
||
| return self | ||
|
|
||
| def _update(self, X, y, C=None): | ||
| """Update regressor with new batch of training data. | ||
|
|
||
| State required: | ||
| Requires state to be "fitted". | ||
|
|
||
| Writes to self: | ||
| Updates fitted model attributes ending in "_". | ||
|
|
||
| Parameters | ||
| ---------- | ||
| X : pandas DataFrame | ||
| feature instances to fit regressor to | ||
| y : pandas DataFrame, must be same length as X | ||
| labels to fit regressor to | ||
| C : pd.DataFrame, optional (default=None) | ||
| censoring information for survival analysis, | ||
| should have same column name as y, same length as X and y | ||
| should have entries 0 and 1 (float or int) | ||
| 0 = uncensored, 1 = (right) censored | ||
| if None, all observations are assumed to be uncensored | ||
| Can be passed to any probabilistic regressor, | ||
| but is ignored if capability:survival tag is False. | ||
|
|
||
| Returns | ||
| ------- | ||
| self : reference to self | ||
| """ | ||
| alpha = self.alpha | ||
| min_weight = self.min_weight | ||
|
|
||
| # decay existing weights | ||
| self._weights = self._weights * alpha | ||
|
|
||
| # concatenate new data | ||
| n_new = len(X) | ||
|
|
||
| X_pool = self._update_data(self._X, X) | ||
| y_pool = self._update_data(self._y, y) | ||
| C_pool = self._update_data(self._C, C) | ||
|
|
||
| # append weights of 1 for new observations | ||
| new_weights = pd.Series(np.ones(n_new)) | ||
| self._weights = pd.concat([self._weights, new_weights], ignore_index=True) | ||
|
|
||
| # prune observations below min_weight | ||
| mask = self._weights >= min_weight | ||
|
|
||
| X_pool = X_pool.loc[mask.values].reset_index(drop=True) | ||
| y_pool = y_pool.loc[mask.values].reset_index(drop=True) | ||
| if C_pool is not None: | ||
| C_pool = C_pool.loc[mask.values].reset_index(drop=True) | ||
| self._weights = self._weights.loc[mask.values].reset_index(drop=True) | ||
|
|
||
| # user-provided sample_weight via metadata? not supported yet | ||
|
|
||
| # refit on surviving data | ||
| estimator = self.estimator.clone() | ||
| estimator.fit(X=X_pool, y=y_pool, C=C_pool) | ||
| self.estimator_ = estimator | ||
|
|
||
| # remember data | ||
| self._X = X_pool | ||
| self._y = y_pool | ||
| self._C = C_pool | ||
|
|
||
| return self | ||
|
|
||
| def _update_data(self, X, X_new): | ||
| """Update data with new batch of training data. | ||
|
|
||
| Treats X_new as data with new indices, even if some indices overlap with X. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| X : pandas DataFrame | ||
| X_new : pandas DataFrame | ||
|
|
||
| Returns | ||
| ------- | ||
| X_updated : pandas DataFrame | ||
| concatenated data, with reset index | ||
| """ | ||
| if X is None and X_new is None: | ||
| return None | ||
| if X is None and X_new is not None: | ||
| return X_new.reset_index(drop=True) | ||
| if X is not None and X_new is None: | ||
| return X.reset_index(drop=True) | ||
| # else, both X and X_new are not None | ||
| X_updated = pd.concat([X, X_new], ignore_index=True) | ||
| return X_updated | ||
|
|
||
| @classmethod | ||
| def get_test_params(cls, parameter_set="default"): | ||
| """Return testing parameter settings for the estimator. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| parameter_set : str, default="default" | ||
| Name of the set of test parameters to return, for use in tests. If no | ||
| special parameters are defined for a value, will return ``"default"`` set. | ||
|
|
||
| Returns | ||
| ------- | ||
| params : dict or list of dict, default = {} | ||
| Parameters to create testing instances of the class | ||
| Each dict are parameters to construct an "interesting" test instance, i.e., | ||
| ``MyClass(**params)`` or ``MyClass(**params[i])`` creates a valid test | ||
| instance. | ||
| ``create_test_instance`` uses the first (or only) dictionary in ``params`` | ||
| """ | ||
| from skbase.utils.dependencies import _check_estimator_deps | ||
| from sklearn.linear_model import LinearRegression, Ridge | ||
|
|
||
| from skpro.regression.residual import ResidualDouble | ||
| from skpro.survival.coxph import CoxPH | ||
|
|
||
| regressor = ResidualDouble(LinearRegression()) | ||
|
|
||
| params = [ | ||
| {"estimator": regressor}, | ||
| {"estimator": regressor, "alpha": 0.9, "min_weight": 1e-2}, | ||
| ] | ||
|
|
||
| if _check_estimator_deps(CoxPH, severity="none"): | ||
| coxph = CoxPH() | ||
| params.append({"estimator": coxph}) | ||
| else: | ||
| ridge = Ridge() | ||
| params.append({"estimator": ResidualDouble(ridge)}) | ||
|
|
||
| return params | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should say:
estimator : skpro regressor, descendant of BaseProbaRegressor
regressor to be update-refitted on surviving data, blueprint
Or more precisely: "regressor to be refitted on data surviving the weight threshold, blueprint"