Skip to content

Multivariate Structural Statespace Components #529

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

Open
wants to merge 57 commits into
base: main
Choose a base branch
from

Conversation

jessegrabowski
Copy link
Member

@jessegrabowski jessegrabowski commented Jun 25, 2025

This PR lifts the requirement that models built with the structural sub-module of PyMC be univariate. It's a chonky PR, so I split it into commits. Most of the files changes are changed by the first commit, which is just reorganization of files. It is safe to ignore that one.

Here are the steps I followed:

  1. The structural module was getting pretty unweildly, so I broke it into a bunch of sub-files. This makes the code easier to find and extend. This is handled in the Reorganize structural model modlue commit
  2. We need tools that can merge different components with potentially different (or overlapping) observed time series. This is handled by the Allow combination of component with different numbers of observed states PR. I am confident this code can be improved.
  3. Each component needs to have new logic implemented to handle the case where there are multiple observed series. Users can optionally pass a list of names to each component as observed_state_names. Every time you add two components together, all the relevant matrices are padded and expanded, and the total observed states are created as a union between the components.

For now, we assume all states in a component follow the same parameterization. It's now also valid to add together the same component twice with different states to work around this (e.g. AutoRegressive(order=1, observed_state_names=['data_1']) + Autoregressive(order=5, observed_state_names=['data_2'])) would be a valid model with 2 observed states, but each has it's own autoregressive dynamics.

When you pass a batch of observed_state_names, e.g. LevelTrend(order=2, observed_state_names=['data_1', 'data_2']), the parameters will all be given a batch dimension, but will otherwise be the same as the base case.

More docs coming, but I tried obsessively document what in there so far.

The logic for extending the components is pretty straight-forward -- mostly copying + block_diag or concat, but there are some corner cases that need attention.

This PR should be seen as a companion to #450. Instead of vectorizing across the computation of a model, we're concatenating models. There will be cases where this is superior -- for example when you want to explicitly model latent interactions between components. But in other cases, this approach will be worse. I am interested in having both.

@AlexAndorra
Copy link
Contributor

AutoRegressive(order=1, observed_state_names=['data_1']) + Autoregressive(order=5, observed_state_names=['data_2'])) would be a valid model with 2 observed states, but each has it's own autoregressive dynamics.

This is cool! I will review ASAP.

Note that #450 is currently blocked by what I think is a pytensor bug

Copy link
Contributor

@AlexAndorra AlexAndorra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is 🔥 @jessegrabowski 🤯
I just left a suggestion for what I think was a typo in the docstring. I'll merge once this is resolved, and then test all of this for our PyData tutorial -- probably this weekend.

Just a quick question: IIUC, now users can also have batched RegressionComponents, correct?

@AlexAndorra AlexAndorra self-requested a review June 28, 2025 22:11
Copy link
Contributor

@AlexAndorra AlexAndorra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is 🔥 @jessegrabowski 🤯
I just left a suggestion for what I think was a typo in the docstring.

Still missing this feature are:

We also need to:

  • Make sure that there are tests that combined LevelTrend + AR + error for two observed variables with no interaction model matches two separate models for each, given the same parameters.
  • Make sure that pytensor ops are used everywhere for building the SS matrices (no numpy/scipy)

@AlexAndorra
Copy link
Contributor

I think I'm done for a first review from you on the Cycle component @jessegrabowski 🍾

Dekermanjian and others added 4 commits July 5, 2025 08:23
2. Adjusted the regression component to allow multivariate regression component specification
3. Added a notebook for quick evaluation of the adjustments and additions made
2. replaced scipy block diag with pytensor block diag
3. Added forecast to test model in multivariate ssm notebook
Added multivariate regression-component
Copy link

Check out this pull request on  ReviewNB

See visual diffs & provide feedback on Jupyter Notebooks.


Powered by ReviewNB

Copy link
Member Author

@jessegrabowski jessegrabowski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AlexAndorra I left comments for you

Since it's my own PR I can't request changes. It's better in future if you fork the PR branch and open a new PR into this PR, then we can do the usual review workflow on your PR and merge it into this PR when we're ready

@jessegrabowski
Copy link
Member Author

jessegrabowski commented Jul 6, 2025

@AlexAndorra @Dekermanjian I want the names of parameters in the components to be really consistent and unsurprising. So please vote on:

  1. For the sigma parameters: name_sigma vs sigma_name
  2. For the initial state parameters :name_initial vs initial_name vs name
  3. For assorted greek things, like rho in Cycle: name_greek vs greek_name vs descriptive_name vs name_descriptive
  4. For shock state names: name vs name_shock vs name_innovation

Concrete examples for (3):
a. business_cycle_rho
b. rho_business_cycle
c. dampening_business_cycle
d. business_cycle_dampening

