Skip to content
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

Add baselined saturation #498

Merged
merged 14 commits into from
Jan 26, 2024
Merged

Add baselined saturation #498

merged 14 commits into from
Jan 26, 2024

Conversation

ferrine
Copy link
Contributor

@ferrine ferrine commented Jan 25, 2024

Baselined Tanh Saturation.

An alternative parameterization of the reach function is given by:

$$ \begin{align} c_0 &= \frac{r}{g \cdot \arctan(r)} \\ \beta &= \frac{g \cdot x_0}{r} \\ \text{saturation}(x, \beta, c_0) &= \beta \cdot \tanh \left( \frac{x}{c_0 \cdot \beta} \right) \end{align} $$

where:

  • $x_0$ is the "reference point". This is a point chosen
    by the user (not given a prior) where they expect most of their data to lie (median recommended).
    For example, if you're spending between 50 and 150 dollars on a particular channel,
    you might choose $x_0 = 100$.

  • $g$ is the "gain", which is the value of the CAC ($c_0$) at the reference point.
    You have to set a prior on what you think the CAC is when you spend $x_0 = 100$.
    Imagine you have four advertising channels, and you acquired 1000 new users.
    If each channel performed equally well, and advertising drove all sales, you might expect
    that you gained 250 users from each channel. Here, your "gain" would be $250 / 100 = 2.5$.

  • $r$, the overspend fraction is telling you where the reference point is.

    • $0$ - we can increase our budget by a lot to reach the saturated region,
      the deminishing returns are not visible yet.
    • $1$ - the reference point is already in the saturation region
      and additional dollar spend will not lead to any new users.
    • $0.8$, you can still increase acquired users by $50%$ as much
      you get in the reference point by increasing the budget.
      $x_0$ effect is 20% way from saturation

Key benefits

  • Works in equally well in saturated and undersaturated scenarios
  • Observed to have less sampling issues
  • Priors for $g$ and $r$ can be set using domain expertise easier than for the original parameterization

Origins

The original reach or saturation function used in an MMM is formulated as

$$ \text{saturation}(x, \beta, c_0) = \beta \cdot \tanh \left( \frac{x}{c_0 \cdot \beta} \right) $$

where:

  • $\beta$ is the saturation, or the limit of the total number
    of new users obtained when an infinite number of dollars are spent on that channel.
  • $c_0$ is the cost per acquisition (CAC0), so the initial cost per new user.
  • $\frac{1}{c_0}$ is the inverse of the CAC0, so it's the number of new
    users we might expect after spending our first dollar.

Description

The PR adds novel parameterization that is easier to use for industry applications where domain knowledge is an essence

image

Code example

  import pymc as pm
  import numpy as np

  x_in = np.exp(3+np.random.randn(100))
  true_cac = 1
  true_saturation = 100
  y_out = abs(np.random.normal(tanh_saturation(x_in, true_saturation, true_cac).eval(), 0.1))

  with pm.Model() as model_reparam:
      r = pm.Uniform("r")
      gain = pm.Exponential("gain", 1)
      input = pm.ConstantData("spent", x_in)
      response = pm.ConstantData("response", y_out)
      sigma = pm.HalfNormal("n")
      output = tanh_saturation_baselined(input, np.median(x_in), gain, r)
      pm.Normal("output", output, sigma, observed=response)
      trace = pm.sample()

Related Issue

  • Closes #
  • Related to #

Checklist

Modules affected

  • MMM
  • CLV

Type of change

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

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

@ferrine ferrine changed the title add baselined saturation with test and plots Add baselined saturation Jan 25, 2024
Copy link

codecov bot commented Jan 25, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Comparison is base (eee06ac) 90.87% compared to head (e660740) 91.24%.
Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #498      +/-   ##
==========================================
+ Coverage   90.87%   91.24%   +0.37%     
==========================================
  Files          21       21              
  Lines        1983     2044      +61     
==========================================
+ Hits         1802     1865      +63     
+ Misses        181      179       -2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@bwengals
Copy link

