bunobee 0.0.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
bunobee/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from importlib.metadata import version
2
+
3
+ name = "bunobee"
4
+ __version__ = version("bunobee")
bunobee/hyper_tune.py ADDED
@@ -0,0 +1,180 @@
1
+ import jax
2
+ import jax.numpy as jnp
3
+ import numpy as np
4
+ from jax import vmap
5
+ import logging
6
+ import xarray as xr
7
+
8
+ from typing import Dict, Tuple
9
+
10
+ from .utils import flatten_front_dim
11
+ from .models.dlt import run_dlt_model
12
+
13
+ logger = logging.getLogger("wunku")
14
+
15
+
16
+ def slice_trend_single(trend_comp: jnp.ndarray, h: int) -> jnp.ndarray:
17
+ """Slices a single trend into overlapping windows of size h.
18
+
19
+ Args
20
+ ----
21
+ trend_comp: array representing the trend component with shape (n_steps, )
22
+ h: Size of the window to slice the trend into.
23
+
24
+ Returns
25
+ -------
26
+ A 2D array of shape (n_windows, h) where each row is a window of size h where
27
+ num_windows = n_steps - h + 1.
28
+ """
29
+ n_steps = trend_comp.shape[0]
30
+ num_windows = n_steps - h + 1
31
+
32
+ def get_window(t_idx):
33
+ return jax.lax.dynamic_slice(trend_comp, start_indices=(t_idx,), slice_sizes=(h,))
34
+
35
+ t_indices = jnp.arange(num_windows)
36
+ # (n_windows, h)
37
+ return vmap(get_window)(t_indices)
38
+
39
+
40
+ def slice_trend(trend_comp, h):
41
+ """Slices multiple trends into overlapping windows of size h.
42
+ Args
43
+ ----
44
+ trend_comp: array representing multiple trend components with shape (N_samples, n_steps)
45
+ h: Size of the window to slice the trends into.
46
+
47
+ Returns
48
+ -------
49
+ A 3D array of shape (N_samples, n_windows, h) where each row is a window of size h
50
+ and num_windows = n_steps - h + 1 for each trend.
51
+ """
52
+ # trends: shape (N_samples, T)
53
+ return vmap(lambda trend_comp: slice_trend_single(trend_comp, h))(trend_comp)
54
+
55
+
56
+ def generate_forecast_span_samples(idata: xr.Dataset, h: int) -> jnp.ndarray:
57
+ # how to do a sliding window prediction?
58
+ dlt_comp = flatten_front_dim(idata["dlt_comp"].to_numpy(), n=2)
59
+ reg_comp = flatten_front_dim(idata["reg_comp"].to_numpy(), n=2)
60
+
61
+ logger.debug("dlt_comp shape:", dlt_comp.shape)
62
+ logger.debug("reg_comp shape:", reg_comp.shape)
63
+
64
+ dlt_comp_slice = dlt_comp[:, : -(h - 1), None]
65
+ reg_comp_slice = slice_trend(reg_comp, h=h)
66
+
67
+ # (n_samples, n_windows, h)
68
+ yhat_span = dlt_comp_slice + reg_comp_slice
69
+ return yhat_span
70
+
71
+
72
+ def compute_log_likelihood(yhat_span, sigma, y):
73
+ """Computes the log likelihood of the forecasted span given the observations.
74
+
75
+ Args
76
+ ----
77
+ yhat_span: Forecasted span of shape (n_samples, n_windows, h).
78
+ sigma: Standard deviation of the noise, shape (n_samples,)
79
+ y_slice: Observations for the forecasted span, shape (n_windows, h).
80
+
81
+ Returns
82
+ -------
83
+ A 3D array of log probabilities of shape (n_samples, n_windows, h).
84
+ """
85
+ h = yhat_span.shape[-1]
86
+ # reshape for broadcasting
87
+ # (n_samples, 1, 1)
88
+ sigma_broadcast = sigma[:, None, None]
89
+ # (n_windos, h)
90
+ y_slice = slice_trend_single(y, h=h)
91
+ # (1, n_windows, h)
92
+ y_slice_broadcast = y_slice[None, :, :]
93
+
94
+ # compute squared error
95
+ sq_error = (y_slice_broadcast - yhat_span) ** 2
96
+
97
+ # compute log likelihood per (s, t, h)
98
+ log_prob = -0.5 * jnp.log(2 * jnp.pi) - jnp.log(sigma_broadcast) - 0.5 * sq_error / (sigma_broadcast**2)
99
+
100
+ return log_prob
101
+
102
+
103
+ def compute_wbic(loglk: jnp.ndarray) -> float:
104
+ """Compute Weighted Bayesian Information Criterion (WBIC) from log likelihood.
105
+ Args
106
+ ----
107
+ loglk: array-like, log likelihood per samples per obs; should be with values of shape (n_samples, n_steps, h)
108
+ """
109
+ # in original paper, they use sum but it leads to unstable scale due to different size of datasets
110
+ # we use mean instead
111
+ loglk_per_sample = jnp.nanmean(loglk, axis=(-1, -2))
112
+ # (n_steps * h, )
113
+ nobs = loglk.shape[-1] * loglk.shape[-2]
114
+ beta = 1.0 / jnp.log(nobs)
115
+ wbic = -(1.0 / beta) * jnp.nanmean(loglk_per_sample)
116
+ wbic = float(wbic)
117
+ return wbic
118
+
119
+
120
+ def run_dlt_model_and_compute_wbic(params: Tuple, data: Dict[str, np.ndarray], h: int = 12) -> float:
121
+ lev_sm, slp_sm, theta = params
122
+ y = data["y"]
123
+ x_seas = data["x_seas"]
124
+ x_glb_trend = data["x_glb_trend"]
125
+ logger.info(f"Trying lev_sm={lev_sm:.4f}, slp_sm={slp_sm:.4f}, theta={theta:.4f}")
126
+
127
+ idata = run_dlt_model(
128
+ lev_sm=lev_sm,
129
+ slp_sm=slp_sm,
130
+ theta=theta,
131
+ x_seas=x_seas,
132
+ x_glb_trend=x_glb_trend,
133
+ y=y,
134
+ )
135
+
136
+ yhat_span = generate_forecast_span_samples(idata=idata, h=h)
137
+ loglk = compute_log_likelihood(
138
+ yhat_span=yhat_span,
139
+ sigma=flatten_front_dim(idata["sigma"].to_numpy(), n=2),
140
+ y=y,
141
+ )
142
+ wbic = compute_wbic(loglk)
143
+
144
+ print(f"WBIC: {wbic:.4f}")
145
+ return wbic
146
+
147
+
148
+ def hyper_tuning_dlt_with_wbic(
149
+ data: Dict[str, np.ndarray],
150
+ # forecast horizon
151
+ h: int = 12,
152
+ n_calls: int = 15,
153
+ random_state: int = 42,
154
+ ):
155
+ print("Starting hyperparameter tuning for DLT model using WBIC...")
156
+ # print args
157
+ print(f"h: {h}, n_calls: {n_calls}, random_state: {random_state}")
158
+ # try import skopt
159
+ try:
160
+ from skopt import gp_minimize
161
+ from skopt.space import Real
162
+ except ImportError:
163
+ raise ImportError("Please install scikit-optimize to run hyperparameter tuning.")
164
+
165
+ # Define the hyperparam space
166
+ search_space = [
167
+ Real(0.0001, 0.1, prior="log-uniform", name="lev_sm"),
168
+ Real(0.001, 0.1, prior="log-uniform", name="slp_sm"),
169
+ Real(0, 1.0, prior="uniform", name="theta"),
170
+ ]
171
+
172
+ # Run Bayesian Optimization
173
+ result = gp_minimize(
174
+ func=lambda params: run_dlt_model_and_compute_wbic(params=params, data=data, h=h),
175
+ dimensions=search_space,
176
+ n_calls=n_calls,
177
+ random_state=random_state,
178
+ )
179
+
180
+ return result
@@ -0,0 +1 @@
1
+