コード例 #1
0
    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
コード例 #2
0
 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.",
     )
コード例 #3
0
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}.",
        )
コード例 #4
0
    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
コード例 #5
0
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)
コード例 #6
0
    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()