google / lightweight_mmm

LightweightMMM 🦇 is a lightweight Bayesian Marketing Mix Modeling (MMM) library that allows users to easily train MMMs and obtain channel attribution information.
https://lightweight-mmm.readthedocs.io/en/latest/index.html
Apache License 2.0
883 stars 189 forks source link

Saturation + Carryover transformation #256

Open shubham223601 opened 1 year ago

shubham223601 commented 1 year ago

I understand that model type - carryover only takes into the consideration the delayed lagged effect (peaks observed can have delay in it) , however it does not take into account the saturation of the marketing channel.

to address this issue, i played around with source code modifying the carryover implemetation such that it takes into account the saturation formulation as well.

I take the below formulation of the carryover:

Screenshot 2023-10-05 at 14 28 40

and on this carryover implementation i apply the saturation component as below:

Screenshot 2023-10-05 at 14 28 53

updated code logic is as below:

def transform_hill_carryover(media_data: jnp.ndarray,
                        custom_priors: MutableMapping[str, Prior],
                        number_lags: int = 13) -> jnp.ndarray:
  """Transforms the input data with the carryover function and exponent.

  Args:
    media_data: Media data to be transformed. It is expected to have 2 dims for
      national models and 3 for geo models.
    custom_priors: The custom priors we want the model to take instead of the
      default ones. The possible names of parameters for carryover and exponent
      are "ad_effect_retention_rate_plate", "peak_effect_delay_plate" and
      "exponent".
    number_lags: Number of lags for the carryover function.

  Returns:
    The transformed media data.
  """
  transform_default_priors = _get_transform_default_priors()["carryover"]

  with numpyro.plate(name=f"{_HALF_MAX_EFFECTIVE_CONCENTRATION}_plate",
                     size=media_data.shape[1]):
    half_max_effective_concentration = numpyro.sample(
        name=_HALF_MAX_EFFECTIVE_CONCENTRATION,
        fn=custom_priors.get(
            _HALF_MAX_EFFECTIVE_CONCENTRATION,
            transform_default_priors[_HALF_MAX_EFFECTIVE_CONCENTRATION]))

  with numpyro.plate(name=f"{_SLOPE}_plate",
                     size=media_data.shape[1]):
    slope = numpyro.sample(
        name=_SLOPE,
        fn=custom_priors.get(_SLOPE, transform_default_priors[_SLOPE]))

  with numpyro.plate(name=f"{_AD_EFFECT_RETENTION_RATE}_plate",
                     size=media_data.shape[1]):
    ad_effect_retention_rate = numpyro.sample(
        name=_AD_EFFECT_RETENTION_RATE,
        fn=custom_priors.get(
            _AD_EFFECT_RETENTION_RATE,
            transform_default_priors[_AD_EFFECT_RETENTION_RATE]))

  with numpyro.plate(name=f"{_PEAK_EFFECT_DELAY}_plate",
                     size=media_data.shape[1]):
    peak_effect_delay = numpyro.sample(
        name=_PEAK_EFFECT_DELAY,
        fn=custom_priors.get(
            _PEAK_EFFECT_DELAY, transform_default_priors[_PEAK_EFFECT_DELAY]))

  with numpyro.plate(name=f"{_EXPONENT}_plate",
                     size=media_data.shape[1]):
    exponent = numpyro.sample(
        name=_EXPONENT,
        fn=custom_priors.get(_EXPONENT,
                             transform_default_priors[_EXPONENT]))

  half_max_effective_concentration = jnp.array(half_max_effective_concentration).reshape(3,1)
  slope = jnp.array(slope).reshape(3,1)                
  carryover = media_transforms.hill(media_transforms.carryover(
      data=media_data,
      ad_effect_retention_rate=ad_effect_retention_rate,
      peak_effect_delay=peak_effect_delay,
      number_lags=number_lags),half_max_effective_concentration=half_max_effective_concentration,
      slope=slope)

  if media_data.ndim == 3:
    exponent = jnp.expand_dims(exponent, axis=-1)
  return carryover

can you please suggest if this idea makes sense. and i would be happy if others would also like to test on their use case if needed

becksimpson commented 1 year ago

@shubham223601 Yes this makes sense, but just for the record, there is some saturation effects modelled for 'carryover' and 'adstock' models in the form of a learned exponent parameter. Now I've not had great success with exponent saturation, it is quite a restricted functional transformation, especially in LightweightMMM there is a considerable prior belief that there is no saturation effect, with prior _EXPONENT: dist.Beta(concentration1=9., concentration0=1.)

JasonHSchwartz commented 10 months ago

@shubham223601, this addition would align the methodology of Lightweight MMM with the paper "Bayesian Methods for Media Mix Modeling with Carryover and Shape Effects". Have you had success with this? I'm testing this now and I'll let you know my findings. It would be great to have this code added to the main repo.

shubham223601 commented 10 months ago

@JasonHSchwartz yes i tried indeed and the formulation i suggested above works, adding the saturation to the model formulation along with carryover factor. Even i investigated the channels saturation behaviour post modelling, which indeed confirms the added saturation and moreover the results doesnot deviate too much as compared with orginial carryover formulation as well

shubham223601 commented 10 months ago

@JasonHSchwartz did you get a chance as well to have a look at this?

JasonHSchwartz commented 10 months ago

@shubham223601, Yes, I've built a few models using this approach. Some of the saturation curves are not saturating quickly enough and I've had limited success with tuning priors to avoid this behaviour. I'm planning to run a few more iterations to find the right prior setup to control this behaviour.

The implementation does appear to be working though.

Are you planning to make a pull request? This could be useful for me (and I'm assuming others) who are running a parallel codebase to support the addstock-carryover model.

shubham223601 commented 10 months ago

@JasonHSchwartz Thanks for the check, indeed for my use case it does work as well. I already created a pull request today, hoping the developers can review the MR so that it can be merged. Thanks for the testing it out!

JasonHSchwartz commented 10 months ago

@shubham223601, No problem, thanks for taking the initiative, writing the code and making the PR.

I'm still testing priors, I'll post an update when I've found a good set of default priors for this model type.