diff --git a/docs/source/tutorials/building.ipynb b/docs/source/tutorials/building.ipynb index 2910bf87f..3b37c048e 100644 --- a/docs/source/tutorials/building.ipynb +++ b/docs/source/tutorials/building.ipynb @@ -875,13 +875,13 @@ " assert dataset.max_prediction_length == dataset.min_prediction_length, \"Decoder only supports a fixed length\"\n", " assert dataset.min_encoder_length == dataset.max_encoder_length, \"Encoder only supports a fixed length\"\n", " assert (\n", - " len(dataset.time_varying_known_categoricals) == 0\n", - " and len(dataset.time_varying_known_reals) == 0\n", - " and len(dataset.time_varying_unknown_categoricals) == 0\n", - " and len(dataset.static_categoricals) == 0\n", - " and len(dataset.static_reals) == 0\n", - " and len(dataset.time_varying_unknown_reals) == 1\n", - " and dataset.time_varying_unknown_reals[0] == dataset.target\n", + " len(dataset._time_varying_known_categoricals) == 0\n", + " and len(dataset._time_varying_known_reals) == 0\n", + " and len(dataset._time_varying_unknown_categoricals) == 0\n", + " and len(dataset._static_categoricals) == 0\n", + " and len(dataset._static_reals) == 0\n", + " and len(dataset._time_varying_unknown_reals) == 1\n", + " and dataset._time_varying_unknown_reals[0] == dataset.target\n", " ), \"Only covariate should be the target in 'time_varying_unknown_reals'\"\n", "\n", " return super().from_dataset(dataset, **new_kwargs)" @@ -1587,12 +1587,12 @@ " assert dataset.max_prediction_length == dataset.min_prediction_length, \"Decoder only supports a fixed length\"\n", " assert dataset.min_encoder_length == dataset.max_encoder_length, \"Encoder only supports a fixed length\"\n", " assert (\n", - " len(dataset.time_varying_known_categoricals) == 0\n", - " and len(dataset.time_varying_known_reals) == 0\n", - " and len(dataset.time_varying_unknown_categoricals) == 0\n", - " and len(dataset.static_categoricals) == 0\n", - " and len(dataset.static_reals) == 0\n", - " and len(dataset.time_varying_unknown_reals) == 1\n", + " len(dataset._time_varying_known_categoricals) == 0\n", + " and len(dataset._time_varying_known_reals) == 0\n", + " and len(dataset._time_varying_unknown_categoricals) == 0\n", + " and len(dataset._static_categoricals) == 0\n", + " and len(dataset._static_reals) == 0\n", + " and len(dataset._time_varying_unknown_reals) == 1\n", " ), \"Only covariate should be in 'time_varying_unknown_reals'\"\n", "\n", " return super().from_dataset(dataset, **new_kwargs)\n", @@ -2136,12 +2136,12 @@ " assert dataset.max_prediction_length == dataset.min_prediction_length, \"Decoder only supports a fixed length\"\n", " assert dataset.min_encoder_length == dataset.max_encoder_length, \"Encoder only supports a fixed length\"\n", " assert (\n", - " len(dataset.time_varying_known_categoricals) == 0\n", - " and len(dataset.time_varying_known_reals) == 0\n", - " and len(dataset.time_varying_unknown_categoricals) == 0\n", - " and len(dataset.static_categoricals) == 0\n", - " and len(dataset.static_reals) == 0\n", - " and len(dataset.time_varying_unknown_reals)\n", + " len(dataset._time_varying_known_categoricals) == 0\n", + " and len(dataset._time_varying_known_reals) == 0\n", + " and len(dataset._time_varying_unknown_categoricals) == 0\n", + " and len(dataset._static_categoricals) == 0\n", + " and len(dataset._static_reals) == 0\n", + " and len(dataset._time_varying_unknown_reals)\n", " == len(dataset.target_names) # Expect as as many unknown reals as targets\n", " ), \"Only covariate should be in 'time_varying_unknown_reals'\"\n", "\n", @@ -3414,13 +3414,13 @@ " assert dataset.max_prediction_length == dataset.min_prediction_length, \"Decoder only supports a fixed length\"\n", " assert dataset.min_encoder_length == dataset.max_encoder_length, \"Encoder only supports a fixed length\"\n", " assert (\n", - " len(dataset.time_varying_known_categoricals) == 0\n", - " and len(dataset.time_varying_known_reals) == 0\n", - " and len(dataset.time_varying_unknown_categoricals) == 0\n", - " and len(dataset.static_categoricals) == 0\n", - " and len(dataset.static_reals) == 0\n", - " and len(dataset.time_varying_unknown_reals) == 1\n", - " and dataset.time_varying_unknown_reals[0] == dataset.target\n", + " len(dataset._time_varying_known_categoricals) == 0\n", + " and len(dataset._time_varying_known_reals) == 0\n", + " and len(dataset._time_varying_unknown_categoricals) == 0\n", + " and len(dataset._static_categoricals) == 0\n", + " and len(dataset._static_reals) == 0\n", + " and len(dataset._time_varying_unknown_reals) == 1\n", + " and dataset._time_varying_unknown_reals[0] == dataset.target\n", " ), \"Only covariate should be the target in 'time_varying_unknown_reals'\"\n", "\n", " return super().from_dataset(dataset, **new_kwargs)\n", diff --git a/pytorch_forecasting/data/encoders.py b/pytorch_forecasting/data/encoders.py index a0f0da111..2bcf521ca 100644 --- a/pytorch_forecasting/data/encoders.py +++ b/pytorch_forecasting/data/encoders.py @@ -2,11 +2,12 @@ Encoders for encoding categorical variables and scaling continuous data. """ -from typing import Any, Callable, Dict, Iterable, List, Tuple, Union +from typing import Any, Callable, Dict, Iterable, List, Tuple, Union, Optional import warnings import numpy as np import pandas as pd +from copy import deepcopy from sklearn.base import BaseEstimator, TransformerMixin import torch from torch.distributions import constraints @@ -396,7 +397,7 @@ def __init__( method: str = "standard", center: bool = True, transformation: Union[str, Tuple[Callable, Callable]] = None, - method_kwargs: Dict[str, Any] = {}, + method_kwargs: Optional[Dict[str, Any]] = None, ): """ Args: @@ -428,6 +429,7 @@ def __init__( self.center = center self.transformation = transformation self.method_kwargs = method_kwargs + self._method_kwargs = deepcopy(method_kwargs) if method_kwargs is not None else {} def get_parameters(self, *args, **kwargs) -> torch.Tensor: """ @@ -496,17 +498,17 @@ def _set_parameters( elif self.method == "robust": if isinstance(y_center, torch.Tensor): - self.center_ = y_center.quantile(self.method_kwargs.get("center", 0.5), dim=-1) - q_75 = y_scale.quantile(self.method_kwargs.get("upper", 0.75), dim=-1) - q_25 = y_scale.quantile(self.method_kwargs.get("lower", 0.25), dim=-1) + self.center_ = y_center.quantile(self._method_kwargs.get("center", 0.5), dim=-1) + q_75 = y_scale.quantile(self._method_kwargs.get("upper", 0.75), dim=-1) + q_25 = y_scale.quantile(self._method_kwargs.get("lower", 0.25), dim=-1) elif isinstance(y_center, np.ndarray): - self.center_ = np.percentile(y_center, self.method_kwargs.get("center", 0.5) * 100, axis=-1) - q_75 = np.percentile(y_scale, self.method_kwargs.get("upper", 0.75) * 100, axis=-1) - q_25 = np.percentile(y_scale, self.method_kwargs.get("lower", 0.25) * 100, axis=-1) + self.center_ = np.percentile(y_center, self._method_kwargs.get("center", 0.5) * 100, axis=-1) + q_75 = np.percentile(y_scale, self._method_kwargs.get("upper", 0.75) * 100, axis=-1) + q_25 = np.percentile(y_scale, self._method_kwargs.get("lower", 0.25) * 100, axis=-1) else: - self.center_ = np.percentile(y_center, self.method_kwargs.get("center", 0.5) * 100, axis=-1) - q_75 = np.percentile(y_scale, self.method_kwargs.get("upper", 0.75) * 100) - q_25 = np.percentile(y_scale, self.method_kwargs.get("lower", 0.25) * 100) + self.center_ = np.percentile(y_center, self._method_kwargs.get("center", 0.5) * 100, axis=-1) + q_75 = np.percentile(y_scale, self._method_kwargs.get("upper", 0.75) * 100) + q_25 = np.percentile(y_scale, self._method_kwargs.get("lower", 0.25) * 100) self.scale_ = (q_75 - q_25) / 2.0 + eps if not self.center and self.method != "identity": self.scale_ = self.center_ @@ -623,7 +625,7 @@ def __init__( center: bool = True, max_length: Union[int, List[int]] = None, transformation: Union[str, Tuple[Callable, Callable]] = None, - method_kwargs: Dict[str, Any] = {}, + method_kwargs: Dict[str, Any] = None, ): """ Initialize @@ -655,6 +657,7 @@ def __init__( should be defined if ``reverse`` is not the inverse of the forward transformation. ``inverse_torch`` can be defined to provide a torch distribution transform for inverse transformations. """ + method_kwargs = deepcopy(method_kwargs) if method_kwargs is not None else {} super().__init__(method=method, center=center, transformation=transformation, method_kwargs=method_kwargs) self.max_length = max_length @@ -726,11 +729,11 @@ class GroupNormalizer(TorchNormalizer): def __init__( self, method: str = "standard", - groups: List[str] = [], + groups: Optional[List[str]] = None, center: bool = True, scale_by_group: bool = False, - transformation: Union[str, Tuple[Callable, Callable]] = None, - method_kwargs: Dict[str, Any] = {}, + transformation: Optional[Union[str, Tuple[Callable, Callable]]] = None, + method_kwargs: Optional[Dict[str, Any]] = None, ): """ Group normalizer to normalize a given entry by groups. Can be used as target normalizer. @@ -765,7 +768,9 @@ def __init__( """ self.groups = groups + self._groups = list(groups) if groups is not None else [] self.scale_by_group = scale_by_group + method_kwargs = deepcopy(method_kwargs) if method_kwargs is not None else {} super().__init__(method=method, center=center, transformation=transformation, method_kwargs=method_kwargs) def fit(self, y: pd.Series, X: pd.DataFrame): @@ -781,7 +786,7 @@ def fit(self, y: pd.Series, X: pd.DataFrame): """ y = self.preprocess(y) eps = np.finfo(np.float16).eps - if len(self.groups) == 0: + if len(self._groups) == 0: assert not self.scale_by_group, "No groups are defined, i.e. `scale_by_group=[]`" if self.method == "standard": self.norm_ = {"center": np.mean(y), "scale": np.std(y) + eps} # center and scale @@ -789,9 +794,9 @@ def fit(self, y: pd.Series, X: pd.DataFrame): quantiles = np.quantile( y, [ - self.method_kwargs.get("lower", 0.25), - self.method_kwargs.get("center", 0.5), - self.method_kwargs.get("upper", 0.75), + self._method_kwargs.get("lower", 0.25), + self._method_kwargs.get("center", 0.5), + self._method_kwargs.get("upper", 0.75), ], ) self.norm_ = { @@ -810,7 +815,7 @@ def fit(self, y: pd.Series, X: pd.DataFrame): .groupby(g, observed=True) .agg(center=("y", "mean"), scale=("y", "std")) .assign(center=lambda x: x["center"], scale=lambda x: x.scale + eps) - for g in self.groups + for g in self._groups } else: self.norm_ = { @@ -819,21 +824,21 @@ def fit(self, y: pd.Series, X: pd.DataFrame): .groupby(g, observed=True) .y.quantile( [ - self.method_kwargs.get("lower", 0.25), - self.method_kwargs.get("center", 0.5), - self.method_kwargs.get("upper", 0.75), + self._method_kwargs.get("lower", 0.25), + self._method_kwargs.get("center", 0.5), + self._method_kwargs.get("upper", 0.75), ] ) .unstack(-1) .assign( - center=lambda x: x[self.method_kwargs.get("center", 0.5)], + center=lambda x: x[self._method_kwargs.get("center", 0.5)], scale=lambda x: ( - x[self.method_kwargs.get("upper", 0.75)] - x[self.method_kwargs.get("lower", 0.25)] + x[self._method_kwargs.get("upper", 0.75)] - x[self._method_kwargs.get("lower", 0.25)] ) / 2.0 + eps, )[["center", "scale"]] - for g in self.groups + for g in self._groups } # calculate missings if not self.center: # swap center and scale @@ -849,29 +854,29 @@ def swap_parameters(norm): else: if self.method == "standard": self.norm_ = ( - X[self.groups] + X[self._groups] .assign(y=y) - .groupby(self.groups, observed=True) + .groupby(self._groups, observed=True) .agg(center=("y", "mean"), scale=("y", "std")) .assign(center=lambda x: x["center"], scale=lambda x: x.scale + eps) ) else: self.norm_ = ( - X[self.groups] + X[self._groups] .assign(y=y) - .groupby(self.groups, observed=True) + .groupby(self._groups, observed=True) .y.quantile( [ - self.method_kwargs.get("lower", 0.25), - self.method_kwargs.get("center", 0.5), - self.method_kwargs.get("upper", 0.75), + self._method_kwargs.get("lower", 0.25), + self._method_kwargs.get("center", 0.5), + self._method_kwargs.get("upper", 0.75), ] ) .unstack(-1) .assign( - center=lambda x: x[self.method_kwargs.get("center", 0.5)], + center=lambda x: x[self._method_kwargs.get("center", 0.5)], scale=lambda x: ( - x[self.method_kwargs.get("upper", 0.75)] - x[self.method_kwargs.get("lower", 0.25)] + x[self._method_kwargs.get("upper", 0.75)] - x[self._method_kwargs.get("lower", 0.25)] ) / 2.0 + eps, @@ -883,7 +888,7 @@ def swap_parameters(norm): self.missing_ = self.norm_.median().to_dict() if ( - (self.scale_by_group and any((self.norm_[group]["scale"] < 1e-7).any() for group in self.groups)) + (self.scale_by_group and any((self.norm_[group]["scale"] < 1e-7).any() for group in self._groups)) or (not self.scale_by_group and isinstance(self.norm_["scale"], float) and self.norm_["scale"] < 1e-7) or ( not self.scale_by_group @@ -973,13 +978,13 @@ def get_parameters(self, groups: Union[torch.Tensor, list, tuple], group_names: if isinstance(groups, list): groups = tuple(groups) if group_names is None: - group_names = self.groups + group_names = self._groups else: # filter group names - group_names = [name for name in group_names if name in self.groups] - assert len(group_names) == len(self.groups), "Passed groups and fitted do not match" + group_names = [name for name in group_names if name in self._groups] + assert len(group_names) == len(self._groups), "Passed groups and fitted do not match" - if len(self.groups) == 0: + if len(self._groups) == 0: params = np.array([self.norm_["center"], self.norm_["scale"]]) elif self.scale_by_group: norm = np.array([1.0, 1.0]) @@ -988,7 +993,7 @@ def get_parameters(self, groups: Union[torch.Tensor, list, tuple], group_names: norm = norm * self.norm_[group_name].loc[group].to_numpy() except KeyError: norm = norm * np.asarray([self.missing_[group_name][name] for name in self.names]) - norm = np.power(norm, 1.0 / len(self.groups)) + norm = np.power(norm, 1.0 / len(self._groups)) params = norm else: try: @@ -1007,7 +1012,7 @@ def get_norm(self, X: pd.DataFrame) -> pd.DataFrame: Returns: pd.DataFrame: dataframe with scaling parameterswhere each row corresponds to the input dataframe """ - if len(self.groups) == 0: + if len(self._groups) == 0: norm = np.asarray([self.norm_["center"], self.norm_["scale"]]).reshape(1, -1) elif self.scale_by_group: norm = [ @@ -1017,15 +1022,15 @@ def get_norm(self, X: pd.DataFrame) -> pd.DataFrame: .map(self.norm_[group_name][name]) .fillna(self.missing_[group_name][name]) .to_numpy() - for group_name in self.groups + for group_name in self._groups ], axis=0, ) for name in self.names ] - norm = np.power(np.stack(norm, axis=1), 1.0 / len(self.groups)) + norm = np.power(np.stack(norm, axis=1), 1.0 / len(self._groups)) else: - norm = X[self.groups].set_index(self.groups).join(self.norm_).fillna(self.missing_).to_numpy() + norm = X[self._groups].set_index(self._groups).join(self.norm_).fillna(self.missing_).to_numpy() return norm diff --git a/pytorch_forecasting/data/timeseries.py b/pytorch_forecasting/data/timeseries.py index dd26ffb23..968e2d640 100644 --- a/pytorch_forecasting/data/timeseries.py +++ b/pytorch_forecasting/data/timeseries.py @@ -8,7 +8,7 @@ from copy import copy as _copy, deepcopy from functools import lru_cache import inspect -from typing import Any, Callable, Dict, List, Tuple, Union +from typing import Any, Callable, Dict, List, Tuple, Union, Optional import warnings import numpy as np @@ -185,22 +185,22 @@ def __init__( min_prediction_idx: int = None, min_prediction_length: int = None, max_prediction_length: int = 1, - static_categoricals: List[str] = [], - static_reals: List[str] = [], - time_varying_known_categoricals: List[str] = [], - time_varying_known_reals: List[str] = [], - time_varying_unknown_categoricals: List[str] = [], - time_varying_unknown_reals: List[str] = [], - variable_groups: Dict[str, List[int]] = {}, - constant_fill_strategy: Dict[str, Union[str, float, int, bool]] = {}, + static_categoricals: Optional[List[str]] = None, + static_reals: Optional[List[str]] = None, + time_varying_known_categoricals: Optional[List[str]] = None, + time_varying_known_reals: Optional[List[str]] = None, + time_varying_unknown_categoricals: Optional[List[str]] = None, + time_varying_unknown_reals: Optional[List[str]] = None, + variable_groups: Optional[Dict[str, List[int]]] = None, + constant_fill_strategy: Optional[Dict[str, Union[str, float, int, bool]]] = None, allow_missing_timesteps: bool = False, - lags: Dict[str, List[int]] = {}, + lags: Optional[Dict[str, List[int]]] = None, add_relative_time_idx: bool = False, add_target_scales: bool = False, add_encoder_length: Union[bool, str] = "auto", target_normalizer: Union[NORMALIZER, str, List[NORMALIZER], Tuple[NORMALIZER], None] = "auto", - categorical_encoders: Dict[str, NaNLabelEncoder] = {}, - scalers: Dict[str, Union[StandardScaler, RobustScaler, TorchNormalizer, EncoderNormalizer]] = {}, + categorical_encoders: Optional[Dict[str, NaNLabelEncoder]] = None, + scalers: Optional[Dict[str, Union[StandardScaler, RobustScaler, TorchNormalizer, EncoderNormalizer]]] = None, randomize_length: Union[None, Tuple[float, float], bool] = False, predict_mode: bool = False, ): @@ -352,13 +352,25 @@ def __init__( self.target = target self.weight = weight self.time_idx = time_idx - self.group_ids = [] + group_ids - self.static_categoricals = [] + static_categoricals - self.static_reals = [] + static_reals - self.time_varying_known_categoricals = [] + time_varying_known_categoricals - self.time_varying_known_reals = [] + time_varying_known_reals - self.time_varying_unknown_categoricals = [] + time_varying_unknown_categoricals - self.time_varying_unknown_reals = [] + time_varying_unknown_reals + self.group_ids = [] if group_ids is None else list(group_ids) + self.static_categoricals = static_categoricals + self._static_categoricals = [] if static_categoricals is None else list(static_categoricals) + self.static_reals = static_reals + self._static_reals = [] if static_reals is None else list(static_reals) + self.time_varying_known_categoricals = time_varying_known_categoricals + self._time_varying_known_categoricals = ( + [] if time_varying_known_categoricals is None else list(time_varying_known_categoricals) + ) + self.time_varying_known_reals = time_varying_known_reals + self._time_varying_known_reals = [] if time_varying_known_reals is None else list(time_varying_known_reals) + self.time_varying_unknown_categoricals = time_varying_unknown_categoricals + self._time_varying_unknown_categoricals = ( + [] if time_varying_unknown_categoricals is None else list(time_varying_unknown_categoricals) + ) + self.time_varying_unknown_reals = time_varying_unknown_reals + self._time_varying_unknown_reals = ( + [] if time_varying_unknown_reals is None else list(time_varying_unknown_reals) + ) self.add_relative_time_idx = add_relative_time_idx # set automatic defaults @@ -371,15 +383,20 @@ def __init__( if min_prediction_idx is None: min_prediction_idx = data[self.time_idx].min() self.min_prediction_idx = min_prediction_idx - self.constant_fill_strategy = {} if len(constant_fill_strategy) == 0 else constant_fill_strategy + self.constant_fill_strategy = constant_fill_strategy + self._constant_fill_strategy = {} if constant_fill_strategy is None else deepcopy(constant_fill_strategy) self.predict_mode = predict_mode self.allow_missing_timesteps = allow_missing_timesteps self.target_normalizer = target_normalizer - self.categorical_encoders = {} if len(categorical_encoders) == 0 else categorical_encoders - self.scalers = {} if len(scalers) == 0 else scalers + self.categorical_encoders = categorical_encoders + self._categorical_encoders = {} if categorical_encoders is None else deepcopy(categorical_encoders) + self.scalers = scalers + self._scalers = {} if scalers is None else deepcopy(scalers) self.add_target_scales = add_target_scales - self.variable_groups = {} if len(variable_groups) == 0 else variable_groups - self.lags = {} if len(lags) == 0 else lags + self.variable_groups = variable_groups + self._variable_groups = {} if variable_groups is None else deepcopy(variable_groups) + self.lags = lags + self._lags = {} if lags is None else deepcopy(lags) # add_encoder_length if isinstance(add_encoder_length, str): @@ -400,7 +417,7 @@ def __init__( for target in self.target_names: assert ( - target not in self.time_varying_known_reals + target not in self._time_varying_known_reals ), f"target {target} should be an unknown continuous variable in the future" # add time index relative to prediction position @@ -410,18 +427,18 @@ def __init__( assert ( "relative_time_idx" not in data.columns ), "relative_time_idx is a protected column and must not be present in data" - if "relative_time_idx" not in self.time_varying_known_reals and "relative_time_idx" not in self.reals: - self.time_varying_known_reals.append("relative_time_idx") - data.loc[:, "relative_time_idx"] = 0.0 # dummy - real value will be set dynamiclly in __getitem__() + if "relative_time_idx" not in self._time_varying_known_reals and "relative_time_idx" not in self.reals: + self._time_varying_known_reals.append("relative_time_idx") + data.loc[:, "relative_time_idx"] = 0.0 # dummy - real value will be set dynamically in __getitem__() # add decoder length to static real variables if self.add_encoder_length: assert ( "encoder_length" not in data.columns ), "encoder_length is a protected column and must not be present in data" - if "encoder_length" not in self.time_varying_known_reals and "encoder_length" not in self.reals: - self.static_reals.append("encoder_length") - data.loc[:, "encoder_length"] = 0 # dummy - real value will be set dynamiclly in __getitem__() + if "encoder_length" not in self._time_varying_known_reals and "encoder_length" not in self.reals: + self._static_reals.append("encoder_length") + data.loc[:, "encoder_length"] = 0 # dummy - real value will be set dynamically in __getitem__() # validate self._validate_data(data) @@ -429,40 +446,40 @@ def __init__( # add lags assert self.min_lag > 0, "lags should be positive" - if len(self.lags) > 0: + if len(self._lags) > 0: # add variables - for name in self.lags: + for name in self._lags: lagged_names = self._get_lagged_names(name) for lagged_name in lagged_names: assert ( lagged_name not in data.columns ), f"{lagged_name} is a protected column and must not be present in data" # add lags - if name in self.time_varying_known_reals: + if name in self._time_varying_known_reals: for lagged_name in lagged_names: - if lagged_name not in self.time_varying_known_reals: - self.time_varying_known_reals.append(lagged_name) - elif name in self.time_varying_known_categoricals: + if lagged_name not in self._time_varying_known_reals: + self._time_varying_known_reals.append(lagged_name) + elif name in self._time_varying_known_categoricals: for lagged_name in lagged_names: - if lagged_name not in self.time_varying_known_categoricals: - self.time_varying_known_categoricals.append(lagged_name) - elif name in self.time_varying_unknown_reals: + if lagged_name not in self._time_varying_known_categoricals: + self._time_varying_known_categoricals.append(lagged_name) + elif name in self._time_varying_unknown_reals: for lagged_name, lag in lagged_names.items(): if lag < self.max_prediction_length: # keep in unknown as if lag is too small - if lagged_name not in self.time_varying_unknown_reals: - self.time_varying_unknown_reals.append(lagged_name) + if lagged_name not in self._time_varying_unknown_reals: + self._time_varying_unknown_reals.append(lagged_name) else: - if lagged_name not in self.time_varying_known_reals: + if lagged_name not in self._time_varying_known_reals: # switch to known so that lag can be used in decoder directly - self.time_varying_known_reals.append(lagged_name) - elif name in self.time_varying_unknown_categoricals: + self._time_varying_known_reals.append(lagged_name) + elif name in self._time_varying_unknown_categoricals: for lagged_name, lag in lagged_names.items(): if lag < self.max_prediction_length: # keep in unknown as if lag is too small - if lagged_name not in self.time_varying_unknown_categoricals: - self.time_varying_unknown_categoricals.append(lagged_name) - if lagged_name not in self.time_varying_known_categoricals: + if lagged_name not in self._time_varying_unknown_categoricals: + self._time_varying_unknown_categoricals.append(lagged_name) + if lagged_name not in self._time_varying_known_categoricals: # switch to known so that lag can be used in decoder directly - self.time_varying_known_categoricals.append(lagged_name) + self._time_varying_known_categoricals.append(lagged_name) else: raise KeyError(f"lagged variable {name} is not a known nor unknown time-varying variable") @@ -476,7 +493,7 @@ def __init__( # preprocess data data = self._preprocess_data(data) for target in self.target_names: - assert target not in self.scalers, "Target normalizer is separate and not in scalers." + assert target not in self._scalers, "Target normalizer is separate and not in scalers." # create index self.index = self._construct_index(data, predict_mode=self.predict_mode) @@ -490,7 +507,7 @@ def dropout_categoricals(self) -> List[str]: list of categorical variables that are unknown when making a forecast without observed history """ - return [name for name, encoder in self.categorical_encoders.items() if encoder.add_nan] + return [name for name, encoder in self._categorical_encoders.items() if encoder.add_nan] def _get_lagged_names(self, name: str) -> Dict[str, int]: """ @@ -502,7 +519,7 @@ def _get_lagged_names(self, name: str) -> Dict[str, int]: Returns: Dict[str, int]: dictionary mapping new variable names to lags """ - return {f"{name}_lagged_by_{lag}": lag for lag in self.lags.get(name, [])} + return {f"{name}_lagged_by_{lag}": lag for lag in self._lags.get(name, [])} @property @lru_cache(None) @@ -515,7 +532,7 @@ def lagged_variables(self) -> Dict[str, str]: mapped to variable that is lagged """ vars = {} - for name in self.lags: + for name in self._lags: vars.update({lag_name: name for lag_name in self._get_lagged_names(name)}) return vars @@ -524,7 +541,7 @@ def lagged_variables(self) -> Dict[str, str]: def lagged_targets(self) -> Dict[str, str]: """Subset of `lagged_variables` but only includes variables that are lagged targets.""" vars = {} - for name in self.lags: + for name in self._lags: vars.update({lag_name: name for lag_name in self._get_lagged_names(name) if name in self.target_names}) return vars @@ -537,10 +554,10 @@ def min_lag(self) -> int: Returns: int: minimum lag """ - if len(self.lags) == 0: + if len(self._lags) == 0: return 1e9 else: - return min([min(lag) for lag in self.lags.values()]) + return min([min(lag) for lag in self._lags.values()]) @property @lru_cache(None) @@ -551,10 +568,10 @@ def max_lag(self) -> int: Returns: int: maximum lag """ - if len(self.lags) == 0: + if len(self._lags) == 0: return 0 else: - return max([max(lag) for lag in self.lags.values()]) + return max([max(lag) for lag in self._lags.values()]) def _set_target_normalizer(self, data: pd.DataFrame): """ @@ -682,19 +699,19 @@ def _preprocess_data(self, data: pd.DataFrame) -> pd.DataFrame: pd.DataFrame: pre-processed dataframe """ # add lags to data - for name in self.lags: + for name in self._lags: # todo: add support for variable groups assert ( - name not in self.variable_groups - ), f"lagged variables that are in {self.variable_groups} are not supported yet" + name not in self._variable_groups + ), f"lagged variables that are in {self._variable_groups} are not supported yet" for lagged_name, lag in self._get_lagged_names(name).items(): data[lagged_name] = data.groupby(self.group_ids, observed=True)[name].shift(lag) # encode group ids - this encoding for name, group_name in self._group_ids_mapping.items(): # use existing encoder - but a copy of it not too loose current encodings - encoder = deepcopy(self.categorical_encoders.get(group_name, NaNLabelEncoder())) - self.categorical_encoders[group_name] = encoder.fit(data[name].to_numpy().reshape(-1), overwrite=False) + encoder = deepcopy(self._categorical_encoders.get(group_name, NaNLabelEncoder())) + self._categorical_encoders[group_name] = encoder.fit(data[name].to_numpy().reshape(-1), overwrite=False) data[group_name] = self.transform_values(name, data[name], inverse=False, group_id=True) # encode categoricals first to ensure that group normalizer for relies on encoded categories @@ -707,25 +724,25 @@ def _preprocess_data(self, data: pd.DataFrame) -> pd.DataFrame: for name in dict.fromkeys(group_ids_to_encode + self.categoricals): if name in self.lagged_variables: continue # do not encode here but only in transform - if name in self.variable_groups: # fit groups - columns = self.variable_groups[name] - if name not in self.categorical_encoders: - self.categorical_encoders[name] = NaNLabelEncoder().fit(data[columns].to_numpy().reshape(-1)) - elif self.categorical_encoders[name] is not None: + if name in self._variable_groups: # fit groups + columns = self._variable_groups[name] + if name not in self._categorical_encoders: + self._categorical_encoders[name] = NaNLabelEncoder().fit(data[columns].to_numpy().reshape(-1)) + elif self._categorical_encoders[name] is not None: try: - check_is_fitted(self.categorical_encoders[name]) + check_is_fitted(self._categorical_encoders[name]) except NotFittedError: - self.categorical_encoders[name] = self.categorical_encoders[name].fit( + self._categorical_encoders[name] = self._categorical_encoders[name].fit( data[columns].to_numpy().reshape(-1) ) else: - if name not in self.categorical_encoders: - self.categorical_encoders[name] = NaNLabelEncoder().fit(data[name]) - elif self.categorical_encoders[name] is not None and name not in self.target_names: + if name not in self._categorical_encoders: + self._categorical_encoders[name] = NaNLabelEncoder().fit(data[name]) + elif self._categorical_encoders[name] is not None and name not in self.target_names: try: - check_is_fitted(self.categorical_encoders[name]) + check_is_fitted(self._categorical_encoders[name]) except NotFittedError: - self.categorical_encoders[name] = self.categorical_encoders[name].fit(data[name]) + self._categorical_encoders[name] = self._categorical_encoders[name].fit(data[name]) # encode them for name in dict.fromkeys(group_ids_to_encode + self.flat_categoricals): @@ -808,23 +825,23 @@ def _preprocess_data(self, data: pd.DataFrame) -> pd.DataFrame: ), f"{feature_name} is a protected column and must not be present in data" data[feature_name] = scales[target_idx][:, scale_idx].squeeze() if feature_name not in self.reals: - self.static_reals.append(feature_name) + self._static_reals.append(feature_name) # rescale continuous variables apart from target for name in self.reals: if name in self.target_names or name in self.lagged_variables: # lagged variables are only transformed - not fitted continue - elif name not in self.scalers: - self.scalers[name] = StandardScaler().fit(data[[name]]) - elif self.scalers[name] is not None: + elif name not in self._scalers: + self._scalers[name] = StandardScaler().fit(data[[name]]) + elif self._scalers[name] is not None: try: - check_is_fitted(self.scalers[name]) + check_is_fitted(self._scalers[name]) except NotFittedError: - if isinstance(self.scalers[name], GroupNormalizer): - self.scalers[name] = self.scalers[name].fit(data[name], data) + if isinstance(self._scalers[name], GroupNormalizer): + self._scalers[name] = self._scalers[name].fit(data[name], data) else: - self.scalers[name] = self.scalers[name].fit(data[[name]]) + self._scalers[name] = self._scalers[name].fit(data[[name]]) # encode after fitting for name in self.reals: @@ -845,7 +862,7 @@ def _preprocess_data(self, data: pd.DataFrame) -> pd.DataFrame: # encode constant values self.encoded_constant_fill_strategy = {} - for name, value in self.constant_fill_strategy.items(): + for name, value in self._constant_fill_strategy.items(): if name in self.target_names: self.encoded_constant_fill_strategy[f"__target__{name}"] = value self.encoded_constant_fill_strategy[name] = self.transform_values( @@ -883,7 +900,7 @@ def get_transformer(self, name: str, group_id: bool = False): if name in self.target_names: transformer = self.target_normalizers[self.target_names.index(name)] else: - transformer = self.categorical_encoders.get(name, None) + transformer = self._categorical_encoders.get(name, None) return transformer elif name in self.reals: @@ -891,7 +908,7 @@ def get_transformer(self, name: str, group_id: bool = False): if name in self.target_names: transformer = self.target_normalizers[self.target_names.index(name)] else: - transformer = self.scalers.get(name, None) + transformer = self._scalers.get(name, None) return transformer else: return None @@ -1036,7 +1053,9 @@ def categoricals(self) -> List[str]: Returns: List[str]: list of variables """ - return self.static_categoricals + self.time_varying_known_categoricals + self.time_varying_unknown_categoricals + return ( + self._static_categoricals + self._time_varying_known_categoricals + self._time_varying_unknown_categoricals + ) @property def flat_categoricals(self) -> List[str]: @@ -1048,8 +1067,8 @@ def flat_categoricals(self) -> List[str]: """ categories = [] for name in self.categoricals: - if name in self.variable_groups: - categories.extend(self.variable_groups[name]) + if name in self._variable_groups: + categories.extend(self._variable_groups[name]) else: categories.append(name) return categories @@ -1063,7 +1082,7 @@ def variable_to_group_mapping(self) -> Dict[str, str]: Dict[str, str]: dictionary mapping from :py:meth:`~categorical` to :py:meth:`~flat_categoricals`. """ groups = {} - for group_name, sublist in self.variable_groups.items(): + for group_name, sublist in self._variable_groups.items(): groups.update({name: group_name for name in sublist}) return groups @@ -1075,7 +1094,7 @@ def reals(self) -> List[str]: Returns: List[str]: list of variables """ - return self.static_reals + self.time_varying_known_reals + self.time_varying_unknown_reals + return self._static_reals + self._time_varying_known_reals + self._time_varying_unknown_reals @property @lru_cache(None) @@ -1127,8 +1146,8 @@ def get_parameters(self) -> Dict[str, Any]: for name in inspect.signature(self.__class__.__init__).parameters.keys() if name not in ["data", "self"] } - kwargs["categorical_encoders"] = self.categorical_encoders - kwargs["scalers"] = self.scalers + kwargs["categorical_encoders"] = self._categorical_encoders + kwargs["scalers"] = self._scalers return kwargs @classmethod @@ -1417,14 +1436,14 @@ def set_overwrite_values( "encoder", ], f"target has be one of 'all', 'decoder' or 'encoder' but target={target} instead" - if variable in self.static_categoricals or variable in self.static_categoricals: + if variable in self._static_categoricals or variable in self._static_categoricals: target = "all" if variable in self.target_names: raise NotImplementedError("Target variable is not supported") if self.weight is not None and self.weight == variable: raise NotImplementedError("Weight variable is not supported") - if isinstance(self.scalers.get(variable, self.categorical_encoders.get(variable)), TorchNormalizer): + if isinstance(self._scalers.get(variable, self._categorical_encoders.get(variable)), TorchNormalizer): raise NotImplementedError("TorchNormalizer (e.g. GroupNormalizer) is not supported") if self._overwrite_values is None: diff --git a/pytorch_forecasting/metrics/base_metrics.py b/pytorch_forecasting/metrics/base_metrics.py index b1ba82a45..43c84998e 100644 --- a/pytorch_forecasting/metrics/base_metrics.py +++ b/pytorch_forecasting/metrics/base_metrics.py @@ -503,23 +503,30 @@ class CompositeMetric(LightningMetric): higher_is_better = False is_differentiable = True - def __init__(self, metrics: List[LightningMetric] = [], weights: List[float] = None): + def __init__(self, metrics: Optional[List[LightningMetric]] = None, weights: Optional[List[float]] = None): """ Args: - metrics (List[LightningMetric], optional): list of metrics to combine. Defaults to []. + metrics (List[LightningMetric], optional): list of metrics to combine. Defaults to None. weights (List[float], optional): list of weights / multipliers for weights. Defaults to 1.0 for all metrics. """ + self.metrics = metrics + self.weights = weights + + if metrics is None: + metrics = [] if weights is None: weights = [1.0 for _ in metrics] assert len(weights) == len(metrics), "Number of weights has to match number of metrics" - self.metrics = metrics - self.weights = weights + self._metrics = list(metrics) + self._weights = list(weights) super().__init__() def __repr__(self): - name = " + ".join([f"{w:.3g} * {repr(m)}" if w != 1.0 else repr(m) for w, m in zip(self.weights, self.metrics)]) + name = " + ".join( + [f"{w:.3g} * {repr(m)}" if w != 1.0 else repr(m) for w, m in zip(self._weights, self._metrics)] + ) return name def update(self, y_pred: torch.Tensor, y_actual: torch.Tensor, **kwargs): @@ -533,7 +540,7 @@ def update(self, y_pred: torch.Tensor, y_actual: torch.Tensor, **kwargs): Returns: torch.Tensor: metric value on which backpropagation can be applied """ - for metric in self.metrics: + for metric in self._metrics: try: metric.update(y_pred, y_actual, **kwargs) except TypeError: @@ -547,7 +554,7 @@ def compute(self) -> torch.Tensor: torch.Tensor: metric """ results = [] - for weight, metric in zip(self.weights, self.metrics): + for weight, metric in zip(self._weights, self._metrics): results.append(metric.compute() * weight) if len(results) == 1: @@ -570,7 +577,7 @@ def forward(self, y_pred: torch.Tensor, y_actual: torch.Tensor, **kwargs): torch.Tensor: metric value on which backpropagation can be applied """ results = [] - for weight, metric in zip(self.weights, self.metrics): + for weight, metric in zip(self._weights, self._metrics): try: results.append(metric(y_pred, y_actual, **kwargs) * weight) except TypeError: @@ -590,11 +597,11 @@ def _sync_dist(self, dist_sync_fn: Optional[Callable] = None, process_group: Opt pass def reset(self) -> None: - for metric in self.metrics: + for metric in self._metrics: metric.reset() def persistent(self, mode: bool = False) -> None: - for metric in self.metrics: + for metric in self._metrics: metric.persistent(mode=mode) def to_prediction(self, y_pred: torch.Tensor, **kwargs) -> torch.Tensor: @@ -610,7 +617,7 @@ def to_prediction(self, y_pred: torch.Tensor, **kwargs) -> torch.Tensor: Returns: torch.Tensor: point prediction """ - return self.metrics[0].to_prediction(y_pred, **kwargs) + return self._metrics[0].to_prediction(y_pred, **kwargs) def to_quantiles(self, y_pred: torch.Tensor, **kwargs) -> torch.Tensor: """ @@ -625,20 +632,20 @@ def to_quantiles(self, y_pred: torch.Tensor, **kwargs) -> torch.Tensor: Returns: torch.Tensor: prediction quantiles """ - return self.metrics[0].to_quantiles(y_pred, **kwargs) + return self._metrics[0].to_quantiles(y_pred, **kwargs) def __add__(self, metric: LightningMetric): if isinstance(metric, self.__class__): - self.metrics.extend(metric.metrics) - self.weights.extend(metric.weights) + self._metrics.extend(metric._metrics) + self._weights.extend(metric._weights) else: - self.metrics.append(metric) - self.weights.append(1.0) + self._metrics.append(metric) + self._weights.append(1.0) return self def __mul__(self, multiplier: float): - self.weights = [w * multiplier for w in self.weights] + self._weights = [w * multiplier for w in self._weights] return self __rmul__ = __mul__ @@ -897,9 +904,7 @@ class DistributionLoss(MultiHorizonMetric): distribution_class: distributions.Distribution distribution_arguments: List[str] - def __init__( - self, name: str = None, quantiles: List[float] = [0.02, 0.1, 0.25, 0.5, 0.75, 0.9, 0.98], reduction="mean" - ): + def __init__(self, name: str = None, quantiles: Optional[List[float]] = None, reduction="mean"): """ Initialize metric @@ -909,6 +914,8 @@ def __init__( Defaults to [0.02, 0.1, 0.25, 0.5, 0.75, 0.9, 0.98]. reduction (str, optional): Reduction, "none", "mean" or "sqrt-mean". Defaults to "mean". """ + if quantiles is None: + quantiles = [0.02, 0.1, 0.25, 0.5, 0.75, 0.9, 0.98] super().__init__(name=name, quantiles=quantiles, reduction=reduction) def map_x_to_distribution(self, x: torch.Tensor) -> distributions.Distribution: @@ -945,7 +952,7 @@ def to_prediction(self, y_pred: torch.Tensor, n_samples: int = 100) -> torch.Ten Args: y_pred: prediction output of network - + n_samples (int): number of samples to draw Returns: torch.Tensor: mean prediction """ diff --git a/pytorch_forecasting/metrics/distributions.py b/pytorch_forecasting/metrics/distributions.py index 02e7f987d..30c3db558 100644 --- a/pytorch_forecasting/metrics/distributions.py +++ b/pytorch_forecasting/metrics/distributions.py @@ -53,7 +53,7 @@ class MultivariateNormalDistributionLoss(MultivariateDistributionLoss): def __init__( self, name: str = None, - quantiles: List[float] = [0.02, 0.1, 0.25, 0.5, 0.75, 0.9, 0.98], + quantiles: Optional[List[float]] = None, reduction: str = "mean", rank: int = 10, sigma_init: float = 1.0, @@ -71,6 +71,8 @@ def __init__( sigma_init (float, optional): default value for diagonal covariance. Defaults to 1.0. sigma_minimum (float, optional): minimum value for diagonal covariance. Defaults to 1e-3. """ + if quantiles is None: + quantiles = [0.02, 0.1, 0.25, 0.5, 0.75, 0.9, 0.98] super().__init__(name=name, quantiles=quantiles, reduction=reduction) self.rank = rank self.sigma_minimum = sigma_minimum @@ -263,7 +265,7 @@ class MQF2DistributionLoss(DistributionLoss): def __init__( self, prediction_length: int, - quantiles: List[float] = [0.02, 0.1, 0.25, 0.5, 0.75, 0.9, 0.98], + quantiles: Optional[List[float]] = None, hidden_size: Optional[int] = 4, es_num_samples: int = 50, beta: float = 1.0, @@ -286,6 +288,8 @@ def __init__( icnn_num_layers (int, optional): number of hidden layers in distribution estimating network. Defaults to 2. estimate_logdet (bool, optional): if to estimate log determinant. Defaults to False. """ + if quantiles is None: + quantiles = [0.02, 0.1, 0.25, 0.5, 0.75, 0.9, 0.98] super().__init__(quantiles=quantiles) from cpflows.flows import ActNorm @@ -445,7 +449,7 @@ class ImplicitQuantileNetworkDistributionLoss(DistributionLoss): def __init__( self, - quantiles: List[float] = [0.02, 0.1, 0.25, 0.5, 0.75, 0.9, 0.98], + quantiles: Optional[List[float]] = None, input_size: Optional[int] = 16, hidden_size: Optional[int] = 32, n_loss_samples: Optional[int] = 64, @@ -459,6 +463,8 @@ def __init__( hidden_size (int, optional): hidden size per prediction length. Defaults to 64. n_loss_samples (int, optional): number of quantiles to sample to calculate loss. """ + if quantiles is None: + quantiles = [0.02, 0.1, 0.25, 0.5, 0.75, 0.9, 0.98] super().__init__(quantiles=quantiles) self.quantile_network = ImplicitQuantileNetwork(input_size=input_size, hidden_size=hidden_size) self.distribution_arguments = list(range(int(input_size))) diff --git a/pytorch_forecasting/metrics/point.py b/pytorch_forecasting/metrics/point.py index e05c6074a..753d309e5 100644 --- a/pytorch_forecasting/metrics/point.py +++ b/pytorch_forecasting/metrics/point.py @@ -211,7 +211,8 @@ def update( def loss(self, y_pred, target, scaling): return (self.to_prediction(y_pred) - target).abs() / scaling.unsqueeze(-1) - def calculate_scaling(self, target, lengths, encoder_target, encoder_lengths): + @staticmethod + def calculate_scaling(target, lengths, encoder_target, encoder_lengths): # calcualte mean(abs(diff(targets))) eps = 1e-6 batch_size = target.size(0) diff --git a/pytorch_forecasting/metrics/quantile.py b/pytorch_forecasting/metrics/quantile.py index 3c550b3a3..ad9f9921c 100644 --- a/pytorch_forecasting/metrics/quantile.py +++ b/pytorch_forecasting/metrics/quantile.py @@ -1,6 +1,6 @@ """Quantile metrics for forecasting multiple quantiles per time step.""" -from typing import List +from typing import List, Optional import torch @@ -16,7 +16,7 @@ class QuantileLoss(MultiHorizonMetric): def __init__( self, - quantiles: List[float] = [0.02, 0.1, 0.25, 0.5, 0.75, 0.9, 0.98], + quantiles: Optional[List[float]] = None, **kwargs, ): """ @@ -25,6 +25,8 @@ def __init__( Args: quantiles: quantiles for metric """ + if quantiles is None: + quantiles = [0.02, 0.1, 0.25, 0.5, 0.75, 0.9, 0.98] super().__init__(quantiles=quantiles, **kwargs) def loss(self, y_pred: torch.Tensor, target: torch.Tensor) -> torch.Tensor: diff --git a/pytorch_forecasting/models/base_model.py b/pytorch_forecasting/models/base_model.py index 786f1f44a..77d45197b 100644 --- a/pytorch_forecasting/models/base_model.py +++ b/pytorch_forecasting/models/base_model.py @@ -407,7 +407,7 @@ def __init__( reduce_on_plateau_min_lr: float = 1e-5, weight_decay: float = 0.0, optimizer_params: Dict[str, Any] = None, - monotone_constaints: Dict[str, int] = {}, + monotone_constaints: Dict[str, int] = None, output_transformer: Callable = None, optimizer=None, ): @@ -446,6 +446,8 @@ def __init__( `"ranger" `_, if pytorch_optimizer is installed, otherwise "adam". """ + if monotone_constaints is None: + monotone_constaints = {} super().__init__() # update hparams frame = inspect.currentframe() @@ -690,8 +692,8 @@ def create_log( y: Tuple[torch.Tensor, torch.Tensor], out: Dict[str, torch.Tensor], batch_idx: int, - prediction_kwargs: Dict[str, Any] = {}, - quantiles_kwargs: Dict[str, Any] = {}, + prediction_kwargs: Optional[Dict[str, Any]] = None, + quantiles_kwargs: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: """ Create the log used in the training and validation step. @@ -709,6 +711,9 @@ def create_log( Returns: Dict[str, Any]: log dictionary to be returned by training and validation steps """ + + prediction_kwargs = {} if prediction_kwargs is None else deepcopy(prediction_kwargs) + quantiles_kwargs = {} if quantiles_kwargs is None else deepcopy(quantiles_kwargs) # log if isinstance(self.loss, DistributionLoss): prediction_kwargs.setdefault("n_samples", 20) @@ -1005,8 +1010,8 @@ def plot_prediction( add_loss_to_title: Union[Metric, torch.Tensor, bool] = False, show_future_observed: bool = True, ax=None, - quantiles_kwargs: Dict[str, Any] = {}, - prediction_kwargs: Dict[str, Any] = {}, + quantiles_kwargs: Optional[Dict[str, Any]] = None, + prediction_kwargs: Optional[Dict[str, Any]] = None, ): """ Plot prediction of prediction vs actuals @@ -1026,6 +1031,11 @@ def plot_prediction( Returns: matplotlib figure """ + if quantiles_kwargs is None: + quantiles_kwargs = {} + if prediction_kwargs is None: + prediction_kwargs = {} + _check_matplotlib("plot_prediction") from matplotlib import pyplot as plt @@ -1700,43 +1710,45 @@ def from_dataset( # assert fixed encoder and decoder length for the moment if allowed_encoder_known_variable_names is None: allowed_encoder_known_variable_names = ( - dataset.time_varying_known_categoricals + dataset.time_varying_known_reals + dataset._time_varying_known_categoricals + dataset._time_varying_known_reals ) # embeddings embedding_labels = { name: encoder.classes_ - for name, encoder in dataset.categorical_encoders.items() + for name, encoder in dataset._categorical_encoders.items() if name in dataset.categoricals } embedding_paddings = dataset.dropout_categoricals # determine embedding sizes based on heuristic embedding_sizes = { name: (len(encoder.classes_), get_embedding_size(len(encoder.classes_))) - for name, encoder in dataset.categorical_encoders.items() + for name, encoder in dataset._categorical_encoders.items() if name in dataset.categoricals } embedding_sizes.update(kwargs.get("embedding_sizes", {})) kwargs.setdefault("embedding_sizes", embedding_sizes) new_kwargs = dict( - static_categoricals=dataset.static_categoricals, + static_categoricals=dataset._static_categoricals, time_varying_categoricals_encoder=[ - name for name in dataset.time_varying_known_categoricals if name in allowed_encoder_known_variable_names + name + for name in dataset._time_varying_known_categoricals + if name in allowed_encoder_known_variable_names ] - + dataset.time_varying_unknown_categoricals, - time_varying_categoricals_decoder=dataset.time_varying_known_categoricals, - static_reals=dataset.static_reals, + + dataset._time_varying_unknown_categoricals, + time_varying_categoricals_decoder=dataset._time_varying_known_categoricals, + static_reals=dataset._static_reals, time_varying_reals_encoder=[ - name for name in dataset.time_varying_known_reals if name in allowed_encoder_known_variable_names + name for name in dataset._time_varying_known_reals if name in allowed_encoder_known_variable_names ] - + dataset.time_varying_unknown_reals, - time_varying_reals_decoder=dataset.time_varying_known_reals, + + dataset._time_varying_unknown_reals, + time_varying_reals_decoder=dataset._time_varying_known_reals, x_reals=dataset.reals, x_categoricals=dataset.flat_categoricals, embedding_labels=embedding_labels, embedding_paddings=embedding_paddings, - categorical_groups=dataset.variable_groups, + categorical_groups=dataset._variable_groups, ) new_kwargs.update(kwargs) return super().from_dataset(dataset, **new_kwargs) @@ -2059,7 +2071,7 @@ def from_dataset( """ kwargs.setdefault("target", dataset.target) # check that lags for targets are the same - lags = {name: lag for name, lag in dataset.lags.items() if name in dataset.target_names} # filter for targets + lags = {name: lag for name, lag in dataset._lags.items() if name in dataset.target_names} # filter for targets target0 = dataset.target_names[0] lag = set(lags.get(target0, [])) for target in dataset.target_names: @@ -2293,8 +2305,8 @@ def plot_prediction( add_loss_to_title: Union[Metric, torch.Tensor, bool] = False, show_future_observed: bool = True, ax=None, - quantiles_kwargs: Dict[str, Any] = {}, - prediction_kwargs: Dict[str, Any] = {}, + quantiles_kwargs: Optional[Dict[str, Any]] = None, + prediction_kwargs: Optional[Dict[str, Any]] = None, ): """ Plot prediction of prediction vs actuals @@ -2315,6 +2327,9 @@ def plot_prediction( matplotlib figure """ + prediction_kwargs = {} if prediction_kwargs is None else deepcopy(prediction_kwargs) + quantiles_kwargs = {} if quantiles_kwargs is None else deepcopy(quantiles_kwargs) + # get predictions if isinstance(self.loss, DistributionLoss): prediction_kwargs.setdefault("use_metric", False) diff --git a/pytorch_forecasting/models/deepar/__init__.py b/pytorch_forecasting/models/deepar/__init__.py index 04f0c1c12..f9bcb186b 100644 --- a/pytorch_forecasting/models/deepar/__init__.py +++ b/pytorch_forecasting/models/deepar/__init__.py @@ -38,22 +38,22 @@ def __init__( hidden_size: int = 10, rnn_layers: int = 2, dropout: float = 0.1, - static_categoricals: List[str] = [], - static_reals: List[str] = [], - time_varying_categoricals_encoder: List[str] = [], - time_varying_categoricals_decoder: List[str] = [], - categorical_groups: Dict[str, List[str]] = {}, - time_varying_reals_encoder: List[str] = [], - time_varying_reals_decoder: List[str] = [], - embedding_sizes: Dict[str, Tuple[int, int]] = {}, - embedding_paddings: List[str] = [], - embedding_labels: Dict[str, np.ndarray] = {}, - x_reals: List[str] = [], - x_categoricals: List[str] = [], + static_categoricals: Optional[List[str]] = None, + static_reals: Optional[List[str]] = None, + time_varying_categoricals_encoder: Optional[List[str]] = None, + time_varying_categoricals_decoder: Optional[List[str]] = None, + categorical_groups: Optional[Dict[str, List[str]]] = None, + time_varying_reals_encoder: Optional[List[str]] = None, + time_varying_reals_decoder: Optional[List[str]] = None, + embedding_sizes: Optional[Dict[str, Tuple[int, int]]] = None, + embedding_paddings: Optional[List[str]] = None, + embedding_labels: Optional[Dict[str, np.ndarray]] = None, + x_reals: Optional[List[str]] = None, + x_categoricals: Optional[List[str]] = None, n_validation_samples: int = None, n_plotting_samples: int = None, target: Union[str, List[str]] = None, - target_lags: Dict[str, List[int]] = {}, + target_lags: Optional[Dict[str, List[int]]] = None, loss: DistributionLoss = None, logging_metrics: nn.ModuleList = None, **kwargs, @@ -115,6 +115,32 @@ def __init__( n_plotting_samples = n_validation_samples else: n_plotting_samples = 100 + if static_categoricals is None: + static_categoricals = [] + if static_reals is None: + static_reals = [] + if time_varying_categoricals_encoder is None: + time_varying_categoricals_encoder = [] + if time_varying_categoricals_decoder is None: + time_varying_categoricals_decoder = [] + if categorical_groups is None: + categorical_groups = {} + if time_varying_reals_encoder is None: + time_varying_reals_encoder = [] + if time_varying_reals_decoder is None: + time_varying_reals_decoder = [] + if embedding_sizes is None: + embedding_sizes = {} + if embedding_paddings is None: + embedding_paddings = [] + if embedding_labels is None: + embedding_labels = {} + if x_reals is None: + x_reals = [] + if x_categoricals is None: + x_categoricals = [] + if target_lags is None: + target_lags = {} self.save_hyperparameters() # store loss function separately as it is a module super().__init__(loss=loss, logging_metrics=logging_metrics, **kwargs) diff --git a/pytorch_forecasting/models/mlp/__init__.py b/pytorch_forecasting/models/mlp/__init__.py index 6338cd3cb..24b72c0f3 100644 --- a/pytorch_forecasting/models/mlp/__init__.py +++ b/pytorch_forecasting/models/mlp/__init__.py @@ -2,7 +2,7 @@ Simple models based on fully connected networks """ -from typing import Dict, List, Tuple, Union +from typing import Dict, List, Tuple, Union, Optional import numpy as np import torch @@ -29,18 +29,18 @@ def __init__( n_hidden_layers: int = 3, dropout: float = 0.1, norm: bool = True, - static_categoricals: List[str] = [], - static_reals: List[str] = [], - time_varying_categoricals_encoder: List[str] = [], - time_varying_categoricals_decoder: List[str] = [], - categorical_groups: Dict[str, List[str]] = {}, - time_varying_reals_encoder: List[str] = [], - time_varying_reals_decoder: List[str] = [], - embedding_sizes: Dict[str, Tuple[int, int]] = {}, - embedding_paddings: List[str] = [], - embedding_labels: Dict[str, np.ndarray] = {}, - x_reals: List[str] = [], - x_categoricals: List[str] = [], + static_categoricals: Optional[List[str]] = None, + static_reals: Optional[List[str]] = None, + time_varying_categoricals_encoder: Optional[List[str]] = None, + time_varying_categoricals_decoder: Optional[List[str]] = None, + categorical_groups: Optional[Dict[str, List[str]]] = None, + time_varying_reals_encoder: Optional[List[str]] = None, + time_varying_reals_decoder: Optional[List[str]] = None, + embedding_sizes: Optional[Dict[str, Tuple[int, int]]] = None, + embedding_paddings: Optional[List[str]] = None, + embedding_labels: Optional[Dict[str, np.ndarray]] = None, + x_reals: Optional[List[str]] = None, + x_categoricals: Optional[List[str]] = None, output_size: Union[int, List[int]] = 1, target: Union[str, List[str]] = None, loss: MultiHorizonMetric = None, @@ -82,6 +82,30 @@ def __init__( loss = QuantileLoss() if logging_metrics is None: logging_metrics = nn.ModuleList([SMAPE(), MAE(), RMSE(), MAPE(), MASE()]) + if static_categoricals is None: + static_categoricals = [] + if static_reals is None: + static_reals = [] + if time_varying_reals_encoder is None: + time_varying_reals_encoder = [] + if time_varying_categoricals_decoder is None: + time_varying_categoricals_decoder = [] + if categorical_groups is None: + categorical_groups = {} + if time_varying_reals_encoder is None: + time_varying_reals_encoder = [] + if time_varying_reals_decoder is None: + time_varying_reals_decoder = [] + if embedding_sizes is None: + embedding_sizes = {} + if embedding_paddings is None: + embedding_paddings = [] + if embedding_labels is None: + embedding_labels = {} + if x_reals is None: + x_reals = [] + if x_categoricals is None: + x_categoricals = [] self.save_hyperparameters() # store loss function separately as it is a module super().__init__(loss=loss, logging_metrics=logging_metrics, **kwargs) diff --git a/pytorch_forecasting/models/nbeats/__init__.py b/pytorch_forecasting/models/nbeats/__init__.py index 149b4fbcc..1aeedcf2c 100644 --- a/pytorch_forecasting/models/nbeats/__init__.py +++ b/pytorch_forecasting/models/nbeats/__init__.py @@ -2,7 +2,7 @@ N-Beats model for timeseries forecasting without covariates. """ -from typing import Dict, List +from typing import Dict, List, Optional import torch from torch import nn @@ -18,12 +18,12 @@ class NBeats(BaseModel): def __init__( self, - stack_types: List[str] = ["trend", "seasonality"], - num_blocks=[3, 3], - num_block_layers=[3, 3], - widths=[32, 512], - sharing: List[int] = [True, True], - expansion_coefficient_lengths: List[int] = [3, 7], + stack_types: Optional[List[str]] = None, + num_blocks: Optional[List[int]] = None, + num_block_layers: Optional[List[int]] = None, + widths: Optional[List[int]] = None, + sharing: Optional[List[bool]] = None, + expansion_coefficient_lengths: Optional[List[int]] = None, prediction_length: int = 1, context_length: int = 1, dropout: float = 0.1, @@ -87,6 +87,18 @@ def __init__( Defaults to nn.ModuleList([SMAPE(), MAE(), RMSE(), MAPE(), MASE()]) **kwargs: additional arguments to :py:class:`~BaseModel`. """ + if expansion_coefficient_lengths is None: + expansion_coefficient_lengths = [3, 7] + if sharing is None: + sharing = [True, True] + if widths is None: + widths = [32, 512] + if num_block_layers is None: + num_block_layers = [3, 3] + if num_blocks is None: + num_blocks = [3, 3] + if stack_types is None: + stack_types = ["trend", "seasonality"] if logging_metrics is None: logging_metrics = nn.ModuleList([SMAPE(), MAE(), RMSE(), MAPE(), MASE()]) if loss is None: @@ -215,8 +227,8 @@ def from_dataset(cls, dataset: TimeSeriesDataSet, **kwargs): assert ( len(dataset.flat_categoricals) == 0 and len(dataset.reals) == 1 - and len(dataset.time_varying_unknown_reals) == 1 - and dataset.time_varying_unknown_reals[0] == dataset.target + and len(dataset._time_varying_unknown_reals) == 1 + and dataset._time_varying_unknown_reals[0] == dataset.target ), "The only variable as input should be the target which is part of time_varying_unknown_reals" # initialize class diff --git a/pytorch_forecasting/models/nhits/__init__.py b/pytorch_forecasting/models/nhits/__init__.py index 68816f222..53cf18920 100644 --- a/pytorch_forecasting/models/nhits/__init__.py +++ b/pytorch_forecasting/models/nhits/__init__.py @@ -23,18 +23,18 @@ class NHiTS(BaseModelWithCovariates): def __init__( self, output_size: Union[int, List[int]] = 1, - static_categoricals: List[str] = [], - static_reals: List[str] = [], - time_varying_categoricals_encoder: List[str] = [], - time_varying_categoricals_decoder: List[str] = [], - categorical_groups: Dict[str, List[str]] = {}, - time_varying_reals_encoder: List[str] = [], - time_varying_reals_decoder: List[str] = [], - embedding_sizes: Dict[str, Tuple[int, int]] = {}, - embedding_paddings: List[str] = [], - embedding_labels: Dict[str, np.ndarray] = {}, - x_reals: List[str] = [], - x_categoricals: List[str] = [], + static_categoricals: Optional[List[str]] = None, + static_reals: Optional[List[str]] = None, + time_varying_categoricals_encoder: Optional[List[str]] = None, + time_varying_categoricals_decoder: Optional[List[str]] = None, + categorical_groups: Optional[Dict[str, List[str]]] = None, + time_varying_reals_encoder: Optional[List[str]] = None, + time_varying_reals_decoder: Optional[List[str]] = None, + embedding_sizes: Optional[Dict[str, Tuple[int, int]]] = None, + embedding_paddings: Optional[List[str]] = None, + embedding_labels: Optional[List[str]] = None, + x_reals: Optional[List[str]] = None, + x_categoricals: Optional[List[str]] = None, context_length: int = 1, prediction_length: int = 1, static_hidden_size: Optional[int] = None, @@ -42,7 +42,7 @@ def __init__( shared_weights: bool = True, activation: str = "ReLU", initialization: str = "lecun_normal", - n_blocks: List[int] = [1, 1, 1], + n_blocks: Optional[List[str]] = None, n_layers: Union[int, List[int]] = 2, hidden_size: int = 512, pooling_sizes: Optional[List[int]] = None, @@ -141,6 +141,32 @@ def __init__( Defaults to nn.ModuleList([SMAPE(), MAE(), RMSE(), MAPE(), MASE()]) **kwargs: additional arguments to :py:class:`~BaseModel`. """ + if static_categoricals is None: + static_categoricals = [] + if static_reals is None: + static_reals = [] + if time_varying_categoricals_encoder is None: + time_varying_categoricals_encoder = [] + if time_varying_categoricals_decoder is None: + time_varying_categoricals_decoder = [] + if categorical_groups is None: + categorical_groups = {} + if time_varying_reals_encoder is None: + time_varying_reals_encoder = [] + if time_varying_reals_decoder is None: + time_varying_reals_decoder = [] + if embedding_sizes is None: + embedding_sizes = {} + if embedding_paddings is None: + embedding_paddings = [] + if embedding_labels is None: + embedding_labels = {} + if x_reals is None: + x_reals = [] + if x_categoricals is None: + x_categoricals = [] + if n_blocks is None: + n_blocks = [1, 1, 1] if logging_metrics is None: logging_metrics = nn.ModuleList([SMAPE(), MAE(), RMSE(), MAPE(), MASE()]) if loss is None: diff --git a/pytorch_forecasting/models/nn/embeddings.py b/pytorch_forecasting/models/nn/embeddings.py index e8912d9ab..c9af1740f 100644 --- a/pytorch_forecasting/models/nn/embeddings.py +++ b/pytorch_forecasting/models/nn/embeddings.py @@ -1,4 +1,4 @@ -from typing import Dict, List, Tuple, Union +from typing import Dict, List, Tuple, Union, Optional import torch import torch.nn as nn @@ -35,8 +35,8 @@ def __init__( self, embedding_sizes: Union[Dict[str, Tuple[int, int]], Dict[str, int], List[int], List[Tuple[int, int]]], x_categoricals: List[str] = None, - categorical_groups: Dict[str, List[str]] = {}, - embedding_paddings: List[str] = [], + categorical_groups: Optional[Dict[str, List[str]]] = None, + embedding_paddings: Optional[List[str]] = None, max_embedding_size: int = None, ): """Embedding layer for categorical variables including groups of categorical variables. @@ -70,6 +70,10 @@ def __init__( max_embedding_size (int, optional): if embedding size defined by ``embedding_sizes`` is larger than ``max_embedding_size``, it will be constrained. Defaults to None. """ + if categorical_groups is None: + categorical_groups = {} + if embedding_paddings is None: + embedding_paddings = [] super().__init__() if isinstance(embedding_sizes, dict): self.concat_output = False # return dictionary of embeddings diff --git a/pytorch_forecasting/models/rnn/__init__.py b/pytorch_forecasting/models/rnn/__init__.py index c685d67a5..142892dcb 100644 --- a/pytorch_forecasting/models/rnn/__init__.py +++ b/pytorch_forecasting/models/rnn/__init__.py @@ -3,7 +3,7 @@ """ from copy import copy -from typing import Dict, List, Tuple, Union +from typing import Dict, List, Tuple, Union, Optional import numpy as np import torch @@ -24,21 +24,21 @@ def __init__( hidden_size: int = 10, rnn_layers: int = 2, dropout: float = 0.1, - static_categoricals: List[str] = [], - static_reals: List[str] = [], - time_varying_categoricals_encoder: List[str] = [], - time_varying_categoricals_decoder: List[str] = [], - categorical_groups: Dict[str, List[str]] = {}, - time_varying_reals_encoder: List[str] = [], - time_varying_reals_decoder: List[str] = [], - embedding_sizes: Dict[str, Tuple[int, int]] = {}, - embedding_paddings: List[str] = [], - embedding_labels: Dict[str, np.ndarray] = {}, - x_reals: List[str] = [], - x_categoricals: List[str] = [], + static_categoricals: Optional[List[str]] = None, + static_reals: Optional[List[str]] = None, + time_varying_categoricals_encoder: Optional[List[str]] = None, + time_varying_categoricals_decoder: Optional[List[str]] = None, + categorical_groups: Optional[Dict[str, List[str]]] = None, + time_varying_reals_encoder: Optional[List[str]] = None, + time_varying_reals_decoder: Optional[List[str]] = None, + embedding_sizes: Optional[Dict[str, Tuple[int, int]]] = None, + embedding_paddings: Optional[List[str]] = None, + embedding_labels: Optional[Dict[str, np.ndarray]] = None, + x_reals: Optional[List[str]] = None, + x_categoricals: Optional[List[str]] = None, output_size: Union[int, List[int]] = 1, target: Union[str, List[str]] = None, - target_lags: Dict[str, List[int]] = {}, + target_lags: Optional[Dict[str, List[int]]] = None, loss: MultiHorizonMetric = None, logging_metrics: nn.ModuleList = None, **kwargs, @@ -81,6 +81,32 @@ def __init__( logging_metrics (nn.ModuleList, optional): Metrics to log during training. Defaults to nn.ModuleList([SMAPE(), MAE(), RMSE(), MAPE(), MASE()]). """ + if static_categoricals is None: + static_categoricals = [] + if static_reals is None: + static_reals = [] + if time_varying_categoricals_encoder is None: + time_varying_categoricals_encoder = [] + if time_varying_categoricals_decoder is None: + time_varying_categoricals_decoder = [] + if categorical_groups is None: + categorical_groups = {} + if time_varying_reals_encoder is None: + time_varying_reals_encoder = [] + if time_varying_reals_decoder is None: + time_varying_reals_decoder = [] + if embedding_sizes is None: + embedding_sizes = {} + if embedding_paddings is None: + embedding_paddings = [] + if embedding_labels is None: + embedding_labels = {} + if x_reals is None: + x_reals = [] + if x_categoricals is None: + x_categoricals = [] + if target_lags is None: + target_lags = {} if loss is None: loss = MAE() if logging_metrics is None: diff --git a/pytorch_forecasting/models/temporal_fusion_transformer/__init__.py b/pytorch_forecasting/models/temporal_fusion_transformer/__init__.py index cc5066126..e260b928e 100644 --- a/pytorch_forecasting/models/temporal_fusion_transformer/__init__.py +++ b/pytorch_forecasting/models/temporal_fusion_transformer/__init__.py @@ -3,7 +3,7 @@ """ from copy import copy -from typing import Dict, List, Tuple, Union +from typing import Dict, List, Tuple, Union, Optional import numpy as np import torch @@ -36,26 +36,26 @@ def __init__( loss: MultiHorizonMetric = None, attention_head_size: int = 4, max_encoder_length: int = 10, - static_categoricals: List[str] = [], - static_reals: List[str] = [], - time_varying_categoricals_encoder: List[str] = [], - time_varying_categoricals_decoder: List[str] = [], - categorical_groups: Dict[str, List[str]] = {}, - time_varying_reals_encoder: List[str] = [], - time_varying_reals_decoder: List[str] = [], - x_reals: List[str] = [], - x_categoricals: List[str] = [], + static_categoricals: Optional[List[str]] = None, + static_reals: Optional[List[str]] = None, + time_varying_categoricals_encoder: Optional[List[str]] = None, + time_varying_categoricals_decoder: Optional[List[str]] = None, + categorical_groups: Optional[Union[Dict, List[str]]] = None, + time_varying_reals_encoder: Optional[List[str]] = None, + time_varying_reals_decoder: Optional[List[str]] = None, + x_reals: Optional[List[str]] = None, + x_categoricals: Optional[List[str]] = None, hidden_continuous_size: int = 8, - hidden_continuous_sizes: Dict[str, int] = {}, - embedding_sizes: Dict[str, Tuple[int, int]] = {}, - embedding_paddings: List[str] = [], - embedding_labels: Dict[str, np.ndarray] = {}, + hidden_continuous_sizes: Optional[Dict[str, int]] = None, + embedding_sizes: Optional[Dict[str, Tuple[int, int]]] = None, + embedding_paddings: Optional[List[str]] = None, + embedding_labels: Optional[Dict[str, np.ndarray]] = None, learning_rate: float = 1e-3, log_interval: Union[int, float] = -1, log_val_interval: Union[int, float] = None, log_gradient_flow: bool = False, reduce_on_plateau_patience: int = 1000, - monotone_constaints: Dict[str, int] = {}, + monotone_constaints: Optional[Dict[str, int]] = None, share_single_variable_networks: bool = False, causal_attention: bool = True, logging_metrics: nn.ModuleList = None, @@ -133,6 +133,34 @@ def __init__( Defaults to nn.ModuleList([SMAPE(), MAE(), RMSE(), MAPE()]). **kwargs: additional arguments to :py:class:`~BaseModel`. """ + if monotone_constaints is None: + monotone_constaints = {} + if embedding_labels is None: + embedding_labels = {} + if embedding_paddings is None: + embedding_paddings = [] + if embedding_sizes is None: + embedding_sizes = {} + if hidden_continuous_sizes is None: + hidden_continuous_sizes = {} + if x_categoricals is None: + x_categoricals = [] + if x_reals is None: + x_reals = [] + if time_varying_reals_decoder is None: + time_varying_reals_decoder = [] + if time_varying_reals_encoder is None: + time_varying_reals_encoder = [] + if categorical_groups is None: + categorical_groups = {} + if time_varying_categoricals_decoder is None: + time_varying_categoricals_decoder = [] + if time_varying_categoricals_encoder is None: + time_varying_categoricals_encoder = [] + if static_reals is None: + static_reals = [] + if static_categoricals is None: + static_categoricals = [] if logging_metrics is None: logging_metrics = nn.ModuleList([SMAPE(), MAE(), RMSE(), MAPE()]) if loss is None: @@ -606,7 +634,7 @@ def interpret_output( - shifts[:, None, None, None] ) % encoder_attention.size(3) encoder_attention = torch.gather(encoder_attention, dim=3, index=new_index) - # expand encoder_attentiont to full size + # expand encoder_attention to full size if encoder_attention.size(-1) < self.hparams.max_encoder_length: encoder_attention = torch.concat( [ diff --git a/pytorch_forecasting/models/temporal_fusion_transformer/sub_modules.py b/pytorch_forecasting/models/temporal_fusion_transformer/sub_modules.py index f3d6ae351..db9a0a884 100644 --- a/pytorch_forecasting/models/temporal_fusion_transformer/sub_modules.py +++ b/pytorch_forecasting/models/temporal_fusion_transformer/sub_modules.py @@ -4,6 +4,7 @@ import math from typing import Dict, Tuple +from copy import deepcopy import torch import torch.nn as nn @@ -250,20 +251,21 @@ def __init__( self, input_sizes: Dict[str, int], hidden_size: int, - input_embedding_flags: Dict[str, bool] = {}, + input_embedding_flags: Dict[str, bool] = None, dropout: float = 0.1, context_size: int = None, - single_variable_grns: Dict[str, GatedResidualNetwork] = {}, - prescalers: Dict[str, nn.Linear] = {}, + single_variable_grns: Dict[str, GatedResidualNetwork] = None, + prescalers: Dict[str, nn.Linear] = None, ): """ - Calcualte weights for ``num_inputs`` variables which are each of size ``input_size`` + Calculate weights for ``num_inputs`` variables which are each of size ``input_size`` """ super().__init__() self.hidden_size = hidden_size self.input_sizes = input_sizes self.input_embedding_flags = input_embedding_flags + self._input_embedding_flags = {} if input_embedding_flags is None else deepcopy(input_embedding_flags) self.dropout = dropout self.context_size = context_size @@ -285,13 +287,14 @@ def __init__( self.dropout, residual=False, ) - + if single_variable_grns is None: + single_variable_grns = {} self.single_variable_grns = nn.ModuleDict() self.prescalers = nn.ModuleDict() for name, input_size in self.input_sizes.items(): if name in single_variable_grns: self.single_variable_grns[name] = single_variable_grns[name] - elif self.input_embedding_flags.get(name, False): + elif self._input_embedding_flags.get(name, False): self.single_variable_grns[name] = ResampleNorm(input_size, self.hidden_size) else: self.single_variable_grns[name] = GatedResidualNetwork( @@ -300,16 +303,18 @@ def __init__( output_size=self.hidden_size, dropout=self.dropout, ) + if prescalers is None: + prescalers = {} if name in prescalers: # reals need to be first scaled up self.prescalers[name] = prescalers[name] - elif not self.input_embedding_flags.get(name, False): + elif not self._input_embedding_flags.get(name, False): self.prescalers[name] = nn.Linear(1, input_size) self.softmax = nn.Softmax(dim=-1) @property def input_size_total(self): - return sum(size if name in self.input_embedding_flags else size for name, size in self.input_sizes.items()) + return sum(size if name in self._input_embedding_flags else size for name, size in self.input_sizes.items()) @property def num_inputs(self): diff --git a/pytorch_forecasting/utils/_utils.py b/pytorch_forecasting/utils/_utils.py index 1236ca666..a814c3cb5 100644 --- a/pytorch_forecasting/utils/_utils.py +++ b/pytorch_forecasting/utils/_utils.py @@ -514,7 +514,7 @@ def repr_class( obj, attributes: Union[List[str], Dict[str, Any]], max_characters_before_break: int = 100, - extra_attributes: Dict[str, Any] = {}, + extra_attributes: Dict[str, Any] = None, ) -> str: """Print class name and parameters. @@ -527,6 +527,8 @@ def repr_class( Returns: str """ + if extra_attributes is None: + extra_attributes = {} # get attributes if isinstance(attributes, (tuple, list)): attributes = {name: getattr(obj, name) for name in attributes if hasattr(obj, name)}