thanks for the writeup!!! Should be sure to mention that the reference point x0 has to be set within the range of the actual spends. As in, you buy ads three times and spend 5, 6 and 7 dollars, x0 has to be set within [5, 7], so not 4 not 8. Otherwise the posterior of r and gain becomes a skinny diagonal line. I got stuck on that one, and it could be very relevant if there is very little spend observations for a particular channel.

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.

@ferrine this is amazing! Thanks! I think once you add @bwengals 's suggestion we are ready to merge!

More of a general question: Where would the adstock transformation come in? I guess before applying this one right?

Also, maybe for a next PR we can create another MMM model subclass to make sure users can use the API :)

@bwengals
Copy link

bwengals commented Jan 25, 2024

Why not just do:

def tanh_saturation_baseline(x, x0, gain, r):
    saturation = (gain * x0) / r
    cac0 = r / (gain * pt.arctanh(r))
    return tanh_saturation(x, cac0, saturation)

Only 4 lines! What functionality is the dataclass for the params and baseline and debaseline adding?

Also: I don't understand the name "baseline", or how this is "baselined". Maybe something else would be a clearer name (sorry to critique without suggestions, I can't think of anything though!)

@ferrine
Copy link
Contributor Author

ferrine commented Jan 26, 2024

Why not just do:

def tanh_saturation_baseline(x, x0, gain, r):
    saturation = (gain * x0) / r
    cac0 = r / (gain * pt.arctanh(r))
    return tanh_saturation(x, cac0, saturation)

Only 4 lines! What functionality is the dataclass for the params and baseline and debaseline adding?

Also: I don't understand the name "baseline", or how this is "baselined". Maybe something else would be a clearer name (sorry to critique without suggestions, I can't think of anything though!)

The dataclasses are the helpers in case you need to visualize not just gain or r, but cac, saturation, calculate it for a different x0, etc. they can just sit there and serve for those who needs them.

@ferrine
Copy link
Contributor Author

ferrine commented Jan 26, 2024

More of a general question: Where would the adstock transformation come in? I guess before applying this one right?

The adstock will be in just the same order

Also, maybe for a next PR we can create another MMM model subclass to make sure users can use the API :)

Why not a boolean parameter?

@juanitorduz
Copy link
Collaborator

Also, maybe for a next PR we can create another MMM model subclass to make sure users can use the API :)

Why not a boolean parameter?

I could imagine in the future, we would have many different models with different plotting functions, optimizations, depending on the functional forms ... just thinking ahead :)

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.

LGTM! Thanks!

As a side comment, personally (so not necessarily true ;) ) I like the NamedTuple structure. It has crossed my mind to even use pydantic for parameter and data validation.

@ferrine
Copy link
Contributor Author

ferrine commented Jan 26, 2024

LGTM! Thanks!

As a side comment, personally (so not necessarily true ;) ) I like the NamedTuple structure. It has crossed my mind to even use pydantic for parameter and data validation.

Pydantic is great and I appreciate anyone taking effort to refactor everything to Pydantic, just not now. WDYT?

@ferrine ferrine merged commit 28aad7a into main Jan 26, 2024
13 checks passed
@ferrine ferrine deleted the saturation-baselined branch January 26, 2024 11:19
@juanitorduz
Copy link
Collaborator

I created a discussion issue #502 . I will be happy to work on this :)

juanitorduz added a commit that referenced this pull request Mar 8, 2024
* current status as method

* format

* Update version.txt

