-
Notifications
You must be signed in to change notification settings - Fork 75
Implement Bayesian Structural Time Series (BSTS) #473
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
base: main
Are you sure you want to change the base?
Conversation
Check out this pull request on See visual diffs & provide feedback on Jupyter Notebooks. Powered by ReviewNB |
Thinking to bring HSGP to here, and be able to replicate the second example. |
WOAH! Ultra quick review / questions until I've got some more time:
Sorry for the relatively superficial review at this point - family weekend time. Will enjoy diving into the details soon. |
Hey @drbenvincent Still BSTS, the Structural time series (STS) models are a family of probability models for time series that includes and generalizes many standard time-series modeling ideas, including:
Definitely, it's handy and popular to talk about state-space, but my understanding is that BSTS in the broad sense does not require a true state‐space; it merely requires a Bayesian, additive decomposition into trend, seasonality, and optional regressors. Unless you insist on the classic DLM/state‐space definition from a Kalman‐Filter sense. In the Google blog the first model its very similar to this, the only difference is that the slope from the linear trend is "evolving slowly over time following a random walk". Thanks to the HSGP class, we can add something similar if we want to. |
Happy to do it, my only opinion is that the API for the new class is quite different and allow for arbitrary components for trend and seasonality. IF they follow pymc-marketing protocols. I'm doing that because will help me to easily input HSGP and replace this fourier bases approach (just as example), for the same structural time series. Could probably help to just replace by the state-space. I'll be working on that as well :) I feel should be easy to do. |
Almost the same. The bsts fits more during training (high R2) and has less variance (lower sd). |
Nice. So I am a beginner in time series modelling - which is why I've not given this a go yet. But that's a good clarification BSTS != state space. Do you think your implementation leaves the door open for extension into autoregressive components etc? |
Indeed, I'll try to bring an example tomorrow! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Firstly, this is very cool! Will probably need a few rounds of review to get this merged, but it will happen :)
Let's try to make it fit into the current docs and codebase as seamlessly as possible. So far we've got a nice distinction between "experiments" and "models".
The new model class BayesianStructuralTimeSeries
fits in with the current structure very nicely. Given the range of experiments we've got so far the obvious candidate is to work with the interrupted time series experiment, and maybe multi-observation differences in differences. So the natural way to make this work would be to make the InterruptedTimeSeries
class accept your new BayesianStructuralTimeSeries
class. That would be in an ideal world, but let's see what we can do quickly to move in that direction.
The API differences between InterruptedTimeSeries
and StructuredTimeSeries
are minimal. Although obviously what is done with the provided data diverges a lot. Trying to resolve this in one go might be a bit much, but at the moment I've got these concrete suggestions:
- Move the new
StructuredTimeSeries
class intointerrupted_time_series.py
and deletestructured_time_series.py
. - I relatively strongly think we should avoid the duplication of the plotting code. Like I say, we're trying to make the interrupted time series experiment work with a BSTS model, I don't think there is any reason why any custom plot logic is needed. We could extract the plot code into a mixin class (called something like
ITSPlotter
) which can be injected into bothStructuredTimeSeries
andInterruptedTimeSeries
.
I think this already goes a long way towards keeping the nice distinction between experiment classes and model classes. The PR is already not far off that - the main issue I see is that StructuredTimeSeries
is currently treated like a new experiment when in fact it's exactly the same as interrupted time series, but just separate in order to handle the different model input.
The new notebook is clearly applied to the interrupted time series experiment, so I think the notebook contents can be appended to the existing its_pymc.ipynb
. That could be quite nice - the notebook would then be a kind of "how to do ITS from simple linear model to proper time series modeling".
This looks really cool and useful!! |
Thanks @AlexAndorra - this is the direction I was hoping we'd go in. I've done some basic experimentation with it, but I'm a time series newbie and am not confident enough to know when I need to use what component, how to deal with non-stationarity etc. There's a lot more lower hanging fruit for me to work on in CausalPy, so I'm happy that this has been dropped on my lap by @cetagostini :) I have a very crude understanding of how BSTS and state space approaches relate to each other (though @cetagostini explained it a bit above). Would it make sense to end up having both this kind of BSTS model class and (eventually) the |
@cetagostini The I don't think having a fixed out of the box BSTS model is bad, and in some ways it is good because it is what it is and doesn't require a user to meddle much. That said, it could be pretty cool to think about an API where users can modularly build up a pymc model to pass into the experiment class. Though I imagine this would probably be a considerable bit more work? |
Yeah, the |
I'll take a look here! @AlexAndorra Thanks for jumping!
I'm already thinking no this, thats why I left the parameters
Thats cool, and seeing signatures, I feel could be. But not sure if for a first iteration? My guess, better to make a class that can manage different stuff like non state-spaces, and state spaces type of models, then we can move forward in order PR and say, lets bring any pymc time series model, and thats it. What do you think @drbenvincent ? |
Sure @cetagostini I'm happy with an iterative approach. Happy to proceed along the lines in my first review. |
Apply the changes:
|
@drbenvincent Implementing the StateSpace it's taking more effort than expected, will continue during week... But I think, it requires probably a following PR to not make this massive, instead of all in one.. Not sure, take a look and give me feedback. I already add a class draft in the notebook but need more work to be able to be used by the new experiment STS class. |
integration with state space ready, took more than expected because the state-spaces needed a few wrappers.... Can you modify and give me hand with the docs? All in my local looks okay, not sure whats missing here! Currently:
Important We are using y_hat and mu in different places, I think, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @cetagostini.
I can see that we've got 2 new examples in the its_pymc.ipynb
notebook. That looks cool.
Though it's making me think that we should make the synthetic data a bit more complex - at the moment it's just got a simple linear trend and annual seasonality. We should find some classic time series dataset suitable for interrupted time series. That way, the new stuff you've done should be better than the pre-existing y ~ 1 + t + C(month)
linear model, which would better sell the whole thing :)
I'm also seeing the state space model fit doesn't look too great. Maybe we could work on that, possibly getting some input from the state space guru himself?
The changes to the experiment classes isn't quite what I was thinking - though it's close. I was hoping to keep the class name InterruptedTimeSeries
because it's most obviously the name of what we're doing in terms of a quasi experiment. Can we move the entire contents of structural_time_series.py
and put it in interrupted_time_series.py
, and use the InterruptedTimeSeries
name for the experiment class rather than StructuralTimeSeries
? Happy to jump on a call about it.
In terms of plots, it maybe depends. But for causal impact stuff we want mu
. A frequentist approach focusses on the model expectation (mu
), so in the Bayesian case we just have a posterior distribution over that expectation.
With the state space stuff, I see you've got some checks for the presence of pymc-extras
. Maybe we should add some info in the ITS docs notebook to tell the reader to install it, maybe pointing them to the pymc-extras
install instructions?
Though this is looking much closer to done now :)
I'll take a look at the conflicts when I'm back from a work trip. Just wanted to keep up the momentum but I know you've got other demands on your time. |
PR Description:
This PR introduces Bayesian Structural Time Series (BSTS) modeling to CausalPy, enabling advanced time series analysis, forecasting, and causal impact estimation. Inspired by principles from structural time series modeling basic with only trend/seasonal components (e.g., TensorFlow Probability's STS first example), this work decomposes time series into trend, seasonality, and optional regressor components within a Bayesian framework.
Key Changes:
BayesianStructuralTimeSeries
Model (causalpy/pymc_models.py
):pymc-marketing
components (LinearTrend
,YearlyFourier
), supports custom components, and optional exogenous regressors (X
)._data_setter
,predict
,score
, andfit
for time-dependent forecasting and ensuringmu
(sum of components) is sampled.StructuredTimeSeries
Experiment (causalpy/experiments/structured_time_series.py
):DatetimeIndex
data,treatment_time
, andpatsy
formulas for exogenous regressors.y ~ 0
ory ~ 1
by ensuring the BSTS model's trend component provides the baseline, avoiding redundant patsy-generated intercepts for exogenous regressors.summary()
,plot()
(3-panel: fit/counterfactual, pointwise impact, cumulative impact), andget_plot_data()
.Testing & Integration:
test_bayesian_structural_time_series
) covering various scenarios, including custom components and error handling.plot_xY
(causalpy/plot_utils.py
).API & Docs:
StructuredTimeSeries
available ascausalpy.StructuredTimeSeries
.causalpy.pymc_experiments.py
.📚 Documentation preview 📚: https://causalpy--473.org.readthedocs.build/en/473/