chc.response¶
Stability tier: experimental.
A channel's response to its spend: how its return rises with what is spent, how long the spend carries over, and what the return is per unit spent.
Every family is a standard shape g of spend in units of its own scale K,
h(spend) = g(spend / K), with K in currency and every other parameter dimensionless. A
change of currency therefore moves K and nothing else, and every number read off a curve -- a
value, a slope per unit of spend, an inflection or a tangency in spend -- moves with it exactly.
Bounded families (Saturation) start at h(0) = 0, increase and rise to a ceiling of
1; a channel's coefficient carries the size of the return, the curve only its shape.
- Concave from zero spend:
MichaelisMenten,Exponential(Mitscherlich's law, the monomolecular curve),Tanh,Arctan,AlgebraicandHalfNormal. - A shape parameter, S-shaped over part of its range:
Hill(the log-logistic),Weibull,LogisticandGompertz, both normalised to 0 at 0,Richards,ChapmanRichards,GammaCDF,LogNormalCDF,BurrXII, andBetaCDFandKumaraswamy, which reach the ceiling atK.
Unbounded baselines (Response): Logarithmic and Power, the constant
elasticity. Not monotone, apart: Ricker, the inverted U of ad fatigue; nothing below
about envelopes applies to it.
A floor is not a shape. At zero spend a floor is the plant's base, so Janoschek's curve is
Weibull with a floor, and ADBUDG (Little 1970) and Morgan--Mercer--Flodin are
Hill with one.
Why the envelope. On an S-shaped curve h'(0) = 0 wherever the curve starts convex, so zero
spend on a channel is a stationary point, and a planner started at zero can stop there and report
convergence. The concave envelope (Envelope) is the tangent from the origin up to the
spend A where it touches the curve, h(A) = A h'(A), and the curve beyond it. Planned
against, it has no such trap, its plan is a warm start for the true curve, and its value bounds the
true optimum, so the gap between the two certifies the plan. relax swaps every such curve in
a model for its envelope, and chc.plan.causal_plan plans the relaxed model first whenever it
is given no warm start. The inflection and the tangency are in closed form where one exists and
found as a root otherwise (validation/response_curves.mac).
Carryover (Adstock). A period's adstock is the spend of the length periods up to
it, weighted by lag, a_t = sum_{l < L} w_l s_{t - l}, with nothing spent before the series:
GeometricAdstock, DelayedAdstock, whose carryover peaks after the spend, and
WeibullAdstock. The length is the kernel's own, never a share of the series, so logging
another period moves no earlier value; normalized has no default, since packages differ on it
and every coefficient fitted through the kernel moves with it. Channel is a channel's
return, coefficient * curve(kernel(spend)), adstock first.
The return, in the outcome's units unless revenue_per_kpi turns a KPI into revenue:
contribution, the channel's return in a window's periods; and per currency unit spent,
roi, the incremental return on a window's spend, carryover included;
marginal_roi, the return on one more unit spread over the window; and
steady_state_marginal_roi, the slope of a period's return once the adstock of a constant
spend has settled. A coefficient is none of them: through a long geometric kernel of
retention r a unit of spend on a linear channel returns 1 / (1 - r) times what it returns
in its own period, and on a saturating curve the return per unit moves with the spend.
PyMC-Marketing, mapped (scripts/pymc_marketing_reference.py runs its transforms beside
these). geometric_adstock and delayed_adstock with l_max are GeometricAdstock
and DelayedAdstock with length = l_max; the CDF weibull_adstock with l_max
carries one weight more, WeibullAdstock with length = l_max + 1 and its lam as
scale. LogisticSaturation, (1 - e^{-lam x}) / (1 + e^{-lam x}), is
tanh(lam x / 2): concave from zero, so Tanh with K = 2 / lam, not
Logistic. tanh_saturation with b and c is b times Tanh with
K = b c, michaelis_menten with alpha and lam is alpha times
MichaelisMenten with K = lam, hill_function is Hill,
hill_saturation_sigmoid is a multiple of Logistic, and root_saturation is
Power. Neither PyMC-Marketing nor Meridian is a dependency: each would bring a
probabilistic-programming stack for a few closed forms, and neither has the envelope.
HONEST SCOPE:
Saturation.inflection,Saturation.tangencyandEnveloperead concrete parameters: a fitted curve, not one inside a trace. Every curve itself traces, differentiates and compiles, and its parameters are leaves a fit can move. So does every kernel.BetaCDFhas no derivative in its shapesaandb, since JAX'sbetainchas none; asking for one raises.Kumaraswamyis its closed-form counterpart.- Spend is nonnegative. Below zero a family's value is not defined, and several return
nan. - A curve that rises from zero spend like
z^mhas its slopes of order aboveminfinite there unlessmis whole: the slope itself below shape 1 (Hill,Weibull,ChapmanRichards,BurrXII,Kumaraswamy,GammaCDF,BetaCDF, andPower), the curvature below shape 2. Each is read a machine epsilon of the scale off zero instead: finite, so a zero weight on it, as in a period no spend reaches, is nothing rather thannan, and the slope still steeper there than anywhere past it. A number read through one, such as a decision weight in a parameter that turns that spend on, is large and finite, not infinite. Every slope finite at zero spend is the curve's own. - In single precision the numeric tangencies are good to single precision.
- The readings of the return take concrete numbers: they report on a fitted channel, not on one inside a trace. The carryover they count is what falls inside the series; to count the rest, extend the series with what is spent after it.
- Meridian's forms are not mapped yet, nor run beside these.
Response
¶
Bases: Module
A response curve: the return on a channel as a function of its spend, zero at zero spend.
h(spend) = standard(spend / scale), with scale in currency.
standard(z)
abstractmethod
¶
The curve with spend in units of scale.
Saturation
¶
Bases: Response
A bounded response: zero at zero spend, increasing, and rising to a ceiling of 1.
inflection()
¶
Where the curve turns from convex to concave, in spend; 0 if it is concave from zero.
Below it h'' > 0, so a plan's first-order conditions do not certify it there.
tangency()
¶
The spend where the tangent from the origin touches the curve; 0 if it is concave.
For a curve convex up to the end of its support, that end, where the chord from the origin
meets the ceiling at a corner: the beta and Kumaraswamy CDFs at b = 1.
nonconvexity()
¶
The most the envelope rises above the curve, max (envelope - curve); 0 if concave.
A number of the shape alone, on the curve's scale of a ceiling of 1, so a change of
currency leaves it. A plan made on envelopes loses at most a channel's coefficient times
this where it leaves the channel inside its chord (chc.allocation). Hill's at slope
2, 3 and 5 is 0.0674, 0.1547 and 0.2920; it rises to 1, a step's, as the slope grows, and
falls as 0.1619 (n - 1)^2 as the slope n falls to 1, where the curve turns concave
(validation/envelope_nonconvexity.mac).
Envelope
¶
Bases: Saturation
The concave envelope of a saturation curve: the least concave curve on or above it.
Linear from the origin to the tangency A, where h(A) = A h'(A), and the curve beyond
it; the curve itself when it is concave. Planned against, it bounds the return any plan on the
curve can reach, and its plan is the warm start that keeps a planner off zero spend.
MichaelisMenten
¶
Exponential
¶
Bases: Saturation
1 - e^{-z}: Mitscherlich's law, the monomolecular curve; 1 - 1/e of the ceiling at
K.
Tanh
¶
Arctan
¶
Algebraic
¶
HalfNormal
¶
Hill
¶
Bases: Saturation
z^n / (1 + z^n): half the ceiling at K, the log-logistic CDF (Hill 1910).
S-shaped for slope > 1, with inflection ((n - 1)/(n + 1))^{1/n} and tangency
(n - 1)^{1/n}; MichaelisMenten at slope = 1. With a floor it is ADBUDG and
Morgan--Mercer--Flodin.
Weibull
¶
Bases: Saturation
1 - exp(-z^k): the Weibull CDF, 1 - 1/e of the ceiling at K.
S-shaped for shape > 1, with inflection ((k - 1)/k)^{1/k}; the tangency is u^{1/k}
at the positive root of e^u - 1 = k u. Exponential at shape = 1; with a floor,
Janoschek's curve (1957).
Logistic
¶
Bases: Saturation
The logistic 1 / (1 + e^{-s (z - 1)}) moved and stretched to run from 0 at zero spend to
1, with its midpoint and inflection at K. Always S-shaped.
Gompertz
¶
Bases: Saturation
Gompertz's exp(-b e^{-z}) moved and stretched to run from 0 at zero spend to 1.
b is the displacement: the inflection is at log b, so the curve is S-shaped for
b > 1 and concave from zero otherwise.
Richards
¶
Bases: Saturation
Richards' generalised logistic (1 + nu e^{-s (z - 1)})^{-1/nu}, normalised to 0 at 0.
The inflection is at K for every asymmetry nu. Logistic at nu = 1; as
nu -> 0 it tends to Gompertz with b = e^s and scale K / s.
ChapmanRichards
¶
Bases: Saturation
(1 - e^{-z})^p, the Chapman--Richards growth curve.
S-shaped for power > 1, with inflection log p; the tangency is the positive root of
e^z - 1 = p z. Exponential at power = 1.
GammaCDF
¶
Bases: Saturation
The gamma CDF with unit rate, P(a, z). S-shaped for shape > 1, with inflection at
the mode a - 1; Exponential at shape = 1.
LogNormalCDF
¶
Bases: Saturation
Phi(log z / sigma): the log-normal CDF with median K. Always S-shaped, with
inflection at the mode e^{-sigma^2}.
BurrXII
¶
Bases: Saturation
1 - (1 + z^c)^{-k}, the Burr XII CDF, with slope c and tail k.
S-shaped for slope > 1, with inflection ((c - 1)/(c k + 1))^{1/c}. Hill at
tail = 1; the tail sets how slowly the last of the ceiling arrives, as z^{-c k}.
BetaCDF
¶
Bases: Saturation
The beta CDF I_z(a, b) on [0, K], and the ceiling beyond K.
S-shaped for a > 1, with inflection at the mode (a - 1)/(a + b - 2); at b = 1 it is
z^a, convex up to K, where it touches its envelope. b >= 1, since below 1 the curve
would rise ever faster into its ceiling. No derivative in a or b (JAX's betainc
has none); Kumaraswamy has them.
Kumaraswamy
¶
Bases: Saturation
1 - (1 - z^a)^b on [0, K], and the ceiling beyond K: Kumaraswamy's closed-form
counterpart of BetaCDF.
S-shaped for a > 1, with inflection ((a - 1)/(a b - 1))^{1/a}; b >= 1, as for the
beta CDF.
Power
¶
Bases: Response
z^rho, 0 < rho <= 1: constant elasticity rho, concave and unbounded.
PyMC-Marketing's RootSaturation.
Ricker
¶
Bases: Response
z e^{1 - z}: rises to its peak of 1 at K and falls after it, the inverted U of ad
fatigue. Not monotone, so no envelope, and a plan's concavity arguments do not reach it.
Adstock
¶
Bases: Module
A carryover kernel: a_t = sum_{l < L} w_l s_{t - l}, the spend of the length periods
up to a period weighted by lag, with nothing spent before the series.
A period's adstock reads those L periods and no others, so appending periods to a series
moves no earlier value, and normalized divides by the kernel's own sum, never the series'.
It has no default: packages differ on it, and every coefficient fitted through the kernel moves
with it.
GeometricAdstock
¶
DelayedAdstock
¶
Bases: Adstock
w_l = r^{(l - d)^2}: the carryover peaks delay periods after the spend (Jin, Wang,
Sun, Chan and Koehler 2017).
WeibullAdstock
¶
Bases: Adstock
w_0 = 1 and w_l = prod_{j=1}^{l} (1 - F(j)), F the Weibull CDF of shape and
scale: the share of the carryover that survives each lag. The scale is in periods, not a
share of the series.
Channel
¶
Bases: Module
A channel's return on its spend, coefficient * curve(kernel(spend)): adstock first.
coefficient is in the outcome's units per unit of the curve, so the return is in the
outcome's units, revenue or a KPI.
relax(model)
¶
model with every saturation curve in it that starts convex replaced by its envelope.
model is any pytree holding curves: a plant, a tuple of curves, one curve. A curve concave
from zero is left as it is, and when no curve starts convex the answer is model itself, so
relax(model) is model says there is nothing to relax. chc.plan.causal_plan reads it
that way, and starts its descent from the relaxed problem's plan.
contribution(channel, spend, window=slice(None), *, revenue_per_kpi=1.0)
¶
The channel's return in the window's periods, carried over from all the spend before them.
What a decomposition attributes to the channel in those periods, in the outcome's units, or in
revenue through revenue_per_kpi. The return the window's own spend causes, wherever it
falls, is instead roi times what the window spent.
roi(channel, spend, window=slice(None), *, revenue_per_kpi=1.0)
¶
The incremental return on the window's spend, per currency unit, carryover included.
The channel's return over the whole series, less its return with the window's spend removed,
over what the window spent. channel is any function of a (T,) spend series returning a
(T,) series of returns. The carryover counted is what falls inside the series: to count all
of it, extend the series by the kernel's length with what is spent after.
revenue_per_kpi turns a KPI into revenue; at 1 the outcome is revenue.
marginal_roi(channel, spend, window=slice(None), *, revenue_per_kpi=1.0)
¶
The return on one more currency unit, spread over the window in proportion to its spend.
The derivative of the channel's return over the whole series along the window's spend, over
what the window spent: the limit of scaling the window's spend by 1 + e, where a step of
finite size would move the number on a curved response. Carryover is counted as in
roi.
steady_state_marginal_roi(channel, level, *, revenue_per_kpi=1.0)
¶
The return on one more currency unit a period, once level a period has been spent for
longer than the kernel remembers.
The adstock settles at level * sum(w), so a period's return is b h(level * sum(w)) and
its slope in the level b h'(level * sum(w)) sum(w): through a long unnormalised geometric
kernel of retention r, b h'(level / (1 - r)) / (1 - r).