Skip to content

Commit 49ac689

Browse files
authored
Original hill function definition (#925)
1 parent 1c8fefa commit 49ac689

File tree

6 files changed

+239
-28
lines changed

6 files changed

+239
-28
lines changed

docs/source/uml/classes_mmm.png

-135 KB
Loading

pymc_marketing/mmm/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
)
2626
from pymc_marketing.mmm.components.saturation import (
2727
HillSaturation,
28+
HillSaturationSigmoid,
2829
InverseScaledLogisticSaturation,
2930
LogisticSaturation,
3031
MichaelisMentenSaturation,
@@ -50,6 +51,7 @@
5051
"DelayedSaturatedMMM",
5152
"GeometricAdstock",
5253
"HillSaturation",
54+
"HillSaturationSigmoid",
5355
"LogisticSaturation",
5456
"InverseScaledLogisticSaturation",
5557
"MMM",

pymc_marketing/mmm/components/saturation.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ def function(self, x, b):
7777

7878
from pymc_marketing.mmm.components.base import Transformation
7979
from pymc_marketing.mmm.transformers import (
80-
hill_saturation,
80+
hill_function,
81+
hill_saturation_sigmoid,
8182
inverse_scaled_logistic_saturation,
8283
logistic_saturation,
8384
michaelis_menten,
@@ -343,7 +344,7 @@ class MichaelisMentenSaturation(SaturationTransformation):
343344
class HillSaturation(SaturationTransformation):
344345
"""Wrapper around Hill saturation function.
345346
346-
For more information, see :func:`pymc_marketing.mmm.transformers.hill_saturation`.
347+
For more information, see :func:`pymc_marketing.mmm.transformers.hill_function`.
347348
348349
.. plot::
349350
:context: close-figs
@@ -364,7 +365,41 @@ class HillSaturation(SaturationTransformation):
364365

365366
lookup_name = "hill"
366367

367-
function = hill_saturation
368+
def function(self, x, slope, kappa, beta):
369+
return beta * hill_function(x, slope, kappa)
370+
371+
default_priors = {
372+
"slope": Prior("HalfNormal", sigma=1.5),
373+
"kappa": Prior("HalfNormal", sigma=1.5),
374+
"beta": Prior("HalfNormal", sigma=1.5),
375+
}
376+
377+
378+
class HillSaturationSigmoid(SaturationTransformation):
379+
"""Wrapper around Hill saturation sigmoid function.
380+
381+
For more information, see :func:`pymc_marketing.mmm.transformers.hill_saturation_sigmoid`.
382+
383+
.. plot::
384+
:context: close-figs
385+
386+
import matplotlib.pyplot as plt
387+
import numpy as np
388+
from pymc_marketing.mmm import HillSaturationSigmoid
389+
390+
rng = np.random.default_rng(0)
391+
392+
adstock = HillSaturationSigmoid()
393+
prior = adstock.sample_prior(random_seed=rng)
394+
curve = adstock.sample_curve(prior)
395+
adstock.plot_curve(curve, sample_kwargs={"rng": rng})
396+
plt.show()
397+
398+
"""
399+
400+
lookup_name = "hill_sigmoid"
401+
402+
function = hill_saturation_sigmoid
368403

369404
default_priors = {
370405
"sigma": Prior("HalfNormal", sigma=1.5),
@@ -415,6 +450,7 @@ def function(self, x, alpha, beta):
415450
TanhSaturationBaselined,
416451
MichaelisMentenSaturation,
417452
HillSaturation,
453+
HillSaturationSigmoid,
418454
RootSaturation,
419455
]
420456
}

pymc_marketing/mmm/transformers.py

Lines changed: 85 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -899,13 +899,86 @@ def michaelis_menten(
899899
return alpha * x / (lam + x)
900900

901901

902-
def hill_saturation(
902+
def hill_function(
903+
x: pt.TensorLike, slope: pt.TensorLike, kappa: pt.TensorLike
904+
) -> pt.TensorVariable:
905+
r"""Hill Function
906+
907+
.. math::
908+
f(x) = 1 - \frac{\kappa^s}{\kappa^s + x^s}
909+
910+
where:
911+
- :math:`s` is the slope of the hill.
912+
- :math:`\kappa` is the half-saturation point as :math:`f(\kappa) = 0.5` for any value of :math:`s` and :math:`\kappa`.
913+
- :math:`x` is the independent variable and must be non-negative.
914+
915+
Hill function from Equation (5) in the paper [1]_.
916+
917+
.. plot::
918+
:context: close-figs
919+
920+
import numpy as np
921+
import matplotlib.pyplot as plt
922+
from pymc_marketing.mmm.transformers import hill_function
923+
x = np.linspace(0, 10, 100)
924+
# Varying slope
925+
slopes = [0.3, 0.7, 1.2]
926+
fig, axes = plt.subplots(1, 3, figsize=(12, 4), sharey=True)
927+
for i, slope in enumerate(slopes):
928+
plt.subplot(1, 3, i+1)
929+
y = hill_function(x, slope, 2).eval()
930+
plt.plot(x, y)
931+
plt.xlabel('x')
932+
plt.title(f'Slope = {slope}')
933+
plt.subplot(1,3,1)
934+
plt.ylabel('Hill Saturation Sigmoid')
935+
plt.tight_layout()
936+
plt.show()
937+
# Varying kappa
938+
kappas = [1, 5, 10]
939+
fig, axes = plt.subplots(1, 3, figsize=(12, 4), sharey=True)
940+
for i, kappa in enumerate(kappas):
941+
plt.subplot(1, 3, i+1)
942+
y = hill_function(x, 1, kappa).eval()
943+
plt.plot(x, y)
944+
plt.xlabel('x')
945+
plt.title(f'Kappa = {kappa}')
946+
plt.subplot(1,3,1)
947+
plt.ylabel('Hill Saturation Sigmoid')
948+
plt.tight_layout()
949+
plt.show()
950+
951+
Parameters
952+
----------
953+
x : float or array-like
954+
The independent variable, typically representing the concentration of a
955+
substrate or the intensity of a stimulus.
956+
slope : float
957+
The slope of the hill. Must pe non-positive.
958+
kappa : float
959+
The half-saturation point as :math:`f(\kappa) = 0.5` for any value of :math:`s` and :math:`\kappa`.
960+
961+
Returns
962+
-------
963+
float
964+
The value of the Hill function given the parameters.
965+
966+
References
967+
----------
968+
.. [1] Jin, Yuxue, et al. “Bayesian methods for media mix modeling with carryover and shape effects.” (2017).
969+
""" # noqa: E501
970+
return pt.as_tensor_variable(
971+
1 - pt.power(kappa, slope) / (pt.power(kappa, slope) + pt.power(x, slope))
972+
)
973+
974+
975+
def hill_saturation_sigmoid(
903976
x: pt.TensorLike,
904977
sigma: pt.TensorLike,
905978
beta: pt.TensorLike,
906979
lam: pt.TensorLike,
907980
) -> pt.TensorVariable:
908-
r"""Hill Saturation Function
981+
r"""Hill Saturation Sigmoid Function
909982
910983
.. math::
911984
f(x) = \frac{\sigma}{1 + e^{-\beta(x - \lambda)}} - \frac{\sigma}{1 + e^{\beta\lambda}}
@@ -929,45 +1002,45 @@ def hill_saturation(
9291002
9301003
import numpy as np
9311004
import matplotlib.pyplot as plt
932-
from pymc_marketing.mmm.transformers import hill_saturation
1005+
from pymc_marketing.mmm.transformers import hill_saturation_sigmoid
9331006
x = np.linspace(0, 10, 100)
9341007
# Varying sigma
9351008
sigmas = [0.5, 1, 1.5]
9361009
fig, axes = plt.subplots(1, 3, figsize=(12, 4), sharey=True)
9371010
for i, sigma in enumerate(sigmas):
9381011
plt.subplot(1, 3, i+1)
939-
y = hill_saturation(x, sigma, 2, 5).eval()
1012+
y = hill_saturation_sigmoid(x, sigma, 2, 5).eval()
9401013
plt.plot(x, y)
9411014
plt.xlabel('x')
9421015
plt.title(f'Sigma = {sigma}')
9431016
plt.subplot(1,3,1)
944-
plt.ylabel('Hill Saturation')
1017+
plt.ylabel('Hill Saturation Sigmoid')
9451018
plt.tight_layout()
9461019
plt.show()
9471020
# Varying beta
9481021
betas = [1, 2, 3]
9491022
fig, axes = plt.subplots(1, 3, figsize=(12, 4), sharey=True)
9501023
for i, beta in enumerate(betas):
9511024
plt.subplot(1, 3, i+1)
952-
y = hill_saturation(x, 1, beta, 5).eval()
1025+
y = hill_saturation_sigmoid(x, 1, beta, 5).eval()
9531026
plt.plot(x, y)
9541027
plt.xlabel('x')
9551028
plt.title(f'Beta = {beta}')
9561029
plt.subplot(1,3,1)
957-
plt.ylabel('Hill Saturation')
1030+
plt.ylabel('Hill Saturation Sigmoid')
9581031
plt.tight_layout()
9591032
plt.show()
9601033
# Varying lam
9611034
lams = [3, 5, 7]
9621035
fig, axes = plt.subplots(1, 3, figsize=(12, 4), sharey=True)
9631036
for i, lam in enumerate(lams):
9641037
plt.subplot(1, 3, i+1)
965-
y = hill_saturation(x, 1, 2, lam).eval()
1038+
y = hill_saturation_sigmoid(x, 1, 2, lam).eval()
9661039
plt.plot(x, y)
9671040
plt.xlabel('x')
9681041
plt.title(f'Lambda = {lam}')
9691042
plt.subplot(1,3,1)
970-
plt.ylabel('Hill Saturation')
1043+
plt.ylabel('Hill Saturation Sigmoid')
9711044
plt.tight_layout()
9721045
plt.show()
9731046
@@ -977,8 +1050,8 @@ def hill_saturation(
9771050
The independent variable, typically representing the concentration of a
9781051
substrate or the intensity of a stimulus.
9791052
sigma : float
980-
The upper asymptote of the curve, representing the maximum value the
981-
function will approach as x grows large.
1053+
The upper asymptote of the curve, representing the approximate maximum value the
1054+
function will approach as x grows large. The true maximum value is at `sigma * (1 - 1 / (1 + exp(beta * lam)))`
9821055
beta : float
9831056
The slope parameter, determining the steepness of the curve.
9841057
lam : float
@@ -988,7 +1061,7 @@ def hill_saturation(
9881061
Returns
9891062
-------
9901063
float or array-like
991-
The value of the Hill function for each input value of x.
1064+
The value of the Hill saturation sigmoid function for each input value of x.
9921065
"""
9931066
return sigma / (1 + pt.exp(-beta * (x - lam))) - sigma / (1 + pt.exp(beta * lam))
9941067

tests/mmm/components/test_saturation.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
from pymc_marketing.mmm import (
2424
HillSaturation,
25+
HillSaturationSigmoid,
2526
InverseScaledLogisticSaturation,
2627
LogisticSaturation,
2728
MichaelisMentenSaturation,
@@ -48,6 +49,7 @@ def saturation_functions():
4849
TanhSaturationBaselined(),
4950
MichaelisMentenSaturation(),
5051
HillSaturation(),
52+
HillSaturationSigmoid(),
5153
RootSaturation(),
5254
]
5355

@@ -104,6 +106,7 @@ def test_support_for_lift_test_integrations(saturation) -> None:
104106
("tanh_baselined", TanhSaturationBaselined),
105107
("michaelis_menten", MichaelisMentenSaturation),
106108
("hill", HillSaturation),
109+
("hill_sigmoid", HillSaturationSigmoid),
107110
("root", RootSaturation),
108111
],
109112
)

0 commit comments

Comments
 (0)