pymc-labs / pymc-marketing

Bayesian marketing toolbox in PyMC. Media Mix (MMM), customer lifetime value (CLV), buy-till-you-die (BTYD) models and more.
https://www.pymc-marketing.io/
Apache License 2.0
705 stars 197 forks source link

`BudgetOptimizer` does not seem to support time-varying intercept #757

Open AlfredoJF opened 4 months ago

AlfredoJF commented 4 months ago

Not quite sure if this will be fixed in the upcoming 0.7.0 release but noticed the re-written BudgetOptimizer does not work with time-varying intercept or there's a conflict with PyTensor.

I followed the mmm_budget_allocation_example.ipynb and in this cell was when I got the error.

response = mmm.allocate_budget_to_maximize_response(
    budget=total_budget,
    num_days=8,
    time_granularity="weekly",
    budget_bounds=budget_bounds
)

Here is the error I got using an MMM with tvp that did not come up with another MMM without time-varying intercept:

NotImplementedError                       Traceback (most recent call last)
[/usr/local/lib/python3.10/dist-packages/pytensor/link/basic.py](https://localhost:8080/#) in __set__(self, value)
    101                 # Use in-place filtering when/if possible
--> 102                 self.storage[0] = self.type.filter_inplace(
    103                     value, self.storage[0], **kwargs

10 frames
[/usr/local/lib/python3.10/dist-packages/pytensor/graph/type.py](https://localhost:8080/#) in filter_inplace(self, value, storage, strict, allow_downcast)
    127         """
--> 128         raise NotImplementedError()
    129 

NotImplementedError: 

During handling of the above exception, another exception occurred:

TypeError                                 Traceback (most recent call last)
[<ipython-input-49-329a8396104b>](https://localhost:8080/#) in <cell line: 1>()
----> 1 response = mmm.allocate_budget_to_maximize_response(
      2     budget=total_budget,
      3     num_days=8,
      4     time_granularity="weekly",
      5     budget_bounds=budget_bounds

[/usr/local/lib/python3.10/dist-packages/pymc_marketing/mmm/delayed_saturated_mmm.py](https://localhost:8080/#) in allocate_budget_to_maximize_response(self, budget, time_granularity, num_days, budget_bounds, custom_constraints, quantile)
   1977         )
   1978 
-> 1979         return self.sample_posterior_predictive(
   1980             X_pred=synth_dataset,
   1981             extend_idata=False,

[/usr/local/lib/python3.10/dist-packages/pymc_marketing/mmm/delayed_saturated_mmm.py](https://localhost:8080/#) in sample_posterior_predictive(self, X_pred, extend_idata, combined, include_last_observations, original_scale, **sample_posterior_predictive_kwargs)
   1644 
   1645         with self.model:  # sample with new input data
-> 1646             post_pred = pm.sample_posterior_predictive(
   1647                 self.idata, **sample_posterior_predictive_kwargs
   1648             )

[/usr/local/lib/python3.10/dist-packages/pymc/sampling/forward.py](https://localhost:8080/#) in sample_posterior_predictive(trace, model, var_names, sample_dims, random_seed, progressbar, progressbar_theme, return_inferencedata, extend_inferencedata, predictions, idata_kwargs, compile_kwargs)
    859                     param = _trace[idx % len_trace]
    860 
--> 861                 values = sampler_fn(**param)
    862 
    863                 for k, v in zip(vars_, values):

[/usr/local/lib/python3.10/dist-packages/pymc/util.py](https://localhost:8080/#) in wrapped(**kwargs)
    393     def wrapped(**kwargs):
    394         input_point = {k: v for k, v in kwargs.items() if k in ins}
--> 395         return core_function(**input_point)
    396 
    397     return wrapped

[/usr/local/lib/python3.10/dist-packages/pytensor/compile/function/types.py](https://localhost:8080/#) in __call__(self, *args, **kwargs)
    895         if kwargs:  # for speed, skip the items for empty kwargs
    896             for k, arg in kwargs.items():
--> 897                 self[k] = arg
    898 
    899         if (

[/usr/local/lib/python3.10/dist-packages/pytensor/compile/function/types.py](https://localhost:8080/#) in __setitem__(self, item, value)
    547 
    548     def __setitem__(self, item, value):
--> 549         self.value[item] = value
    550 
    551     def __copy__(self):

[/usr/local/lib/python3.10/dist-packages/pytensor/compile/function/types.py](https://localhost:8080/#) in __setitem__(self, item, value)
    503                     )
    504                 if isinstance(s, Container):
--> 505                     s.value = value
    506                     s.provided += 1
    507                 else:

[/usr/local/lib/python3.10/dist-packages/pytensor/link/basic.py](https://localhost:8080/#) in __set__(self, value)
    104                 )
    105             except NotImplementedError:
--> 106                 self.storage[0] = self.type.filter(value, **kwargs)
    107 
    108         except Exception as e:

[/usr/local/lib/python3.10/dist-packages/pytensor/tensor/type.py](https://localhost:8080/#) in filter(self, data, strict, allow_downcast)
    240 
    241         if self.ndim != data.ndim:
--> 242             raise TypeError(
    243                 f"Wrong number of dimensions: expected {self.ndim},"
    244                 f" got {data.ndim} with shape {data.shape}."

TypeError: ('Wrong number of dimensions: expected 0, got 1 with shape (118,).', 'Container name "intercept"')
wd60622 commented 4 months ago

Hi @AlfredoJF, thank you for bringing this up. Are you having issues with the out of sample prediction as well? Would you be able to give that method as well.

CC: @cetagostini

juanitorduz commented 4 months ago

It seems like a broadcasting issue right @wd60622 ?

AlfredoJF commented 4 months ago

Hi @wd60622, no issues with the Out-of-sample posterior only with the BudgetOptimizer


y_test_pred = mmm.sample_posterior_predictive(X_pred=X_test, extend_idata=False, include_last_observations=True)
wd60622 commented 4 months ago

Hi @AlfredoJF, Thanks for checking that. If you have found the culprit, feel free to open a PR! If not, we can do some investigation. Would you be able to post a small reproducible example (including the initialization of the class)?

AlfredoJF commented 4 months ago

Hi @wd60622,

Yes, I'll post a reproducible example and leave the investigation to the experts for now. :)

wd60622 commented 4 months ago

Yes, I'll post a reproducible example and leave the investigation to the experts for now. :)

Great, sounds good. We a reproducible example, we can dig into it a bit better. We should be able to get to it soon

AlfredoJF commented 4 months ago

Hi @wd60622

I used the mmm and tvp example data to see if I could create a reproducible example for further investigation but I did not get the same error as in my model. I cannot share my dataset as it contains client's data but will try to create another dataset for this purpose.

Thanks for the patience!

AlfredoJF commented 4 months ago

I just tested the first workaround in #814, and it fixed this issue. So I assume #815 also fixes this one for the next release.

wd60622 commented 4 months ago

I just tested the first workaround in #814, and it fixed this issue. So I assume #815 also fixes this one for the next release.

Mmm interesting that that workaround helped out this one as well. Not obvious why that would be the case. Are you using 0.7.0 then? or installed from main branch?

AlfredoJF commented 4 months ago

@wd60622 Yes, I'm using 0.7.0 and have overwritten the MMM class for now as I'm closer to the delivery date of my project. I'll upgrade to 0.8.0 once it's released in a future iteration of my model

wd60622 commented 4 months ago

@wd60622 Yes, I'm using 0.7.0 and have overwritten the MMM class for now as I'm closer to the delivery date of my project. I'll upgrade to 0.8.0 once it's released in a future iteration of my model

Sounds good. Well glad that it sorted itself out. If you run into any more issues, feel free to raise an issue.

I will leave this issue open for now