bunobee 0.0.4__tar.gz
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-0.0.4/LICENSE +21 -0
- bunobee-0.0.4/PKG-INFO +45 -0
- bunobee-0.0.4/README.md +13 -0
- bunobee-0.0.4/pyproject.toml +57 -0
- bunobee-0.0.4/setup.cfg +4 -0
- bunobee-0.0.4/src/bunobee/__init__.py +4 -0
- bunobee-0.0.4/src/bunobee/hyper_tune.py +180 -0
- bunobee-0.0.4/src/bunobee/models/__init__.py +1 -0
- bunobee-0.0.4/src/bunobee/models/dlt.py +541 -0
- bunobee-0.0.4/src/bunobee/models/m5/__init__.py +0 -0
- bunobee-0.0.4/src/bunobee/models/m5/m5_ssp_mcmc.py +135 -0
- bunobee-0.0.4/src/bunobee/models/m5/m5_ssp_optim.py +423 -0
- bunobee-0.0.4/src/bunobee/models/ssp/__init__.py +19 -0
- bunobee-0.0.4/src/bunobee/models/ssp/kalman_1d.py +537 -0
- bunobee-0.0.4/src/bunobee/models/ssp/kalman_1d_ekf.py +455 -0
- bunobee-0.0.4/src/bunobee/models/ssp/kalman_1d_st.py +194 -0
- bunobee-0.0.4/src/bunobee/models/ssp/kalman_1d_st_ekf.py +209 -0
- bunobee-0.0.4/src/bunobee/models/ssp/plotting.py +249 -0
- bunobee-0.0.4/src/bunobee/models/ssp/posterior.py +157 -0
- bunobee-0.0.4/src/bunobee/models/ssp/prior.py +239 -0
- bunobee-0.0.4/src/bunobee/models/ssp/transforms.py +608 -0
- bunobee-0.0.4/src/bunobee/models/ssp/vfactory.py +73 -0
- bunobee-0.0.4/src/bunobee/regression.py +232 -0
- bunobee-0.0.4/src/bunobee/saturation.py +131 -0
- bunobee-0.0.4/src/bunobee/simulation/__init__.py +0 -0
- bunobee-0.0.4/src/bunobee/simulation/ssp.py +97 -0
- bunobee-0.0.4/src/bunobee/utils.py +33 -0
- bunobee-0.0.4/src/bunobee.egg-info/PKG-INFO +45 -0
- bunobee-0.0.4/src/bunobee.egg-info/SOURCES.txt +37 -0
- bunobee-0.0.4/src/bunobee.egg-info/dependency_links.txt +1 -0
- bunobee-0.0.4/src/bunobee.egg-info/requires.txt +12 -0
- bunobee-0.0.4/src/bunobee.egg-info/top_level.txt +1 -0
- bunobee-0.0.4/tests/test_dlt.py +176 -0
- bunobee-0.0.4/tests/test_extend_states_prior.py +217 -0
- bunobee-0.0.4/tests/test_kalman_filters.py +418 -0
- bunobee-0.0.4/tests/test_kalman_smoothers.py +246 -0
- bunobee-0.0.4/tests/test_saturation.py +63 -0
- bunobee-0.0.4/tests/test_ssp_prior.py +86 -0
- bunobee-0.0.4/tests/test_transforms.py +744 -0
bunobee-0.0.4/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Edwin Ng
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
bunobee-0.0.4/PKG-INFO
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bunobee
|
|
3
|
+
Version: 0.0.4
|
|
4
|
+
Summary: The time-series engine for forecasting with JAX and NumPyro
|
|
5
|
+
Author-email: Edwin Ng <edwinnglabs@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/edwinnglabs/bunobee
|
|
8
|
+
Project-URL: Repository, https://github.com/edwinnglabs/bunobee
|
|
9
|
+
Project-URL: Issues, https://github.com/edwinnglabs/bunobee/issues
|
|
10
|
+
Project-URL: Documentation, https://github.com/edwinnglabs/bunobee#readme
|
|
11
|
+
Keywords: time-series,forecasting,bayesian,jax,numpyro,kalman-filter
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: numpyro>=0.20.0
|
|
22
|
+
Requires-Dist: jax<0.10.0,>=0.8.0
|
|
23
|
+
Requires-Dist: pandas>=2.0
|
|
24
|
+
Requires-Dist: arviz
|
|
25
|
+
Requires-Dist: xarray
|
|
26
|
+
Provides-Extra: test
|
|
27
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
28
|
+
Requires-Dist: black==26.1.0; extra == "test"
|
|
29
|
+
Provides-Extra: notebook
|
|
30
|
+
Requires-Dist: ipywidgets; extra == "notebook"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
<div align="center">
|
|
34
|
+
<img src="./bunobee.png" alt="Bunobee Logo" width="320" />
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
# Bunobee
|
|
38
|
+
|
|
39
|
+
Bunobee is an experimental Bayesian time-series library built on [JAX](https://github.com/google/jax) and [NumPyro](https://github.com/pyro-ppl/numpyro). It currently supports the following model families:
|
|
40
|
+
|
|
41
|
+
- **Damped Local Trend (DLT)** — an exponential smoothing model with a damped slope component
|
|
42
|
+
- **State Space Models (SSP)** — structured state space models with Kalman filtering and smoothing
|
|
43
|
+
|
|
44
|
+
**Fun fact:** "bunobee" is a word invented by my kids during their childhood to describe the action of the role they were playing.
|
|
45
|
+
|
bunobee-0.0.4/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="./bunobee.png" alt="Bunobee Logo" width="320" />
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
# Bunobee
|
|
6
|
+
|
|
7
|
+
Bunobee is an experimental Bayesian time-series library built on [JAX](https://github.com/google/jax) and [NumPyro](https://github.com/pyro-ppl/numpyro). It currently supports the following model families:
|
|
8
|
+
|
|
9
|
+
- **Damped Local Trend (DLT)** — an exponential smoothing model with a damped slope component
|
|
10
|
+
- **State Space Models (SSP)** — structured state space models with Kalman filtering and smoothing
|
|
11
|
+
|
|
12
|
+
**Fun fact:** "bunobee" is a word invented by my kids during their childhood to describe the action of the role they were playing.
|
|
13
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "bunobee"
|
|
3
|
+
version = "0.0.4"
|
|
4
|
+
description = "The time-series engine for forecasting with JAX and NumPyro"
|
|
5
|
+
authors = [{name = "Edwin Ng", email = "edwinnglabs@gmail.com"}]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
license = "MIT"
|
|
9
|
+
license-files = ["LICENSE"]
|
|
10
|
+
keywords = ["time-series", "forecasting", "bayesian", "jax", "numpyro", "kalman-filter"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"Intended Audience :: Science/Research",
|
|
14
|
+
"Programming Language :: Python :: 3.11",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Programming Language :: Python :: 3.13",
|
|
17
|
+
"Topic :: Scientific/Engineering",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"numpyro>=0.20.0",
|
|
21
|
+
"jax>=0.8.0,<0.10.0",
|
|
22
|
+
"pandas>=2.0",
|
|
23
|
+
"arviz",
|
|
24
|
+
"xarray",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
test = [
|
|
29
|
+
"pytest>=8",
|
|
30
|
+
"black==26.1.0",
|
|
31
|
+
]
|
|
32
|
+
notebook = [
|
|
33
|
+
"ipywidgets",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://github.com/edwinnglabs/bunobee"
|
|
38
|
+
Repository = "https://github.com/edwinnglabs/bunobee"
|
|
39
|
+
Issues = "https://github.com/edwinnglabs/bunobee/issues"
|
|
40
|
+
Documentation = "https://github.com/edwinnglabs/bunobee#readme"
|
|
41
|
+
|
|
42
|
+
[build-system]
|
|
43
|
+
requires = ["setuptools"]
|
|
44
|
+
build-backend = "setuptools.build_meta"
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.packages.find]
|
|
47
|
+
where = ["src"]
|
|
48
|
+
|
|
49
|
+
[tool.black]
|
|
50
|
+
line-length = 120
|
|
51
|
+
target-version = ['py311']
|
|
52
|
+
include = '\.pyi?$'
|
|
53
|
+
extend-exclude = ""
|
|
54
|
+
|
|
55
|
+
[tool.pytest.ini_options]
|
|
56
|
+
log_cli = true
|
|
57
|
+
log_cli_level = "INFO"
|
bunobee-0.0.4/setup.cfg
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
|
+
|