| name | pymc-marketing-mmm-clv |
| description | Bayesian marketing analytics with PyMC for Media Mix Modeling (MMM), Customer Lifetime Value (CLV), and BTYD models |
| triggers | ["create a media mix model","analyze marketing campaign effectiveness","build a customer lifetime value model","optimize marketing budget allocation","implement bayesian marketing mix modeling","calculate ROAS and channel contributions","forecast customer lifetime value","setup MMM with adstock and saturation"] |
PyMC-Marketing: Bayesian Marketing Analytics
Skill by ara.so — Marketing Skills collection.
PyMC-Marketing is a Bayesian marketing analytics toolbox built on PyMC. It provides production-ready implementations for Media Mix Modeling (MMM), Customer Lifetime Value (CLV), and Buy-Till-You-Die (BTYD) models with full probabilistic inference capabilities.
Installation
Basic Installation
conda create -c conda-forge -n marketing_env pymc-marketing
conda activate marketing_env
pip install pymc-marketing
Docker Installation
The project provides Docker support for Jupyter-based workflows:
cd scripts/docker
docker build -t pymc-marketing .
docker run -p 8888:8888 pymc-marketing
Core Capabilities
1. Media Mix Modeling (MMM)
MMM helps quantify the impact of marketing channels on business outcomes with:
- Adstock transformations: Geometric, delayed, Weibull
- Saturation effects: Logistic, Michaelis-Menten, Tanh
- Time-varying effects: Intercept and media contribution dynamics
- Budget optimization: ROI-maximizing allocation across channels
- Experiment calibration: Lift test integration
2. Customer Lifetime Value (CLV)
Probabilistic models for customer value prediction:
- Beta-Geometric/NBD: For non-contractual settings
- Pareto/NBD: Customer dropout modeling
- Gamma-Gamma: Monetary value modeling
3. Customer Choice Analysis (CSA)
Discrete choice modeling for understanding customer preferences.
Media Mix Modeling (MMM)
Basic MMM Setup
import pandas as pd
from pymc_marketing.mmm import MMM, GeometricAdstock, LogisticSaturation
data = pd.read_csv("marketing_data.csv", parse_dates=["date"])
mmm = MMM(
adstock=GeometricAdstock(l_max=8),
saturation=LogisticSaturation(),
date_column="date",
channel_columns=["tv", "radio", "digital", "social"],
control_columns=["holiday", "promotion", "temperature"],
yearly_seasonality=2,
)
X = data.drop("sales", axis=1)
y = data["sales"]
mmm.fit(X, y)
Available Adstock Functions
from pymc_marketing.mmm import (
GeometricAdstock,
DelayedAdstock,
WeibullAdstock,
)
adstock = GeometricAdstock(l_max=8, normalize=True)
adstock = DelayedAdstock(l_max=12, theta_prior_params={"alpha": 2, "beta": 1})
adstock = WeibullAdstock(l_max=10, mode_prior_params={"mu": 2, "sigma": 1})
Available Saturation Functions
from pymc_marketing.mmm import (
LogisticSaturation,
MichaelisMentenSaturation,
TanhSaturation,
)
saturation = LogisticSaturation()
saturation = MichaelisMentenSaturation()
saturation = TanhSaturation()
Time-Varying Effects
from pymc_marketing.mmm import MMM
from pymc_marketing.mmm.components.adstock import GeometricAdstock
from pymc_marketing.mmm.components.saturation import LogisticSaturation
mmm = MMM(
adstock=GeometricAdstock(l_max=8),
saturation=LogisticSaturation(),
date_column="date",
channel_columns=["tv", "digital"],
time_varying_intercept=True,
intercept_m=100,
)
mmm = MMM(
adstock=GeometricAdstock(l_max=8),
saturation=LogisticSaturation(),
date_column="date",
channel_columns=["tv", "digital"],
time_varying_media=True,
media_m=50,
)
Model Diagnostics and Visualization
mmm.plot_trace()
mmm.plot_components_contributions()
mmm.plot_channel_contribution_share_hdi()
mmm.plot_posterior_predictive(original_scale=True)
mmm.plot_curve()
Budget Optimization
budget_allocator = mmm.allocate_budget(
total_budget=1_000_000,
budget_bounds={
"tv": (100_000, 500_000),
"digital": (50_000, 400_000),
"radio": (0, 200_000),
},
num_days=90,
)
optimal = budget_allocator.allocate()
print(optimal)
mmm.plot_budget_allocation(
total_budget=1_000_000,
budget_bounds={"tv": (0, 500_000), "digital": (0, 500_000)},
)
ROAS and Channel Efficiency
roas = mmm.compute_mean_roas()
print(roas)
contributions = mmm.compute_channel_contribution_original_scale()
print(contributions)
marginal_roas = mmm.compute_marginal_roas(spend_grid_size=20)
Lift Test Calibration
from pymc_marketing.mmm.lift_test import add_lift_measurements_to_likelihood
lift_test_results = pd.DataFrame({
"channel": ["tv", "digital"],
"lift": [0.15, 0.22],
"sigma": [0.03, 0.04],
})
mmm_calibrated = MMM(
adstock=GeometricAdstock(l_max=8),
saturation=LogisticSaturation(),
date_column="date",
channel_columns=["tv", "digital", "radio"],
control_columns=["holiday"],
)
mmm_calibrated.fit(
X, y,
prior_fn=lambda model: add_lift_measurements_to_likelihood(
model, lift_test_results
)
)
Out-of-Sample Prediction
future_data = pd.DataFrame({
"date": pd.date_range("2024-01-01", periods=52, freq="W"),
"tv": [50000] * 52,
"digital": [30000] * 52,
"radio": [10000] * 52,
"holiday": [0] * 52,
})
predictions = mmm.sample_posterior_predictive(
X_pred=future_data,
extend_idata=True,
combined=True,
)
forecast = predictions.posterior_predictive["y"].mean(dim=["chain", "draw"])
Alternative NUTS Samplers
mmm.fit(X, y, nuts_sampler="numpyro", chains=4, draws=2000)
mmm.fit(X, y, nuts_sampler="blackjax", chains=4, draws=2000)
mmm.fit(X, y, nuts_sampler="nutpie", chains=4, draws=2000)
mmm.fit(X, y, nuts_sampler="pymc", chains=4, draws=2000)
Customer Lifetime Value (CLV)
Beta-Geometric/NBD Model
from pymc_marketing.clv import BetaGeoModel
import pandas as pd
rfm_data = pd.DataFrame({
"customer_id": [1, 2, 3, 4, 5],
"frequency": [5, 2, 8, 1, 3],
"recency": [10, 5, 15, 2, 8],
"T": [20, 20, 20, 20, 20],
})
bg_model = BetaGeoModel(
data=rfm_data,
)
bg_model.fit()
expected_purchases = bg_model.expected_num_purchases(
t=30,
frequency=rfm_data["frequency"],
recency=rfm_data["recency"],
T=rfm_data["T"],
)
prob_alive = bg_model.expected_probability_alive(
frequency=rfm_data["frequency"],
recency=rfm_data["recency"],
T=rfm_data["T"],
)
Pareto/NBD Model
from pymc_marketing.clv import ParetoNBDModel
pareto_model = ParetoNBDModel(data=rfm_data)
pareto_model.fit()
clv_12_months = pareto_model.expected_num_purchases(
t=365,
frequency=rfm_data["frequency"],
recency=rfm_data["recency"],
T=rfm_data["T"],
)
Gamma-Gamma Model (Monetary Value)
from pymc_marketing.clv import GammaGammaModel
monetary_data = pd.DataFrame({
"customer_id": [1, 2, 3, 4, 5],
"frequency": [5, 2, 8, 1, 3],
"monetary_value": [250, 180, 420, 90, 310],
})
gg_model = GammaGammaModel(data=monetary_data)
gg_model.fit()
expected_avg_value = gg_model.expected_customer_spend(
frequency=monetary_data["frequency"],
monetary_value=monetary_data["monetary_value"],
)
total_clv = expected_purchases * expected_avg_value
Model Saving and Loading
mmm.save("mmm_model.nc")
from pymc_marketing.mmm import MMM
mmm_loaded = MMM.load("mmm_model.nc")
mmm_loaded.sample_posterior_predictive(X_new)
Configuration Patterns
Custom Priors
import pymc as pm
mmm = MMM(
adstock=GeometricAdstock(l_max=8),
saturation=LogisticSaturation(),
date_column="date",
channel_columns=["tv", "digital"],
control_columns=["holiday"],
)
with mmm.model:
mmm.model["beta_channel"] = pm.HalfNormal(
"beta_channel_custom",
sigma=2,
shape=2
)
mmm.fit(X, y)
Sampler Configuration
mmm.fit(
X, y,
chains=4,
draws=3000,
tune=2000,
target_accept=0.95,
random_seed=42,
)
Troubleshooting
Convergence Issues
import arviz as az
az.summary(mmm.idata, var_names=["beta_channel", "alpha"])
mmm.fit(X, y, tune=3000, draws=2000)
Long Fitting Times
mmm.fit(X, y, nuts_sampler="numpyro")
mmm.fit(X, y, chains=2, draws=1000, tune=1000)
mmm.sample_prior_predictive(samples=500)
Memory Issues
mmm.fit(X, y, draws=2000, tune=1000)
thinned_idata = mmm.idata.sel(draw=slice(None, None, 2))
Data Scaling Issues
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X[["tv", "digital", "radio"]] = scaler.fit_transform(
X[["tv", "digital", "radio"]]
)
Common Workflows
End-to-End MMM Analysis
import pandas as pd
from pymc_marketing.mmm import MMM, GeometricAdstock, LogisticSaturation
data = pd.read_csv("marketing_data.csv", parse_dates=["date"])
X = data.drop("sales", axis=1)
y = data["sales"]
mmm = MMM(
adstock=GeometricAdstock(l_max=8),
saturation=LogisticSaturation(),
date_column="date",
channel_columns=["tv", "digital", "radio"],
control_columns=["holiday", "promotion"],
yearly_seasonality=2,
)
mmm.fit(X, y, chains=4, draws=2000, tune=2000)
mmm.plot_trace()
mmm.plot_posterior_predictive(original_scale=True)
mmm.plot_components_contributions()
roas = mmm.compute_mean_roas()
print(f"ROAS by channel:\n{roas}")
optimal_budget = mmm.allocate_budget(
total_budget=1_000_000,
budget_bounds={
"tv": (100_000, 500_000),
"digital": (50_000, 400_000),
"radio": (0, 200_000),
},
num_days=90,
)
print(f"Optimal allocation:\n{optimal_budget.allocate()}")
CLV Prediction Pipeline
from pymc_marketing.clv import BetaGeoModel, GammaGammaModel
rfm = customer_transactions.groupby("customer_id").agg({
"transaction_date": lambda x: (x.max() - x.min()).days,
"order_id": "count",
"revenue": "mean",
})
rfm["frequency"] = rfm["order_id"] - 1
rfm["T"] = (pd.Timestamp.now() - customer_first_purchase["date"]).dt.days
bg_model = BetaGeoModel(data=rfm[["frequency", "recency", "T"]])
bg_model.fit()
repeat_customers = rfm[rfm["frequency"] > 0]
gg_model = GammaGammaModel(data=repeat_customers[["frequency", "revenue"]])
gg_model.fit()
expected_purchases = bg_model.expected_num_purchases(
t=365, frequency=rfm["frequency"], recency=rfm["recency"], T=rfm["T"]
)
expected_value = gg_model.expected_customer_spend(
frequency=rfm["frequency"], monetary_value=rfm["revenue"]
)
clv_12m = expected_purchases * expected_value
Environment Variables
import os
os.environ["AWS_ACCESS_KEY_ID"] = "your_key_id"
os.environ["AWS_SECRET_ACCESS_KEY"] = "your_secret_key"
os.environ["MLFLOW_TRACKING_URI"] = "http://mlflow-server:5000"
os.environ["PYMC_BACKEND"] = "jax"
Additional Resources