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

[FIX] Make Y_df optional for the reconcile method #65

Merged
merged 3 commits into from
Oct 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 13 additions & 6 deletions hierarchicalforecast/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,13 @@ class HierarchicalReconciliation:
def __init__(self,
reconcilers: List[Callable]):
self.reconcilers = reconcilers
self.insample = any([method.insample for method in reconcilers])

def reconcile(self,
Y_hat_df: pd.DataFrame,
Y_df: pd.DataFrame,
S: pd.DataFrame,
tags: Dict[str, np.ndarray],
Y_df: Optional[pd.DataFrame] = None,
level: Optional[List[int]] = None,
bootstrap: bool = False):
"""Hierarchical Reconciliation Method.
Expand Down Expand Up @@ -87,11 +88,17 @@ def reconcile(self,
# same order of Y_hat_df to prevent errors
S_ = S.loc[uids]
common_vals = dict(
y_insample = Y_df.pivot(columns='ds', values='y').loc[uids].values.astype(np.float32),
S = S_.values.astype(np.float32),
idx_bottom = S_.index.get_indexer(S.columns),
S=S_.values.astype(np.float32),
idx_bottom=S_.index.get_indexer(S.columns),
tags={key: S_.index.get_indexer(val) for key, val in tags.items()}
)
# we need insample values if
# we are using a method that requires them
# or if we are performing boostrap
if self.insample or bootstrap:
if Y_df is None:
raise Exception('you need to pass `Y_df`')
common_vals['y_insample'] = Y_df.pivot(columns='ds', values='y').loc[uids].values.astype(np.float32)
fcsts = Y_hat_df.copy()
for reconcile_fn in self.reconcilers:
reconcile_fn_name = _build_fn_name(reconcile_fn)
Expand All @@ -117,7 +124,7 @@ def reconcile(self,
sigmah = sign * (y_hat_model - sigmah) / z
common_vals['sigmah'] = sigmah
common_vals['level'] = level
if has_fitted or bootstrap:
if (self.insample and has_fitted) or bootstrap:
if model_name in Y_df:
y_hat_insample = Y_df.pivot(columns='ds', values=model_name).loc[uids].values
y_hat_insample = y_hat_insample.astype(np.float32)
Expand Down Expand Up @@ -151,6 +158,6 @@ def reconcile(self,
else:
del common_vals['bootstrap_samples']
del common_vals['bootstrap']
if has_fitted:
if self.insample and has_fitted:
del common_vals['y_hat_insample']
return fcsts
35 changes: 20 additions & 15 deletions hierarchicalforecast/methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ class BottomUp:
- [Orcutt, G.H., Watts, H.W., & Edwards, J.B.(1968). \"Data aggregation and information loss\". The American
Economic Review, 58 , 773{787)](http://www.jstor.org/stable/1815532).
"""
insample = False

