Skip to content

Conversation

@cetagostini-wise
Copy link
Contributor

@cetagostini-wise cetagostini-wise commented Apr 15, 2024

Description

This PR introduces:

  • adstock & saturation transformation classes
    • ability to make new media transformations
  • MMM as replacement to DelayedSaturatedMMM class.
    • passing classes or string to MMM along to specify desired transformations
    • DelayedSaturatedMMM is specific instance of MMM but with "geometric" and "logistic" transformations
  • change order of media transformation with adstock_first parameter
  • refinements to the budget optimization

Currently we could choose the function in the following way:

from pymc_marketing.mmm import MMM

mmm = MMM(
    model_config=my_model_config,
    sampler_config=my_sampler_config,
    adstock="geometric",
    saturation="logistic",
    adstock_first=True,
    date_column="date_week",
    channel_columns=["x1", "x2"],
    control_columns=[
        "event_1",
        "event_2",
        "t",
    ],
    adstock_max_lag=8,
    yearly_seasonality=2,
)

Alternative to the string, there are new classes that can be passed instead

from pymc_marketing.mmm import MichaelisMentenSaturation

saturation = MichaelisMentenSaturation()

mmm = MMM(..., saturation=saturation, ...)

This has out-of-the-box media transformations for:

a) Adstock:

  • Geometric
  • Delayed
  • Weibull CDF
  • Weilbull PDF

b) Saturation:

  • Hill
  • Michaelis Menten
  • Logistic
  • Tanh
  • Tanh baselined

This allows for 4 (adstocks) x 5 (saturations) x 2 (orderings) = 40 out-of-the-box models!

One can also make a custom media transformation by inheriting from the AdstockTransformation or SaturationTransformation base class which can be used in the MMM as well. For instance,

from pymc_marketing.mmm import SaturationTransformation