* Implement different convolution modes (#454)

* Add PR template

* Update pull_request_template.md

* Fix issues in index example

* Update .pre-commit-config.yaml

* Update .pre-commit-config.yaml

* move from other PR

* put legend on side

* Optimisation in customer_lifetime_value when discount_rate == 0 (#468)

* Optimisation in customer_lifetime_value when discount_rate == 0

cf #467

* Update utils.py

* Update README.md

* add support for pre-commit-ci

* add isort

* modify autosummary templates

* Rename `clv_summary` to `rfm_summary` and extend functionality (#479)

* clv_summary adapted into rfm_summary

* added clv_summary with warning

* moved dataset from testing folder

* Update version.txt

* improve ruff

* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.1.11 → v0.1.14](astral-sh/ruff-pre-commit@v0.1.11...v0.1.14)
- [github.com/pre-commit/pre-commit-hooks: v3.2.0 → v4.5.0](pre-commit/pre-commit-hooks@v3.2.0...v4.5.0)

* resolve conflict

* Add baselined saturation (#498)

* add baselined saturation with test and plots

* refactor docs

* add the reparam

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* verify parametrization is equivalent under change of baseline

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* add a note for setting x0

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* make it clear how r_ref is calculated

* fix typo

* fix docstrings

* improve test by making sure transform is gives identical saturation and cac0

* add comment in the docstring

* add blank line in the code-block

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* Swap Before and After convolution modes as per #489 (#501)

* Add support for string mode args

* Swap before and after and make mode explicit

* Use Union due Python 3.9

* Style

* resolve conflict

* add dim_name arg

* add seed to tests and test methods

* add slice as type hint

* use slice in docstring

* defaults to mean for each channel

* add non-negative check

* ax as last arg

* change weeks -> time

* parameterize quantiles

* separate out and add to docs

* rerun the baseline images

* mock the prior

* add new images from latest env

* migrate to toml instead of ci/cd

* test only is axes

* remove the images

---------

Co-authored-by: Juan Orduz <[email protected]>
Co-authored-by: Abdalaziz Rashid <[email protected]>
Co-authored-by: Ricardo Vieira <[email protected]>
Co-authored-by: Ricardo Vieira <[email protected]>
Co-authored-by: vincent-grosbois <[email protected]>
Co-authored-by: juanitorduz <[email protected]>
Co-authored-by: Oriol (ProDesk) <[email protected]>
Co-authored-by: Colt Allen <[email protected]>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Maxim Kochurov <[email protected]>
wd60622 added a commit that referenced this pull request Apr 8, 2024
* current status as method

* format

* Update version.txt

* Implement different convolution modes (#454)

* Add PR template

* Update pull_request_template.md

* Fix issues in index example

* Update .pre-commit-config.yaml

* Update .pre-commit-config.yaml

* move from other PR

* put legend on side

* Optimisation in customer_lifetime_value when discount_rate == 0 (#468)

* Optimisation in customer_lifetime_value when discount_rate == 0

cf #467

* Update utils.py

* Update README.md

* add support for pre-commit-ci

* add isort

* modify autosummary templates

* Rename `clv_summary` to `rfm_summary` and extend functionality (#479)

* clv_summary adapted into rfm_summary

* added clv_summary with warning

* moved dataset from testing folder

* Update version.txt

* improve ruff

* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.1.11 → v0.1.14](astral-sh/ruff-pre-commit@v0.1.11...v0.1.14)
- [github.com/pre-commit/pre-commit-hooks: v3.2.0 → v4.5.0](pre-commit/pre-commit-hooks@v3.2.0...v4.5.0)

* resolve conflict

* Add baselined saturation (#498)

* add baselined saturation with test and plots

* refactor docs

* add the reparam

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* verify parametrization is equivalent under change of baseline

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* add a note for setting x0

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* make it clear how r_ref is calculated

* fix typo

* fix docstrings

* improve test by making sure transform is gives identical saturation and cac0

* add comment in the docstring

* add blank line in the code-block

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* Swap Before and After convolution modes as per #489 (#501)

* Add support for string mode args

* Swap before and after and make mode explicit

* Use Union due Python 3.9

* Style

* resolve conflict

* add dim_name arg

* add seed to tests and test methods

* add slice as type hint

* use slice in docstring

* defaults to mean for each channel

* add non-negative check

* ax as last arg

* change weeks -> time

* parameterize quantiles

* separate out and add to docs

* rerun the baseline images

* mock the prior

* add new images from latest env

* migrate to toml instead of ci/cd

* test only is axes

* remove the images

---------

Co-authored-by: Juan Orduz <[email protected]>
Co-authored-by: Abdalaziz Rashid <[email protected]>
Co-authored-by: Ricardo Vieira <[email protected]>
Co-authored-by: Ricardo Vieira <[email protected]>
Co-authored-by: vincent-grosbois <[email protected]>
Co-authored-by: juanitorduz <[email protected]>
Co-authored-by: Oriol (ProDesk) <[email protected]>
Co-authored-by: Colt Allen <[email protected]>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Maxim Kochurov <[email protected]>
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.

3 participants