For (4), I'm talking about the internal state names that will end up as labels for the R and Q matrices, nothing else.

Also for default names, since all the are going to depend on the names in the multivariate case, should we:

  1. Make all the default names simpler. For example LevelTrend -> level_trend, or Cycle[s={cycle}, dampen={dampen}, innovations={innovations}] -> cycle
  2. Keep the complex names for univariate case, but use a simple default name when its multivariate
  3. Do away with default names, and force the user to always pass a name
  4. As 3, but only in the multivariate case

@jessegrabowski
Copy link
Member Author

jessegrabowski commented Jul 8, 2025

Looks like we have consensus on sigma_name and initial_name. Let's agree that in all cases,the name is always at the end of everything and we have a predictable syntax of f'{something}_{name}'.

To that end, let's also go with {name}_shock for the shock dim name. I actually agree with @Dekermanjian that {name}_innovation is a better name, but I can really see myself getting sick of typing "innovation" over and over. Shock is shorter, and it's what we already use.

Regarding greek/descriptive, I'm really on the fence. I understand why @AlexAndorra is super anti-greek, and I've done implementations that specifically remove greek names (changing beta and gamma in BatchNorm to loc and scale here, for example).

On the other hand, we do pay a cost when inventing our own names, unless they are already widely known/used in the specific literature. In the loc and scale example above, ML people probably don't know what that means, so even though it's clearer to us, a practitioner coming in might not agree.

In actual fact, we already mix descriptive and greek names. In CycleComponent, we use cycle_dampening_factor and cycle_length, but then in RegressionComponent we use beta (not coef like in AutoRegressive and TimeSeasonlity :) ). And of course we use sigma everywhere, but I guess that's a "good greek", not one of those bad greeks that take our jobs.

My suggestion is to punt on this and open a new issue to address package-wide naming conventions. For now, let's just make sure everything is of the form f'{something}_{name}'.

@AlexAndorra
Copy link
Contributor

I can really see myself getting sick of typing "innovation" over and over

Ha ha, literally the reasoning behind my choice.

On the other hand, we do pay a cost when inventing our own names, unless they are already widely known/used in the specific literature

Totally agree, it's just that the way I see it, the cost is internal: even if the descriptive names are only our own (which is not the case here, IIUC):

  1. We can define them in the docstrings (in the Greek case, one has to go look on the internet and make sure it's a one-to-one correspondence
  2. Descriptive is well... more descriptive -- and as Guido says, "explicit is better than implicit"

In CycleComponent, we use cycle_dampening_factor and cycle_length, but then in RegressionComponent we use beta (not coef like in AutoRegressive and TimeSeasonlity :) )

And I like CycleComponent much better for that :)

And of course we use sigma everywhere, but I guess that's a "good greek", not one of those bad greeks that take our jobs.

Masterpiece, nothing to add, you killed me 🤣
In all seriousness though, sigma is so universally associated to standard deviation that this is perfectly fine (and shorter to type)

@jessegrabowski
Copy link
Member Author

This is really close!

I think basically we need to go through and make sure all the names in all the components follow the agreed format. Let's leave {name}_shock for now, but I will open another PR to change that one since it breaks the pattern.

We just need a test for hidden state decomposition with multiple observed, and I think we're pretty much there.

@jessegrabowski
Copy link
Member Author

I added the test for multivariate decomposition. Basically it works, but it leaves a lot to be desired. I will open a separate issue to improve it.

I think this is more or less done. @AlexAndorra and @Dekermanjian , if you could provide a final review then we can merge it.

Also @Dekermanjian what do you want to do with your notebook that got merged in? We can either clean it up a bit to be a simple example, or we can remove it for now and you can do something more elaborate with missingness in the future.

@Dekermanjian
Copy link
Contributor

Dekermanjian commented Jul 11, 2025

I think this is more or less done. @AlexAndorra and @Dekermanjian , if you could provide a final review then we can merge it.

That is great! I will go through and review it today as soon as I am off from work.

Also @Dekermanjian what do you want to do with your notebook that got merged in? We can either clean it up a bit to be a simple example, or we can remove it for now and you can do something more elaborate with missingness in the future.

I am not sure. I think that if the simple example doesn’t add anything on top of what Alex is working on then it would be best to remove it and do something more elaborate as you say with missingness. I am interested in figuring out how that will all work in a Bayesian State Space framework.

Copy link
Contributor

@Dekermanjian Dekermanjian left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is looking great! The main issues I found in my review were related to the doctstring and certain components that still haven't adopted the schema of something_{self.name}. These should be pretty quick to fix. We are almost there!

Copy link
Contributor

@AlexAndorra AlexAndorra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe... this is it guys 🍾

@Dekermanjian
Copy link
Contributor

This is looking fantastic! 👏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants