Skip to content
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

Refactor/forecast residuals fn #1223

Merged
merged 19 commits into from
Sep 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
d86cca8
- silicon M1 installation guide
eliane-maalouf Sep 13, 2022
0de2309
- silicon M1 installation guide
eliane-maalouf Sep 13, 2022
cf7a92a
- past and future covariates in residuals()
eliane-maalouf Sep 13, 2022
65f543d
- update about emulation of x_64 on Apple Silicon M1 (INSTALL.md and …
eliane-maalouf Sep 13, 2022
ae3c4e9
- update 2 about emulation of x_64 on Apple Silicon M1 (INSTALL.md an…
eliane-maalouf Sep 14, 2022
7dc748e
- update_final about emulation of x_64 on Apple Silicon M1 (INSTALL.m…
eliane-maalouf Sep 14, 2022
aeddc1a
- update_final (typo) about emulation of x_64 on Apple Silicon M1 (IN…
eliane-maalouf Sep 14, 2022
759e2a7
Merge branch 'master' into refactor/regression_models
eliane-maalouf Sep 14, 2022
39e802d
- corrected installation docs - x_64 emulation on M1
eliane-maalouf Sep 16, 2022
deea856
- Updated forecasting_model/residuals() fct with past/future covariat…
eliane-maalouf Sep 19, 2022
1af10bb
corrected formatting
eliane-maalouf Sep 19, 2022
d7716a5
Merge branch 'master' into refactor/forecast_residuals_fn
eliane-maalouf Sep 19, 2022
7c3d5d7
corrected formatting
eliane-maalouf Sep 19, 2022
e8b7ade
Merge remote-tracking branch 'origin/refactor/forecast_residuals_fn' …
eliane-maalouf Sep 19, 2022
0f88d20
corrected formatting
eliane-maalouf Sep 19, 2022
1a22ccc
corrected formatting
eliane-maalouf Sep 19, 2022
34192d8
Merge branch 'master' into refactor/forecast_residuals_fn
hrzn Sep 19, 2022
fd72dcf
- updated unitTest for residuals() with one extra case
eliane-maalouf Sep 20, 2022
d3d10a3
Merge remote-tracking branch 'origin/refactor/forecast_residuals_fn' …
eliane-maalouf Sep 20, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,11 @@ To ensure you don't need to worry about formatting and linting when contributing
- Integration in your editor:
- For [Black](https://black.readthedocs.io/en/stable/integrations/editors.html)
- For other integrations please look at the documentation for your editor

### Developement environment on Mac with Apple Silicon M1 processor (arm64 architecture)

Please follow the procedure decribed in [INSTALL.md](https://github.com/unit8co/darts/blob/master/INSTALL.md#test-environment-appple-m1-processor)
to set up a x_64 emulated environment. For the development environment, instead of installing Darts with
`pip install darts`, instead go to the darts cloned repo location and install the packages with: `pip install -r requirements/dev-all.txt`.
If necessary, follow the same steps to setup libomp for lightgbm.
Finally, verify your overall environment setup by successfully running all unitTests with gradlew or pytest.
39 changes: 39 additions & 0 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,45 @@ brew unlink libomp
brew install libomp.rb
```

#### Test environment Appple M1 processor

We currently recommend to run Darts in an x_64 emulated environment on Mac computers with the Silicon M1 processor,
instead of trying to install directly with native arm64 packages, many of the dependent packages still have compatibility
issues. The following is a proposed procedure, if you tested other procedures on similar hardware and they worked,
please let us know about them by opening an issue or by updating this file and opening a PR.

Below are the necessary instructions to create and configure the environment:
- Start by installing conda (e.g., with miniforge : `brew install miniforge`).
- Create the x_64 environment : `CONDA_SUBDIR=osx-64 conda create -n env_name python=3.9 pip`
- Activate the created environment: `conda activate env_name`
- Configure the environment : `conda env config vars set CONDA_SUBDIR=osx-64`
- Deactivate and reactivate the environment:
```
conda deactivate
conda activate env_name
```
- Install darts: `pip install darts`
- With this method of installation, lightgbm might still have issues finding the libomp library.
The following procedure is to garantee that the correct libomp (11.1.0) library is linked.
- Unlink the existing libomp, from terminal : `brew unlink libomp`
- Setup a homebrew installer that is compatible with x_64 packages (follow this [blog](https://medium.com/mkdir-awesome/how-to-install-x86-64-homebrew-packages-on-apple-m1-macbook-54ba295230f)
post):
```
cd ~/Downloads
mkdir homebrew
curl -L https://github.com/Homebrew/brew/tarball/master | tar xz --strip 1 -C homebrew
sudo mv homebrew /usr/local/homebrew
export PATH=$HOME/bin:/usr/local/bin:$PATH
```
- At this point, we have a new brew command located at /usr/local/homebrew/bin/brew
- In the following code bits we download version 11.1.0 of libomp, install it as a x_64 compatible package and link to it so that lightgbm can find it:
```
wget https://raw.githubusercontent.com/Homebrew/homebrew-core/fb8323f2b170bd4ae97e1bac9bf3e2983af3fdb0/Formula/libomp.rb
arch -x86_64 /usr/local/homebrew/bin/brew install libomp.rb
sudo ln -s /usr/local/homebrew/Cellar/libomp/11.1.0/lib /usr/local/opt/libomp/lib
```
- Verify that your lightgbm works by importing lightgbm from your python env. It should not give library loading errors.

## Running the examples only, without installing:

If the conda setup is causing too many problems, we also provide a Docker image with everything set up for you and ready-to-use Python notebooks with demo examples.
Expand Down
32 changes: 29 additions & 3 deletions darts/models/forecasting/forecasting_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ def fit(self, series: TimeSeries) -> "ForecastingModel":

def _supports_range_index(self) -> bool:
"""Checks if the forecasting model supports a range index.
Some models may not support this, if for instance the rely on underlying dates.
Some models may not support this, if for instance they rely on underlying dates.

By default, returns True. Needs to be overwritten by models that do not support
range indexing and raise meaningful exception.
Expand Down Expand Up @@ -878,6 +878,8 @@ def _evaluate_combination(param_combination) -> float:
def residuals(
self,
series: TimeSeries,
past_covariates: Optional[TimeSeries] = None,
future_covariates: Optional[TimeSeries] = None,
forecast_horizon: int = 1,
retrain: bool = True,
verbose: bool = False,
Expand All @@ -894,13 +896,17 @@ def residuals(
Most commonly, the term "residuals" implies a value for `forecast_horizon` of 1; but
this can be configured.

This method works only on univariate series and does not currently support covariates. It uses the median
This method works only on univariate series. It uses the median
prediction (when dealing with stochastic forecasts).

Parameters
----------
series
The univariate TimeSeries instance which the residuals will be computed for.
past_covariates
One or several past-observed covariate time series.
future_covariates
One or several future-known covariate time series.
forecast_horizon
The forecasting horizon used to predict each fitted value.
retrain
Expand All @@ -913,14 +919,34 @@ def residuals(
TimeSeries
The vector of residuals.
"""
series._assert_univariate()
try:
series._assert_univariate()
except (AttributeError, TypeError):
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

raise ValueError(
"series must be of type TimeSeries. "
"If Sequence[TimeSeries] is provided, select the series to compute residuals for."
)

if past_covariates is not None:
raise_if_not(
isinstance(past_covariates, TimeSeries),
"past_covariates should be of type TimeSeries",
)

if future_covariates is not None:
raise_if_not(
isinstance(future_covariates, TimeSeries),
"future_covariates should be of type TimeSeries",
)

# get first index not contained in the first training set
first_index = series.time_index[self.min_train_series_length]

# compute fitted values
p = self.historical_forecasts(
series=series,
past_covariates=past_covariates,
future_covariates=future_covariates,
start=first_index,
forecast_horizon=forecast_horizon,
stride=1,
Expand Down
30 changes: 1 addition & 29 deletions darts/tests/models/forecasting/test_backtesting.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,8 @@
from darts import TimeSeries
from darts.logging import get_logger
from darts.metrics import mape, r2_score
from darts.models import (
ARIMA,
FFT,
ExponentialSmoothing,
NaiveDrift,
NaiveSeasonal,
Theta,
)
from darts.models import ARIMA, FFT, ExponentialSmoothing, NaiveDrift, Theta
from darts.tests.base_test_class import DartsBaseTestClass
from darts.utils.timeseries_generation import constant_timeseries as ct
from darts.utils.timeseries_generation import gaussian_timeseries as gt
from darts.utils.timeseries_generation import linear_timeseries as lt
from darts.utils.timeseries_generation import random_walk_timeseries as rt
Expand Down Expand Up @@ -549,23 +541,3 @@ def test_gridsearch_multi(self):
"kernel_size": [2, 3, 4],
}
TCNModel.gridsearch(tcn_params, dummy_series, forecast_horizon=3, metric=mape)

def test_forecasting_residuals(self):
model = NaiveSeasonal(K=1)

# test zero residuals
constant_ts = ct(length=20)
residuals = model.residuals(constant_ts)
np.testing.assert_almost_equal(
residuals.univariate_values(), np.zeros(len(residuals))
)

# test constant, positive residuals
linear_ts = lt(length=20)
residuals = model.residuals(linear_ts)
np.testing.assert_almost_equal(
np.diff(residuals.univariate_values()), np.zeros(len(residuals) - 1)
)
np.testing.assert_array_less(
np.zeros(len(residuals)), residuals.univariate_values()
)
132 changes: 132 additions & 0 deletions darts/tests/utils/test_residuals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import numpy as np
Copy link
Contributor

Choose a reason for hiding this comment

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

+1 for the separate test file


from darts.logging import get_logger
from darts.models import LinearRegressionModel, NaiveSeasonal
from darts.tests.base_test_class import DartsBaseTestClass
from darts.tests.models.forecasting.test_regression_models import dummy_timeseries
from darts.utils.timeseries_generation import constant_timeseries as ct
from darts.utils.timeseries_generation import linear_timeseries as lt

logger = get_logger(__name__)


class TestResidualsTestCase(DartsBaseTestClass):

np.random.seed(42)

def test_forecasting_residuals_nocov_output(self):
model = NaiveSeasonal(K=1)

# test zero residuals
constant_ts = ct(length=20)
residuals = model.residuals(constant_ts)
np.testing.assert_almost_equal(
residuals.univariate_values(), np.zeros(len(residuals))
)

# test constant, positive residuals
linear_ts = lt(length=20)
residuals = model.residuals(linear_ts)
np.testing.assert_almost_equal(
np.diff(residuals.univariate_values()), np.zeros(len(residuals) - 1)
)
np.testing.assert_array_less(
np.zeros(len(residuals)), residuals.univariate_values()
)

def test_forecasting_residuals_inputs(self):
# test input types past and/or future covariates

# dummy covariates and target TimeSeries instances

target_series, past_covariates, future_covariates = dummy_timeseries(
length=10,
n_series=1,
comps_target=1,
comps_pcov=1,
comps_fcov=1,
) # outputs Sequences[TimeSeries] and not TimeSeries

model = LinearRegressionModel(
lags=4, lags_past_covariates=4, lags_future_covariates=(4, 1)
)
model.fit(
series=target_series,
past_covariates=past_covariates,
future_covariates=future_covariates,
)
# residuals() will fail if the inputs are not of TimeSeries type (Sequence[TimeSeries] don't work)
# because it starts by asserting that the provided object is a univariate TimeSeries.
# The following asserts the correct TimeSeries types

with self.assertRaises(ValueError):
model.residuals(target_series)

with self.assertRaises(ValueError):
model.residuals(target_series, past_covariates=past_covariates)

with self.assertRaises(ValueError):
model.residuals(target_series, future_covariates=future_covariates)

with self.assertRaises(ValueError):
model.residuals(
target_series,
past_covariates=past_covariates,
future_covariates=future_covariates,
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you also add a test case where the residuals computation carries out successfully with past and/or future covariates?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok will do


def test_forecasting_residuals_cov_output(self):
# if covariates are constant and the target is constant/linear,
# residuals should be zero (for a LinearRegression model)

target_series_1 = ct(value=0.5, length=10)
target_series_2 = lt(length=10)
past_covariates = ct(value=0.2, length=10)
future_covariates = ct(value=0.1, length=10)

model_1 = LinearRegressionModel(
lags=1, lags_past_covariates=1, lags_future_covariates=(1, 1)
)
model_2 = LinearRegressionModel(
lags=1, lags_past_covariates=1, lags_future_covariates=(1, 1)
)
model_1.fit(
target_series_1,
past_covariates=past_covariates,
future_covariates=future_covariates,
)
residuals_1 = model_1.residuals(
target_series_1,
past_covariates=past_covariates,
future_covariates=future_covariates,
)

model_2.fit(
target_series_2,
past_covariates=past_covariates,
future_covariates=future_covariates,
)
residuals_2 = model_2.residuals(
target_series_2,
past_covariates=past_covariates,
future_covariates=future_covariates,
)

# residuals zero
np.testing.assert_almost_equal(
residuals_1.univariate_values(), np.zeros(len(residuals_1))
)

np.testing.assert_almost_equal(
residuals_2.univariate_values(), np.zeros(len(residuals_2))
)

# if model is trained with covariates, should raise error when covariates are missing in residuals()
with self.assertRaises(ValueError):
model_1.residuals(target_series_1)

with self.assertRaises(ValueError):
model_1.residuals(target_series_1, past_covariates=past_covariates)

with self.assertRaises(ValueError):
model_1.residuals(target_series_1, future_covariates=future_covariates)