def reconcile(self,
S: np.ndarray,
y_hat: np.ndarray,
Expand Down Expand Up @@ -253,12 +255,13 @@ class TopDown:
def __init__(self,
method: str):
self.method = method
self.insample = method in ['average_proportions', 'proportion_averages']

def reconcile(self,
S: np.ndarray,
y_hat: np.ndarray,
y_insample: np.ndarray,
tags: Dict[str, np.ndarray],
y_insample: Optional[np.ndarray] = None,
sigmah: Optional[np.ndarray] = None,
level: Optional[List[int]] = None,
bootstrap: bool = False,
Expand All @@ -268,6 +271,8 @@ def reconcile(self,
**Parameters:**<br>
`S`: Summing matrix of size (`base`, `bottom`).<br>
`y_hat`: Forecast values of size (`base`, `horizon`).<br>
`tags`: Each key is a level and each value its `S` indices.<br>
`y_insample`: Insample values of size (`base`, `insample_size`). Optional for `forecast_proportions` method.<br>
`idx_bottom`: Indices corresponding to the bottom level of `S`, size (`bottom`).<br>
`sigmah`: float, estimate of the standard deviation of the h-step forecast of size (`base`, `horizon`)<br>
`level`: float list 0-100, confidence levels for prediction intervals.<br>
Expand Down Expand Up @@ -345,7 +350,7 @@ def middle_out(S: np.ndarray,
counter += idxs_len
td = top_down(S_node,
y_hat[idxs_node],
y_insample[idxs_node],
y_insample[idxs_node] if y_insample is not None else None,
levels_node_,
method=top_down_method)
reconciled[idxs_node] = td['mean']
Expand Down Expand Up @@ -375,19 +380,20 @@ def __init__(self,
top_down_method: str):
self.middle_level = middle_level
self.top_down_method = top_down_method
self.insample = top_down_method in ['average_proportions', 'proportion_averages']

def reconcile(self,
S: np.ndarray,
y_hat: np.ndarray,
y_insample: np.ndarray,
tags: Dict[str, np.ndarray]):
tags: Dict[str, np.ndarray],
y_insample: Optional[np.ndarray] = None):
"""Middle Out Reconciliation Method.

**Parameters:**<br>
`S`: Summing matrix of size (`base`, `bottom`).<br>
`y_hat`: Forecast values of size (`base`, `horizon`).<br>
`y_insample`: Insample values of size (`base`, `insample_size`).<br>
`levels`: Each key is a level and each value its `S` indices.<br>
`tags`: Each key is a level and each value its `S` indices.<br>
`y_insample`: Insample values of size (`base`, `insample_size`). Only used for `forecast_proportions`<br>

**Returns:**<br>
`y_tilde`: Reconciliated y_hat using the Middle Out approach.
Expand Down Expand Up @@ -471,7 +477,7 @@ class MinTrace:
\mathbf{S}^{\intercal}\mathbf{W}^{-1}_{h}$$

**Parameters:**<br>
`method`: str, one of `ols`, `wls_struct`, `wls_var`, `mint_shrink`, `mint_co`.<br>
`method`: str, one of `ols`, `wls_struct`, `wls_var`, `mint_shrink`, `mint_cov`.<br>

**References:**<br>
- [Wickramasuriya, S. L., Athanasopoulos, G., & Hyndman, R. J. (2019). \"Optimal forecast reconciliation for
Expand All @@ -481,12 +487,13 @@ class MinTrace:
def __init__(self,
method: str):
self.method = method
self.insample = method in ['wls_var', 'mint_cov', 'mint_shrink']

def reconcile(self,
S: np.ndarray,
y_hat: np.ndarray,
y_insample: np.ndarray,
y_hat_insample: np.ndarray,
y_insample: Optional[np.ndarray] = None,
y_hat_insample: Optional[np.ndarray] = None,
sigmah: Optional[np.ndarray] = None,
level: Optional[List[int]] = None,
bootstrap: bool = False,
Expand All @@ -496,7 +503,8 @@ def reconcile(self,
**Parameters:**<br>
`S`: Summing matrix of size (`base`, `bottom`).<br>
`y_hat`: Forecast values of size (`base`, `horizon`).<br>
`idx_bottom`: Indices corresponding to the bottom level of `S`, size (`bottom`).<br>
`y_insample`: Insample values of size (`base`, `insample_size`). Only used by `wls_var`, `mint_cov`, `mint_shrink`<br>
`y_hat_insample`: Insample fitted values of size (`base`, `insample_size`). Only used by `wls_var`, `mint_cov`, `mint_shrink`<br>
`sigmah`: float, estimate of the standard deviation of the h-step forecast of size (`base`, `horizon`)<br>
`level`: float list 0-100, confidence levels for prediction intervals.<br>
`bootstrap`: bool, whether or not to use bootstraped prediction intervals, alternative normality assumption.<br>
Expand Down Expand Up @@ -560,12 +568,11 @@ def __init__(self,
raise ValueError(f"Optimal Combination class does not support method: \"{method}\"")

self.method = method
self.insample = False

def reconcile(self,
S: np.ndarray,
y_hat: np.ndarray,
y_insample: np.ndarray = None,
y_hat_insample: np.ndarray = None,
sigmah: Optional[np.ndarray] = None,
level: Optional[List[int]] = None,
bootstrap: bool = False,
Expand All @@ -575,7 +582,6 @@ def reconcile(self,
**Parameters:**<br>
`S`: Summing matrix of size (`base`, `bottom`).<br>
`y_hat`: Forecast values of size (`base`, `horizon`).<br>
`idx_bottom`: Indices corresponding to the bottom level of `S`, size (`bottom`).<br>
`sigmah`: float, estimate of the standard deviation of the h-step forecast of size (`base`, `horizon`)<br>
`level`: float list 0-100, confidence levels for prediction intervals.<br>
`bootstrap`: bool, whether or not to use bootstraped prediction intervals, alternative normality assumption.<br>
Expand All @@ -586,8 +592,6 @@ def reconcile(self,
"""
return optimal_combination(S=S,
y_hat=y_hat,
y_insample=y_insample,
y_hat_insample=y_hat_insample,
method=self.method, sigmah=sigmah,
level=level, bootstrap=bootstrap,
bootstrap_samples=bootstrap_samples)
Expand Down Expand Up @@ -704,6 +708,7 @@ def __init__(self,
lambda_reg: float = 1e-2):
self.method = method
self.lambda_reg = lambda_reg
self.insample = True

def reconcile(self,
S: np.ndarray,
Expand Down
Loading