Simplify setup.py, make CmdStan the default backend (#2088)

This commit is contained in:
Brian Ward 2022-04-04 10:06:59 -04:00 committed by GitHub
parent f16c782661
commit 9968f8be41
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 88 additions and 386 deletions

View file

@ -6,9 +6,6 @@ on:
pull_request:
branches: [ main ]
env:
CMDSTAN_VERSION: "2.26.1"
jobs:
build-and-test-python:
@ -38,26 +35,11 @@ jobs:
key: ${{ runner.os }}-pip-${{ hashFiles('**/python/requirements.txt') }}-v1
restore-keys: |
${{ runner.os }}-pip-
- name: "Restore cmdstan cache"
id: cache-cmdstan
uses: actions/cache@v2
with:
path: ${{ env.DEFAULT_HOME }}/.cmdstan
key: ${{ runner.os }}-cmdstan-${{ env.CMDSTAN_VERSION }}-v1
- name: "Download cmdstan"
if: steps.cache-cmdstan.outputs.cache-hit != 'true'
run: |
wget https://github.com/stan-dev/cmdstan/releases/download/v${{ env.CMDSTAN_VERSION }}/cmdstan-${{ env.CMDSTAN_VERSION }}.tar.gz -O /tmp/cmdstan.tar.gz &> /dev/null
mkdir $HOME/.cmdstan
tar -xf /tmp/cmdstan.tar.gz -C $HOME/.cmdstan &> /dev/null
- name: Install and test
run: |
pip install -U -r python/requirements.txt dask[dataframe] distributed
cd python
STAN_BACKEND=PYSTAN python setup.py develop test
python setup.py clean
rm -rf prophet/stan_model
STAN_BACKEND=CMDSTANPY python setup.py develop test
python setup.py develop test
build-and-test-r:

View file

@ -5,9 +5,9 @@ on:
types: [ created ]
workflow_dispatch: {}
env:
STAN_BACKEND: "PYSTAN,CMDSTANPY"
CMDSTAN_VERSION: "2.26.1"
STAN_BACKEND: "CMDSTANPY"
jobs:
make-wheel-windows:
@ -55,13 +55,6 @@ jobs:
python -m pip install --upgrade pip
python -m pip install cibuildwheel build delvewheel
- name: "Restore cmdstan cache"
id: cache-cmdstan
uses: actions/cache@v2
with:
path: ${{ env.DEFAULT_HOME }}/.cmdstan
key: ${{ runner.os }}-cmdstan-${{ env.CMDSTAN_VERSION }}-v1
- name: "Restore RTools40"
id: cache-rtools
uses: actions/cache@v2
@ -69,21 +62,12 @@ jobs:
path: C:/rtools40
key: ${{ runner.os }}-${{ env.OS_VERSION }}-rtools-v1
- name: "Download cmdstan"
if: steps.cache-cmdstan.outputs.cache-hit != 'true'
run: |
$ProgressPreference = "SilentlyContinue"
Invoke-WebRequest https://github.com/stan-dev/cmdstan/releases/download/v${{ env.CMDSTAN_VERSION }}/cmdstan-${{ env.CMDSTAN_VERSION }}.tar.gz -OutFile "D:/a/_temp/cmdstan.tar.gz"
New-Item -Path "$HOME" -Name ".cmdstan" -ItemType "directory"
tar -xf "D:/a/_temp/cmdstan.tar.gz" -C "$HOME/.cmdstan"
- name: "Build wheel"
run: |
cd python && python -m cibuildwheel --output-dir wheelhouse
env:
CIBW_ENVIRONMENT: >
STAN_BACKEND="${{ env.STAN_BACKEND }}"
CMDSTAN_VERSION=${{ env.CMDSTAN_VERSION }}
PIP_CACHE_DIR="${{ env.PIP_DEFAULT_CACHE }}"
CIBW_BUILD: cp36-* cp37-* cp38-*
CIBW_ARCHS: native
@ -112,17 +96,16 @@ jobs:
- name: "Get OS version (Linux)"
if: startsWith(runner.os, 'Linux')
run: |
echo "OS_VERSION=`lsb_release -sr`" >> $GITHUB_ENV
echo "PIP_DEFAULT_CACHE=$HOME/.cache/pip" >> $GITHUB_ENV
echo "DEFAULT_HOME=$HOME" >> $GITHUB_ENV
- name: "Get OS version (macOS)"
if: startsWith(runner.os, 'macOS')
run: |
echo "OS_VERSION=`sw_vers -productVersion`" >> $GITHUB_ENV
echo "PIP_DEFAULT_CACHE=$HOME/Library/Caches/pip" >> $GITHUB_ENV
echo "DEFAULT_HOME=$HOME" >> $GITHUB_ENV
- name: "Checkout repo"
uses: actions/checkout@v2
@ -146,31 +129,15 @@ jobs:
python -m pip install --upgrade pip
python -m pip install cibuildwheel build
- name: "Restore cmdstan cache"
id: cache-cmdstan
uses: actions/cache@v2
with:
path: ${{ env.DEFAULT_HOME }}/.cmdstan
key: ${{ runner.os }}-cmdstan-${{ env.CMDSTAN_VERSION }}-v1
- name: "Download cmdstan"
if: steps.cache-cmdstan.outputs.cache-hit != 'true'
run: |
wget https://github.com/stan-dev/cmdstan/releases/download/v${{ env.CMDSTAN_VERSION }}/cmdstan-${{ env.CMDSTAN_VERSION }}.tar.gz -O /tmp/cmdstan.tar.gz &> /dev/null
mkdir $HOME/.cmdstan
tar -xf /tmp/cmdstan.tar.gz -C $HOME/.cmdstan &> /dev/null
- name: "Build wheel"
run: |
cd python && python -m cibuildwheel --output-dir wheelhouse
env:
CIBW_ENVIRONMENT: >
STAN_BACKEND="${{ env.STAN_BACKEND }}"
CMDSTAN_VERSION=${{ env.CMDSTAN_VERSION }}
# Linux builds run in a Docker container, need to point the cache to the host machine.
CIBW_ENVIRONMENT_LINUX: >
STAN_BACKEND="${{ env.STAN_BACKEND }}"
CMDSTAN_VERSION=${{ env.CMDSTAN_VERSION }}
HOME="/host/${{ env.DEFAULT_HOME }}"
PIP_CACHE_DIR="/host/${{ env.PIP_DEFAULT_CACHE }}"
CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014

View file

@ -56,7 +56,7 @@ On Windows, R requires a compiler so you'll need to [follow the instructions](ht
If you have custom Stan compiler settings, install from source rather than the CRAN binary.
## Installation in Python
## Installation in Python - PyPI release
Prophet is on PyPI, so you can use `pip` to install it. From v0.6 onwards, Python 2 is no longer supported. As of v1.0, the package name on PyPI is "prophet"; prior to v1.0 it was "fbprophet".
@ -64,12 +64,9 @@ Prophet is on PyPI, so you can use `pip` to install it. From v0.6 onwards, Pytho
# Install pystan with pip before using pip to install prophet
# pystan>=3.0 is currently not supported
pip install pystan==2.19.1.1
pip install prophet
```
The default dependency that Prophet has is `pystan`. PyStan has its own [installation instructions](http://pystan.readthedocs.io/en/latest/installation_beginner.html). Install pystan with pip before using pip to install prophet.
#### Experimental backend - cmdstanpy
You can also choose a (more experimental) alternative stan backend called `cmdstanpy`. It requires the [CmdStan](https://mc-stan.org/users/interfaces/cmdstan) command line interface and you will have to specify the environment variable `STAN_BACKEND` pointing to it, for example:
@ -90,20 +87,41 @@ $ CMDSTAN=/tmp/cmdstan-2.22.1 STAN_BACKEND=PYSTAN,CMDSTANPY pip install prophet
After installation, you can [get started!](https://facebook.github.io/prophet/docs/quick_start.html#python-api)
If you upgrade the version of PyStan installed on your system, you may need to reinstall prophet ([see here](https://github.com/facebook/prophet/issues/324)).
If you upgraded the version of PyStan installed on your system, you may need to reinstall prophet ([see here](https://github.com/facebook/prophet/issues/324)).
### Anaconda
Use `conda install gcc` to set up gcc. The easiest way to install Prophet is through conda-forge: `conda install -c conda-forge prophet`.
The easiest way to install Prophet is through conda-forge: `conda install -c conda-forge prophet`.
### Windows
On Windows, PyStan requires a compiler so you'll need to [follow the instructions](https://pystan2.readthedocs.io/en/latest/windows.html). The easiest way to install Prophet in Windows is in Anaconda.
The easiest way to install Prophet in Windows is in Anaconda.
### Linux
Make sure compilers (gcc, g++, build-essential) and Python development tools (python-dev, python3-dev) are installed. In Red Hat systems, install the packages gcc64 and gcc64-c++. If you are using a VM, be aware that you will need at least 4GB of memory to install prophet, and at least 2GB of memory to use prophet.
## Installation in Python - Development version
Since Pystan2 is no longer being maintained, the python package will move to depend solely on `cmdstanpy` (benefits described [here](https://github.com/facebook/prophet/issues/2041)). This has been updated in the development version of the package (1.1), but this version hasn't yet been released to PyPI. If you would like to use `cmdstanpy` only for your workflow, you can clone this repo and build from source manually:
```bash
git clone https://github.com/facebook/prophet.git
cd prophet/python
python -m install -r requirements.txt
python setup.py develop
```
By default, Prophet will use a fixed version of `cmdstan` (downloading and installing it if necessary) to compile the model executables. If this is undesired and you would like to use your own existing `cmdstan` installation, you can set the environment variable `PROPHET_REPACKAGE_CMDSTAN` to `False`:
```bash
export PROPHET_REPACKAGE_CMDSTAN=False; python setup.py develop
```
### Windows
Using `cmdstanpy` with Windows requires a Unix-compatible C compiler such as mingw-gcc. If cmdstanpy is installed first, one can be installed via the `cmdstanpy.install_cxx_toolchain` command.
## Changelog
### Version 1.0 (2021.03.28)

View file

@ -1,5 +1,4 @@
include stan/unix/*.stan
include stan/win/*.stan
include stan/*.stan
include LICENSE
include requirements.txt

View file

@ -18,18 +18,17 @@ Full documentation and examples available at the homepage: https://facebook.gith
- Rob Hyndman's [forecast package](http://robjhyndman.com/software/forecast/)
- [Statsmodels](http://statsmodels.sourceforge.net/)
## Installation
## Installation - PyPI release
```shell
pip install prophet
```
Note: Installation requires PyStan, which has its [own installation instructions](http://pystan.readthedocs.io/en/latest/installation_beginner.html).
On Windows, PyStan requires a compiler so you'll need to [follow the instructions](http://pystan.readthedocs.io/en/latest/windows.html).
The key step is installing a recent [C++ compiler](https://visualstudio.microsoft.com/visual-cpp-build-tools/)
See [Installation in Python - PyPI release](https://github.com/facebook/prophet#installation-in-python---pypi-release)
## Installation using Docker and docker-compose (via Makefile)
## Installation - Development version
Simply type `make build` and if everything is fine you should be able to `make shell` or alternative jump directly to `make py-shell`.
See [Installation in Python - Development version](https://github.com/facebook/prophet#installation-in-python---development-version)
### Installation using Docker and docker-compose (via Makefile)
Simply type `make build` and if everything is fine you should be able to `make shell` or alternative jump directly to `make py-shell`.
To run the tests, inside the container `cd python/prophet` and then `python -m unittest`
@ -41,4 +40,4 @@ To run the tests, inside the container `cd python/prophet` and then `python -m u
>>> m.fit(df) # df is a pandas.DataFrame with 'y' and 'ds' columns
>>> future = m.make_future_dataframe(periods=365)
>>> m.predict(future)
```
```

View file

@ -6,6 +6,7 @@
from __future__ import absolute_import, division, print_function
from abc import abstractmethod, ABC
from tempfile import mkdtemp
from typing import Tuple
from collections import OrderedDict
from enum import Enum
@ -61,30 +62,21 @@ class IStanBackend(ABC):
class CmdStanPyBackend(IStanBackend):
CMDSTAN_VERSION = "2.26.1"
def __init__(self):
super().__init__()
import cmdstanpy
cmdstanpy.set_cmdstan_path(
pkg_resources.resource_filename("prophet", f"stan_model/cmdstan-{self.CMDSTAN_VERSION}")
# this must be set before super.__init__() for load_model to work on Windows
local_cmdstan = pkg_resources.resource_filename(
"prophet", f"stan_model/cmdstan-{self.CMDSTAN_VERSION}"
)
if Path(local_cmdstan).exists():
cmdstanpy.set_cmdstan_path(local_cmdstan)
super().__init__()
@staticmethod
def get_type():
return StanBackendEnum.CMDSTANPY.name
def _add_tbb_to_path(self):
"""Add the TBB library to $PATH on Windows only. Required for loading model binaries."""
if PLATFORM == "win":
tbb_path = pkg_resources.resource_filename(
"prophet",
f"stan_model/cmdstan-{self.CMDSTAN_VERSION}/stan/lib/stan_math/lib/tbb"
)
os.environ["PATH"] = ";".join(
list(OrderedDict.fromkeys([tbb_path] + os.environ.get("PATH", "").split(";")))
)
def load_model(self):
import cmdstanpy
self._add_tbb_to_path()
model_file = pkg_resources.resource_filename(
'prophet',
'stan_model/prophet_model.bin',
@ -102,6 +94,7 @@ class CmdStanPyBackend(IStanBackend):
inits=stan_init,
algorithm='Newton' if stan_data['T'] < 100 else 'LBFGS',
iter=int(1e4),
output_dir = mkdtemp(),
)
args.update(kwargs)

View file

@ -168,7 +168,7 @@ class TestDiagnostics(TestCase):
period='10 days',
cutoffs=[pd.Timestamp('2012-07-31'), pd.Timestamp('2012-08-31')])
self.assertEqual(len(df_cv1['cutoff'].unique()), 2)
def test_cross_validation_uncertainty_disabled(self):
df = self.__df.copy()
for uncertainty in [0, False]:

View file

@ -2,7 +2,6 @@
requires = [
"setuptools>=42",
"wheel",
"pystan~=2.19.1.1",
"cmdstanpy==0.9.77",
"cmdstanpy>=1.0.0",
]
build-backend = "setuptools.build_meta"

View file

@ -1,5 +1,5 @@
Cython>=0.22
cmdstanpy==0.9.77
cmdstanpy>=1.0.0
pystan~=2.19.1.1
numpy>=1.15.4
pandas>=1.0.4

View file

@ -4,11 +4,8 @@
# LICENSE file in the root directory of this source tree.
import os
import pickle
import platform
import subprocess
import sys
from collections import OrderedDict
import platform
from pathlib import Path
from shutil import copy, copytree, rmtree
from typing import List
@ -20,18 +17,10 @@ from setuptools.command.build_py import build_py
from setuptools.command.develop import develop
from setuptools.command.test import test as test_command
PLATFORM = "unix"
if platform.platform().startswith("Win"):
PLATFORM = "win"
MODEL_DIR = os.path.join("stan", PLATFORM)
MODEL_DIR = "stan"
MODEL_TARGET_DIR = os.path.join("prophet", "stan_model")
# TODO: Remove when upgrading to cmdstanpy 1.0, use cmdstanpy internals instead
# cmdstan utils
MAKE = os.getenv("MAKE", "make" if PLATFORM != "win" else "mingw32-make")
EXTENSION = ".exe" if PLATFORM == "win" else ""
CMDSTAN_VERSION = "2.26.1"
BINARIES_DIR = "bin"
BINARIES = ["diagnose", "print", "stanc", "stansummary"]
@ -39,72 +28,6 @@ TBB_PARENT = "stan/lib/stan_math/lib"
TBB_DIRS = ["tbb", "tbb_2019_U8"]
# TODO: Remove when upgrading to cmdstanpy 1.0, use cmdstanpy internals instead
def clean_all_cmdstan(verbose: bool = False) -> None:
"""Run `make clean-all` in the current directory (must be a cmdstan library).
Parameters
----------
verbose: when ``True``, print build msgs to stdout.
"""
cmd = [MAKE, "clean-all"]
proc = subprocess.Popen(
cmd,
cwd=None,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=os.environ,
)
while proc.poll() is None:
if proc.stdout:
output = proc.stdout.readline().decode("utf-8").strip()
if verbose and output:
print(output, flush=True)
_, stderr = proc.communicate()
if proc.returncode:
msgs = ['Command "make clean-all" failed']
if stderr:
msgs.append(stderr.decode("utf-8").strip())
raise RuntimeError("\n".join(msgs))
# TODO: Remove when upgrading to cmdstanpy 1.0, use cmdstanpy internals instead
def build_cmdstan(verbose: bool = False) -> None:
"""Run `make build` in the current directory (must be a cmdstan library).
Parameters
----------
verbose: when ``True``, print build msgs to stdout.
"""
cmd = [MAKE, "build"]
proc = subprocess.Popen(
cmd,
cwd=None,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env=os.environ,
)
while proc.poll() is None:
if proc.stdout:
output = proc.stdout.readline().decode("utf-8").strip()
if verbose and output:
print(output, flush=True)
_, stderr = proc.communicate()
if proc.returncode:
msgs = ['Command "make build" failed']
if stderr:
msgs.append(stderr.decode("utf-8").strip())
raise RuntimeError("\n".join(msgs))
# Add tbb to the $PATH on Windows
if PLATFORM == "win":
libtbb = os.path.join(os.getcwd(), "stan", "lib", "stan_math", "lib", "tbb")
os.environ["PATH"] = ";".join(
list(OrderedDict.fromkeys([libtbb] + os.environ.get("PATH", "").split(";")))
)
def prune_cmdstan(cmdstan_dir: str) -> None:
"""
Keep only the cmdstan executables and tbb files (minimum required to run a cmdstanpy commands on a pre-compiled model).
@ -116,6 +39,7 @@ def prune_cmdstan(cmdstan_dir: str) -> None:
rmtree(temp_dir)
temp_dir.mkdir()
print("Copying ", original_dir, " to ", temp_dir, " for pruning")
copytree(original_dir / BINARIES_DIR, temp_dir / BINARIES_DIR)
for f in (temp_dir / BINARIES_DIR).iterdir():
if f.is_dir():
@ -128,51 +52,42 @@ def prune_cmdstan(cmdstan_dir: str) -> None:
rmtree(original_dir)
temp_dir.rename(original_dir)
def get_cmdstan_cache() -> str:
"""Default directory for an existing cmdstan library. Prevents unnecessary re-downloads of cmdstan."""
return Path.home().resolve() / ".cmdstan" / f"cmdstan-{CMDSTAN_VERSION}"
def repackage_cmdstan():
return os.environ.get("PROPHET_REPACKAGE_CMDSTAN", "").lower() not in ["false", "0"]
def download_cmdstan(cache_dir: Path) -> None:
"""Ensure the cmdstan library exists in the cache directory."""
import cmdstanpy
from cmdstanpy.install_cmdstan import retrieve_version
if cache_dir.is_dir():
print(f"Found existing cmdstan library at {cache_dir}")
else:
print(f"Downloading cmdstan to {cache_dir}")
cache_dir.parent.mkdir(parents=True, exist_ok=True)
with cmdstanpy.utils.pushd(cache_dir.parent):
retrieve_version(version=CMDSTAN_VERSION, progress=False)
def install_cmdstan_toolchain():
def maybe_install_cmdstan_toolchain():
"""Install C++ compilers required to build stan models on Windows machines."""
import cmdstanpy
from cmdstanpy.install_cxx_toolchain import main as _install_cxx_toolchain
_install_cxx_toolchain()
try:
cmdstanpy.utils.cxx_toolchain_path()
except Exception:
_install_cxx_toolchain({"version": None, "dir": None, "verbose": True})
cmdstanpy.utils.cxx_toolchain_path()
def install_cmdstan_deps(cmdstan_dir: Path):
import cmdstanpy
from multiprocessing import cpu_count
cmdstan_cache = get_cmdstan_cache()
download_cmdstan(cmdstan_cache)
if cmdstan_dir.is_dir():
rmtree(cmdstan_dir)
copytree(cmdstan_cache, cmdstan_dir)
if repackage_cmdstan():
if platform.platform().startswith("Win"):
maybe_install_cmdstan_toolchain()
print("Installing cmdstan to", cmdstan_dir)
if os.path.isdir(cmdstan_dir):
rmtree(cmdstan_dir)
if PLATFORM == "win":
try:
cmdstanpy.utils.cxx_toolchain_path()
except Exception:
install_cmdstan_toolchain()
with cmdstanpy.utils.pushd(cmdstan_dir):
clean_all_cmdstan()
build_cmdstan()
cmdstanpy.set_cmdstan_path(str(cmdstan_dir))
if not cmdstanpy.install_cmdstan(
version=CMDSTAN_VERSION,
dir=cmdstan_dir.parent,
overwrite=True,
verbose=True,
cores=cpu_count(),
progress=True,
):
raise RuntimeError("CmdStan failed to install in repackaged directory")
def build_cmdstan_model(target_dir):
@ -186,17 +101,21 @@ def build_cmdstan_model(target_dir):
target_dir: Directory to copy the compiled model executable and core cmdstan files to.
"""
import cmdstanpy
cmdstan_dir = (Path(target_dir) / f"cmdstan-{CMDSTAN_VERSION}").resolve()
install_cmdstan_deps(cmdstan_dir)
model_name = "prophet.stan"
target_name = "prophet_model.bin"
sm = cmdstanpy.CmdStanModel(stan_file=os.path.join(MODEL_DIR, model_name))
copy(sm.exe_file, os.path.join(target_dir, target_name))
# Clean up
for f in Path(MODEL_DIR).iterdir():
if f.is_file() and f.name != model_name:
os.remove(f)
prune_cmdstan(cmdstan_dir)
if repackage_cmdstan():
prune_cmdstan(cmdstan_dir)
def build_pystan_model(target_dir):
@ -204,6 +123,7 @@ def build_pystan_model(target_dir):
Compile the stan model using pystan and pickle it. The pickle is copied to {target_dir}/prophet_model.pkl.
"""
import pystan
import pickle
model_name = "prophet.stan"
target_name = "prophet_model.pkl"
@ -215,7 +135,7 @@ def build_pystan_model(target_dir):
def get_backends_from_env() -> List[str]:
return os.environ.get("STAN_BACKEND", "PYSTAN").split(",")
return os.environ.get("STAN_BACKEND", "CMDSTANPY").split(",")
def build_models(target_dir):
@ -322,7 +242,7 @@ with open("requirements.txt", "r") as f:
setup(
name="prophet",
version="1.0.1",
version="1.1",
description="Automatic Forecasting Procedure",
url="https://facebook.github.io/prophet/",
author="Sean J. Taylor <sjtz@pm.me>, Ben Letham <bletham@fb.com>",
@ -330,7 +250,7 @@ setup(
license="MIT",
packages=find_packages(),
install_requires=install_requires,
python_requires=">=3",
python_requires=">=3.6",
zip_safe=False,
include_package_data=True,
ext_modules=[Extension("prophet.stan_model", [])],

View file

@ -1,175 +0,0 @@
// Copyright (c) Facebook, Inc. and its affiliates.
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.
functions {
real[ , ] get_changepoint_matrix(real[] t, real[] t_change, int T, int S) {
// Assumes t and t_change are sorted.
real A[T, S];
real a_row[S];
int cp_idx;
// Start with an empty matrix.
A = rep_array(0, T, S);
a_row = rep_array(0, S);
cp_idx = 1;
// Fill in each row of A.
for (i in 1:T) {
while ((cp_idx <= S) && (t[i] >= t_change[cp_idx])) {
a_row[cp_idx] = 1;
cp_idx = cp_idx + 1;
}
A[i] = a_row;
}
return A;
}
// Logistic trend functions
real[] logistic_gamma(real k, real m, real[] delta, real[] t_change, int S) {
real gamma[S]; // adjusted offsets, for piecewise continuity
real k_s[S + 1]; // actual rate in each segment
real m_pr;
// Compute the rate in each segment
k_s[1] = k;
for (i in 1:S) {
k_s[i + 1] = k_s[i] + delta[i];
}
// Piecewise offsets
m_pr = m; // The offset in the previous segment
for (i in 1:S) {
gamma[i] = (t_change[i] - m_pr) * (1 - k_s[i] / k_s[i + 1]);
m_pr = m_pr + gamma[i]; // update for the next segment
}
return gamma;
}
real[] logistic_trend(
real k,
real m,
real[] delta,
real[] t,
real[] cap,
real[ , ] A,
real[] t_change,
int S,
int T
) {
real gamma[S];
real Y[T];
gamma = logistic_gamma(k, m, delta, t_change, S);
for (i in 1:T) {
Y[i] = cap[i] / (1 + exp(-(k + dot_product(A[i], delta))
* (t[i] - (m + dot_product(A[i], gamma)))));
}
return Y;
}
// Linear trend function
real[] linear_trend(
real k,
real m,
real[] delta,
real[] t,
real[ , ] A,
real[] t_change,
int S,
int T
) {
real gamma[S];
real Y[T];
for (i in 1:S) {
gamma[i] = -t_change[i] * delta[i];
}
for (i in 1:T) {
Y[i] = (k + dot_product(A[i], delta)) * t[i] + (
m + dot_product(A[i], gamma));
}
return Y;
}
// Flat trend function
real[] flat_trend(
real m,
int T
) {
return rep_array(m, T);
}
}
data {
int T; // Number of time periods
int<lower=1> K; // Number of regressors
real t[T]; // Time
real cap[T]; // Capacities for logistic trend
real y[T]; // Time series
int S; // Number of changepoints
real t_change[S]; // Times of trend changepoints
real X[T,K]; // Regressors
vector[K] sigmas; // Scale on seasonality prior
real<lower=0> tau; // Scale on changepoints prior
int trend_indicator; // 0 for linear, 1 for logistic, 2 for flat
real s_a[K]; // Indicator of additive features
real s_m[K]; // Indicator of multiplicative features
}
transformed data {
real A[T, S];
A = get_changepoint_matrix(t, t_change, T, S);
}
parameters {
real k; // Base trend growth rate
real m; // Trend offset
real delta[S]; // Trend rate adjustments
real<lower=0> sigma_obs; // Observation noise
real beta[K]; // Regressor coefficients
}
transformed parameters {
real trend[T];
real Y[T];
real beta_m[K];
real beta_a[K];
if (trend_indicator == 0) {
trend = linear_trend(k, m, delta, t, A, t_change, S, T);
} else if (trend_indicator == 1) {
trend = logistic_trend(k, m, delta, t, cap, A, t_change, S, T);
} else if (trend_indicator == 2){
trend = flat_trend(m, T);
}
for (i in 1:K) {
beta_m[i] = beta[i] * s_m[i];
beta_a[i] = beta[i] * s_a[i];
}
for (i in 1:T) {
Y[i] = (
trend[i] * (1 + dot_product(X[i], beta_m)) + dot_product(X[i], beta_a)
);
}
}
model {
//priors
k ~ normal(0, 5);
m ~ normal(0, 5);
delta ~ double_exponential(0, tau);
sigma_obs ~ normal(0, 0.5);
beta ~ normal(0, sigmas);
// Likelihood
y ~ normal(Y, sigma_obs);
}