def get_matrices(series: Optional[TimeSeries], method: str): """Returns the G matrix given a specified reconciliation method.""" S = _get_summation_matrix(series) if method == "ols": # G = inv(S'*S)*S' G = np.linalg.inv(S.T @ S) @ S.T return S, G elif method == "wls_struct": # Wh is a diagonal matrix with entry i,i being the sum of row i of S_mat Wh = np.diag(np.sum(S, axis=1)) elif method == "wls_var": # In this case we assume that series contains the residuals of some forecasts MinTReconciliator._assert_deterministic(series) et2 = series.values(copy=False) ** 2 # squared residuals # Wh diagonal is mean squared residual over time: Wh = np.diag(et2.mean(axis=0)) elif method == "wls_val": # Wh is a diagonal matrix with entry i,i being the average value of the corresponding time series quantities = series.all_values(copy=False).mean(axis=2).mean(axis=0) Wh = np.diag(np.array(quantities)) elif method == "mint_cov": MinTReconciliator._assert_deterministic(series) Wh = np.cov( series.values(copy=False).T ) # + 1e-3 * np.eye(len(series.components)) else: raise_if_not(False, f"Unknown method: {method}") Wh_inv = np.linalg.inv(Wh) G = np.linalg.inv(S.T @ Wh_inv @ S) @ S.T @ Wh_inv return S, G
def _assert_deterministic(series: TimeSeries): raise_if_not( series.is_deterministic, "When used with method wls_var or mint_cov, the MinT reconciliator " + "has to be fit on a deterministic series " + "containing residuals. This series is stochastic.", )
def _check(param, predicate, param_name, condition_str): if param is None: return if isinstance(param, (collections.abc.Sequence, np.ndarray)): raise_if_not( all(predicate(p) for p in param), f"All provided parameters {param_name} must be {condition_str}.", ) else: raise_if_not( predicate(param), f"The parameter {param_name} must be {condition_str}.", )
def __init__(self, method="ols"): """ MinT Reconcilator. This implements the MinT reconcilation approach presented in [1]_ and summarised in [2]_. Parameters ---------- method This parameter can take four different values, determining how the covariance matrix ``W`` of the forecast errors is estimated (corresponding to ``Wh`` in [2]_): * ``ols`` uses ``W = I``. This option looks only at the hierarchy but ignores the values of the series provided to ``fit()``. * ``wls_struct`` uses ``W = diag(S1)``, where ``S1`` is a vector of size `n` with values between 0 and `m`, representing the number of base components composing each of the `n` components. This options looks only at the hierarchy but ignores the values of the series provided to ``fit()``. * ``wls_var`` uses ``W = diag(W1)``, where ``W1`` is the temporal average of the variance of the forecasting residuals. This method assumes that the series provided to ``fit()`` contain the forecast residuals (deterministic series). * ``mint_cov`` computes ``W`` as the empirical covariance matrix of the residuals for each component, with residuals samples taken over time. This method assumes that the series provided to ``fit()`` contain the forecast residuals (deterministic series), and it requires the residuals to be linearly independent. * ``wls_val`` uses ``W = diag(V1)``, where ``V1`` is the temporal average of the component values. This method assumes that the series provided to ``fit()`` contains an example of the actual values (e.g., either the training series or the forecasts). This method is not presented in [2]_. References ---------- .. [1] `Optimal forecast reconciliation for hierarchical and grouped time series through trace minimization <https://robjhyndman.com/papers/MinT.pdf>`_ .. [2] https://otexts.com/fpp3/reconciliation.html#the-mint-optimal-reconciliation-approach """ super().__init__() known_methods = ["ols", "wls", "wls_var", "wls_struct", "wls_val", "mint_cov"] raise_if_not( method in known_methods, f"The method must be one of {known_methods}", ) self.method = method
def _get_summation_matrix(series: TimeSeries): """ Returns the matrix S for a series, as defined `here <https://otexts.com/fpp3/reconciliation.html>`_. The dimension of the matrix is `(n, m)`, where `n` is the number of components and `m` the number of base components (components that are not the sum of any other components). S[i, j] contains 1 if component i is "made up" of base component j, and 0 otherwise. The order of the `n` and `m` components in the matrix match the order of the components in the `series`. The matrix is built using the ``hierarchy`` property of the ``series``. ``hierarchy`` must be a dictionary mapping each (non top-level) component to its parent(s) in the aggregation. """ raise_if_not( series.has_hierarchy, "The provided series must have a hierarchy defined for reconciliation to be performed.", ) hierarchy = series.hierarchy components_seq = list(series.components) leaves_seq = series.bottom_level_components m = len(leaves_seq) n = len(components_seq) S = np.zeros((n, m)) components_indexes = {c: i for i, c in enumerate(components_seq)} leaves_indexes = {l: i for i, l in enumerate(leaves_seq)} def increment(cur_node, leaf_idx): """ Recursive function filling S for a given base component and all its ancestors """ S[components_indexes[cur_node], leaf_idx] = 1.0 if cur_node in hierarchy: for parent in hierarchy[cur_node]: increment(parent, leaf_idx) for leaf in leaves_seq: leaf_idx = leaves_indexes[leaf] increment(leaf, leaf_idx) return S.astype(series.dtype)
def compute_loss(self, model_output: torch.Tensor, target: torch.Tensor): """ We are re-defining a custom loss (which is not a likelihood loss) compared to Likelihood Parameters ---------- model_output must be of shape (batch_size, n_timesteps, n_target_variables, n_quantiles) target must be of shape (n_samples, n_timesteps, n_target_variables) """ dim_q = 3 batch_size, length = model_output.shape[:2] device = model_output.device # test if torch model forward produces correct output and store quantiles tensor if self.first: raise_if_not( len(model_output.shape) == 4 and len(target.shape) == 3 and model_output.shape[:2] == target.shape[:2], "mismatch between predicted and target shape", ) raise_if_not( model_output.shape[dim_q] == len(self.quantiles), "mismatch between number of predicted quantiles and target quantiles", ) self.quantiles_tensor = torch.tensor(self.quantiles).to(device) self.first = False errors = target.unsqueeze(-1) - model_output losses = torch.max((self.quantiles_tensor - 1) * errors, self.quantiles_tensor * errors) return losses.sum(dim=dim_q).mean()