class InfiniteReturns(SaturationTransformation): 
    def function(self, x, beta): 
        return beta * x
    
    default_priors = {"beta": {"dist": "HalfNormal", "kwargs": {"sigma": 1}}
   

saturation = InfiniteReturns()

mmm = MMM(..., saturation=saturation, ...)

EDIT: Changed description with latest changes

Related Issue

Checklist

Modules affected

  • MMM
  • CLV

Type of change

  • New feature / enhancement
  • Bug fix
  • Documentation
  • Maintenance
  • Other (please specify):

📚 Documentation preview 📚: https://pymc-marketing--632.org.readthedocs.build/en/632/

Co-Authored-By: Carlos Trujillo <59846724+cetagostini@users.noreply.github.com>
@cetagostini cetagostini requested review from cetagostini, juanitorduz and williambdean and removed request for cetagostini and juanitorduz April 15, 2024 21:59
@cetagostini cetagostini self-assigned this Apr 15, 2024
@cetagostini cetagostini added enhancement New feature or request MMM labels Apr 15, 2024
Co-Authored-By: Carlos Trujillo <59846724+cetagostini@users.noreply.github.com>
@cetagostini
Copy link
Contributor

Now is possible to pass an string to the class or a custom component class, e.g:

mmm = DelayedSaturatedMMM(
    model_config = my_model_config,
    sampler_config = my_sampler_config,
    adstock = "geometric",
    saturation = LogisticSaturationComponent,
    date_column="date_week",
    channel_columns=["x1", "x2"],
    control_columns=[
        "event_1",
        "event_2",
        "t",
    ],
    adstock_max_lag=8,
    yearly_seasonality=2,
)

If the user wants to allow for a custom function (Component) then he must create a class which contains a variable mapping property and a default config property. In order to allow other functions as lift test integration work.

@cetagostini
Copy link
Contributor

cetagostini commented Apr 15, 2024

I have still the issue of:

Traceback (most recent call last):
  File "/home/docs/checkouts/readthedocs.org/user_builds/pymc-marketing/envs/632/lib/python3.10/site-packages/sphinx/config.py", line 358, in eval_config_file
    exec(code, namespace)  # NoQA: S102
  File "/home/docs/checkouts/readthedocs.org/user_builds/pymc-marketing/checkouts/632/docs/source/conf.py", line 5, in <module>
    import pymc_marketing  # isort:skip
  File "/home/docs/checkouts/readthedocs.org/user_builds/pymc-marketing/envs/632/lib/python3.10/site-packages/pymc_marketing/__init__.py", line 1, in <module>
    from pymc_marketing import clv, mmm
  File "/home/docs/checkouts/readthedocs.org/user_builds/pymc-marketing/envs/632/lib/python3.10/site-packages/pymc_marketing/mmm/__init__.py", line 1, in <module>
    from pymc_marketing.mmm import base, delayed_saturated_mmm, preprocessing, validating
  File "/home/docs/checkouts/readthedocs.org/user_builds/pymc-marketing/envs/632/lib/python3.10/site-packages/pymc_marketing/mmm/delayed_saturated_mmm.py", line 22, in <module>
    from pymc_marketing.mmm.models.components.lagging import _get_lagging_function
ModuleNotFoundError: No module named 'pymc_marketing.mmm.models.components'

@juanitorduz @wd60622

If you don't find the separation into folders something practical then I'm going to leave everything in the main to avoid it. If you want to modify, feel free. Anything to move quickly.

I only do it because I would like to maintain some order and possibly have a folder with different models like in the CLV model.

@williambdean
Copy link
Contributor

williambdean commented Apr 16, 2024

The lift tests will need something like this:

variable_mapping = {
    "lam": "saturation_lambda",
    "beta": "saturation_beta",
}

def saturation_function(x, beta, lam):
    return beta * logistic_saturation(x, lam)

add_lift_measurements_to_likelihood(
    df_lift_test,
    variable_mapping,
    saturation_function=saturation_function,
    model=model,
    dist=dist,
    name=name,
)

The variable_mapping is already used, but the saturation_function would have to be exposed in each class as well

EDIT: Opening up an PR into your branch carlos: cetagostini-wise#1

@williambdean
Copy link
Contributor

I have still the issue of:

Traceback (most recent call last):
  File "/home/docs/checkouts/readthedocs.org/user_builds/pymc-marketing/envs/632/lib/python3.10/site-packages/sphinx/config.py", line 358, in eval_config_file
    exec(code, namespace)  # NoQA: S102
  File "/home/docs/checkouts/readthedocs.org/user_builds/pymc-marketing/checkouts/632/docs/source/conf.py", line 5, in <module>
    import pymc_marketing  # isort:skip
  File "/home/docs/checkouts/readthedocs.org/user_builds/pymc-marketing/envs/632/lib/python3.10/site-packages/pymc_marketing/__init__.py", line 1, in <module>
    from pymc_marketing import clv, mmm
  File "/home/docs/checkouts/readthedocs.org/user_builds/pymc-marketing/envs/632/lib/python3.10/site-packages/pymc_marketing/mmm/__init__.py", line 1, in <module>
    from pymc_marketing.mmm import base, delayed_saturated_mmm, preprocessing, validating
  File "/home/docs/checkouts/readthedocs.org/user_builds/pymc-marketing/envs/632/lib/python3.10/site-packages/pymc_marketing/mmm/delayed_saturated_mmm.py", line 22, in <module>
    from pymc_marketing.mmm.models.components.lagging import _get_lagging_function
ModuleNotFoundError: No module named 'pymc_marketing.mmm.models.components'

@juanitorduz @wd60622

If you don't find the separation into folders something practical then I'm going to leave everything in the main to avoid it. If you want to modify, feel free. Anything to move quickly.

I only do it because I would like to maintain some order and possibly have a folder with different models like in the CLV model.

I have not experienced this while working in my branch / locally. This is in the CI/CD right?

@cetagostini cetagostini requested a review from williambdean June 7, 2024 21:34
Copy link
Contributor

@williambdean williambdean left a comment

Choose a reason for hiding this comment

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

some small items and suggestions.

Checking out the notebooks now

Copy link
Contributor

+1


View entire conversation on ReviewNB

Copy link
Contributor

+1


View entire conversation on ReviewNB

@review-notebook-app
Copy link

review-notebook-app bot commented Jun 8, 2024

View / edit / reply to this conversation on ReviewNB

wd60622 commented on 2024-06-08T06:57:33Z
----------------------------------------------------------------

These formulas are not rendering well here or on the page: https://pymc-marketing--632.org.readthedocs.build/en/632/notebooks/mmm/mmm_budget_allocation_example.html


@cetagostini cetagostini requested a review from williambdean June 8, 2024 08:24
Copy link
Collaborator

@juanitorduz juanitorduz left a comment

Choose a reason for hiding this comment

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

I gave a quick review again. Very minor comments (I will check the notebooks tonight). I believe we will merge this one on Monday :D

constraints = custom_constraints

num_channels = len(self.parameters.keys())
initial_guess = [total_budget // num_channels] * num_channels
Copy link
Collaborator

Choose a reason for hiding this comment

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

Just to double check: this is the floor division and then multiplying it back?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, the multiplication is just to repeat the same number for all channels.

Copy link
Contributor

Choose a reason for hiding this comment

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

Why floor division over normal?

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
initial_guess = [total_budget // num_channels] * num_channels
initial_guess = np.ones(num_channels) * total_budget / num_channels

Comment on lines +187 to +188
method="SLSQP",
options={"ftol": 1e-9, "maxiter": 1000},
Copy link
Collaborator

Choose a reason for hiding this comment

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

Shall we allow the users to add this? We can add a **minimize_kwargs argument to allocate_budget and pass them to minimize?

Copy link
Collaborator

Choose a reason for hiding this comment

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

We can keep these defaults.

Also, this is not a blocker for this PR. Feel free to open an issue, and we can work it out later.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'll make the issue and address it in other PR!

Copy link
Contributor

Choose a reason for hiding this comment

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

Created #729

for group_name, params in param_groups.items():
# Build dictionary for the current group of parameters
param_dict = {
param.replace(group_name[:-7] + "_", ""): self.fit_result[param]
Copy link
Collaborator

Choose a reason for hiding this comment

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

can we do something more transparent like using param.split ? Say

>>> word = "my_word"
>>> word.split("my_")
['', 'word']
>>> word.split("my_")[-1]
'word'

budget_bounds: dict[str, list[Any]] | None = None,
custom_constraints: dict[str, float] | None = None,
quantile: float = 0.5,
):
Copy link
Collaborator

Choose a reason for hiding this comment

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

missing return type hint

Copy link
Contributor

@williambdean williambdean left a comment

Choose a reason for hiding this comment

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

Think it looks good!
Will just merge in suggestions from @juanitorduz

Any idea what is up with the code coverage decrease? Is that a fluke?

constraints = custom_constraints

num_channels = len(self.parameters.keys())
initial_guess = [total_budget // num_channels] * num_channels
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
initial_guess = [total_budget // num_channels] * num_channels
initial_guess = np.ones(num_channels) * total_budget / num_channels

Copy link
Collaborator

@juanitorduz juanitorduz left a comment

Choose a reason for hiding this comment

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

@cetagostini @wd60622 this was a massive effort and work! Amazing!

@juanitorduz juanitorduz merged commit f3be754 into pymc-labs:main Jun 10, 2024
@juanitorduz juanitorduz added this to the 0.7.0 milestone Jun 13, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request major API breaking changes MMM priority: high

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants