Here’s the complete article formatted for publishing:
—
# Complete End-to-End Bayesian Marketing Mix Modeling Workflow with Google Meridian
In this tutorial, we build a complete Bayesian marketing mix modeling workflow using Google **Meridian**. We begin by installing the required libraries, verifying GPU availability, and exploring a geo-level marketing dataset that includes media impressions, spend, controls, promotions, conversions, population, and revenue. We then map the raw columns to Meridian’s data schema, define interpretable ROI-based priors, and configure the model before fitting it with prior and posterior NUTS sampling. After training, we evaluate convergence and predictive accuracy, examine channel contributions, ROI, marginal ROI, effectiveness, adstock, saturation, and response curves, and use the Analyzer API to extract custom posterior metrics. We conclude the workflow by optimizing both fixed and flexible budgets, generating shareable HTML reports, and saving the fitted model for reuse.
—
## Setting Up the Environment and Loading Data
We install Google Meridian with GPU-enabled TensorFlow support and import the libraries required for modeling, visualization, and analysis. We verify the runtime environment, detect available GPUs, and load Meridian’s simulated geo-level marketing dataset. We also perform initial exploratory analysis by reviewing data dimensions, date coverage, spend distribution, and national conversion trends.
“`python
!pip install –upgrade -q “google-meridian[and-cuda]”
import numpy as np
import pandas as pd
import altair as alt
import tensorflow as tf
import tensorflow_probability as tfp
from IPython.display import display, HTML
from meridian import constants
from meridian.data import load
from meridian.model import model
from meridian.model import spec
from meridian.model import prior_distribution
from meridian.analysis import analyzer
from meridian.analysis import visualizer
from meridian.analysis import optimizer
from meridian.analysis import summarizer
def show(chart_or_obj, title=None):
if title:
display(HTML(f”
{title}
“))
display(chart_or_obj)
print(“TensorFlow:”, tf.__version__)
gpus = tf.config.experimental.list_physical_devices(“GPU”)
print(“GPUs detected:”, gpus if gpus else “NONE — sampling will be slow on CPU!”)
“`
The code above sets up the computational environment and confirms whether a GPU is available to accelerate Bayesian inference. If no GPU is found, the model will still run but may be slower.
—
## Preparing the Data and Defining Schema
We map the raw dataset columns to Meridian’s expected schema using `CoordToColumns`. We define paid media, spend, organic channels, controls, treatments, population, KPI, and revenue-related fields before loading the structured input data.
“`python
coord_to_columns = load.CoordToColumns(
time=”time”,
geo=”geo”,
controls=[“competitor_sales_control”, “sentiment_score_control”],
population=”population”,
kpi=”conversions”,
revenue_per_kpi=”revenue_per_conversion”,
media=[
“Channel0_impression”,
“Channel1_impression”,
“Channel2_impression”,
“Channel3_impression”,
“Channel4_impression”,
],
media_spend=[
“Channel0_spend”,
“Channel1_spend”,
“Channel2_spend”,
“Channel3_spend”,
“Channel4_spend”,
],
organic_media=[“Organic_channel0_impression”],
non_media_treatments=[“Promo”],
)
media_to_channel = {f”Channel{i}_impression”: f”Channel_{i}” for i in range(5)}
media_spend_to_channel = {f”Channel{i}_spend”: f”Channel_{i}” for i in range(5)}
loader = load.CsvDataLoader(
csv_path=”https://storage.googleapis.com/meridian/data/simulated_data/csv/geo_all_channels.csv”,
kpi_type=”non_revenue”,
coord_to_columns=coord_to_columns,
media_to_channel=media_to_channel,
media_spend_to_channel=media_spend_to_channel,
)
data = loader.load()
print(“InputData loaded. Media tensor shape (geo, time, channel):”, data.media.shape)
“`
This step ensures that the data aligns with Meridian’s internal data structures, enabling proper modeling of channels, spend, and outcomes.
—
## Specifying Priors and Initializing the Model
We define ROI-based priors centered around realistic expectations using log-normal distributions. These priors reflect our belief about channel performance before seeing the data and will be updated during inference.
“`python
roi_mu = 0.2
roi_sigma = 0.9
prior = prior_distribution.PriorDistribution(
roi_m=tfp.distributions.LogNormal(roi_mu, roi_sigma, name=constants.ROI_M)
)
model_spec = spec.ModelSpec(prior=prior)
mmm = model.Meridian(input_data=data, model_spec=model_spec)
“`
The prior specification allows Meridian to incorporate domain knowledge while remaining flexible enough to adapt based on observed data.
—
## Sampling from Prior and Posterior
We first sample from the prior to understand baseline expectations, then fit the model using posterior NUTS sampling across multiple chains.
“`python
mmm.sample_prior(500)
mmm.sample_posterior(
n_chains=7,
n_adapt=500,
n_burnin=500,
n_keep=1000,
seed=1,
)
print(“Sampling complete.”)
“`
This step performs Bayesian inference, generating posterior distributions for all model parameters, including channel-level effects and saturation curves.
—
## Model Evaluation and Diagnostics
We evaluate convergence using R-hat diagnostics and compare prior and posterior distributions. We also assess model fit and predictive accuracy.
“`python
model_diagnostics = visualizer.ModelDiagnostics(mmm)
show(model_diagnostics.plot_rhat_boxplot(), “R-hat convergence check (want < 1.05)")
show(
model_diagnostics.plot_prior_and_posterior_distribution(),
"Prior vs. posterior (ROI parameters)",
)
model_fit = visualizer.ModelFit(mmm)
show(model_fit.plot_model_fit(), "Model fit: expected vs. actual outcome")
display(model_diagnostics.predictive_accuracy_table())
```R-hat values below 1.05 indicate good convergence. Comparing prior and posterior distributions helps verify that the data meaningfully updated our beliefs.---## Analyzing Channel Contributions and ROIWe analyze channel contributions, ROI, marginal ROI, effectiveness, adstock, and saturation to understand how each channel drives conversions.```python
media_summary = visualizer.MediaSummary(mmm)
show(media_summary.plot_channel_contribution_area_chart(),
"Outcome decomposition over time (baseline + channels)")
show(media_summary.plot_contribution_pie_chart(),
"Share of outcome: baseline vs. media")
show(media_summary.plot_spend_vs_contribution(),
"Spend share vs. contribution share (spot over/under-investment)")
show(media_summary.plot_roi_bar_chart(),
"ROI by channel (with credible intervals)")
show(media_summary.plot_roi_vs_effectiveness(),
"ROI vs. effectiveness (bubble = spend)")
show(media_summary.plot_roi_vs_mroi(),
"ROI vs. marginal ROI — mROI drives optimization, not average ROI")
```These visualizations help identify which channels are most effective, where diminishing returns set in, and where additional spend might be justified.---## Examining Nonlinear Effects and SaturationWe examine channel response curves, adstock decay, and Hill saturation functions to understand diminishing returns and carryover effects.```python
media_effects = visualizer.MediaEffects(mmm)
show(media_effects.plot_response_curves(),
"Response curves (incremental outcome vs. spend)")
show(media_effects.plot_adstock_decay(),
"Adstock decay by channel")
show(media_effects.plot_hill_curves(),
"Hill saturation curves by channel")
```These analyses reveal how response changes with increased spend and how past exposures influence current outcomes.---## Using the Analyzer API for Custom MetricsWe use the Analyzer API to extract posterior ROI draws and compute channel-level statistics, including credible intervals and probabilistic comparisons.```python
analysis = analyzer.Analyzer(mmm)
roi_draws = analysis.roi()
roi_np = np.asarray(roi_draws)
channels = list(data.media_channel.values)
roi_table = pd.DataFrame({
"channel": channels,
"roi_mean": roi_np.mean(axis=(0, 1)),
"roi_p05": np.quantile(roi_np, 0.05, axis=(0, 1)),
"roi_p95": np.quantile(roi_np, 0.95, axis=(0, 1)),
})
print("Posterior ROI summary (custom, from raw draws):")
display(roi_table)
p_better = (roi_np[..., 1] > roi_np[…, 0]).mean()
print(f”P(ROI Channel_1 > ROI Channel_0) = {p_better:.1%}”)
summary_metrics = analysis.summary_metrics()
print(“summary_metrics() xarray variables:”, list(summary_metrics.data_vars))
inc_outcome = np.asarray(analysis.incremental_outcome())
print(“Incremental outcome draws shape (chains, draws, channels):”, inc_outcome.shape)
“`
This step provides detailed, channel-specific insights and enables direct probabilistic comparisons between channels.
—
## Budget Optimization and Reporting
We optimize marketing spend under both fixed-budget and target-ROI scenarios, visualize recommended allocations, and generate shareable HTML reports.
“`python
budget_optimizer = optimizer.BudgetOptimizer(mmm)
optimization_results = budget_optimizer.optimize()
show(optimization_results.plot_budget_allocation(),
“Optimized budget allocation”)
show(optimization_results.plot_spend_delta(),
“Recommended spend change per channel”)
show(optimization_results.plot_incremental_outcome_delta(),
“Incremental outcome gained by reallocating”)
show(optimization_results.plot_response_curves(),
“Response curves with current vs. optimal spend points”)
flexible_results = budget_optimizer.optimize(
fixed_budget=False,
target_roi=1.5,
)
show(flexible_results.plot_budget_allocation(),
“Flexible-budget allocation at target ROI = 1.5”)
“`
Optimization helps translate model insights into actionable budget recommendations. The flexible-budget scenario shows how to achieve a target ROI with dynamically allocated spend.
—
## Saving and Reloading the Model
We export HTML reports and save the fitted model for future reuse, ensuring reproducibility and efficiency.
“`python
mmm_summarizer = summarizer.Summarizer(mmm)
mmm_summarizer.output_model_results_summary(
“model_results_summary.html”, “/content”, “2021-01-25”, “2024-01-15”
)
optimization_results.output_optimization_summary(
“budget_optimization_summary.html”, “/content”
)
print(“Reports written to /content/model_results_summary.html ”
“and /content/budget_optimization_summary.html”)
save_path = “/content/saved_mmm.pkl”
model.save_mmm(mmm, save_path)
mmm_reloaded = model.load_mmm(save_path)
print(“Model saved and reloaded from”, save_path)
roi_reloaded = np.asarray(analyzer.Analyzer(mmm_reloaded).roi()).mean(axis=(0, 1))
print(“Reloaded ROI means:”, np.round(roi_reloaded, 3))
“`
Saving the model allows teams to avoid re-running expensive sampling steps and enables rapid scenario testing.
—
## Conclusion
In conclusion, we developed an end-to-end framework for measuring media performance and translating Bayesian model estimates into practical marketing decisions. We validated the model using convergence diagnostics and predictive metrics before interpreting channel-level results, helping us avoid relying on unstable or misleading estimates.
We assessed each channel using contribution, ROI, marginal ROI, effectiveness, carryover, and saturation, and used posterior draws to quantify uncertainty and compare channels probabilistically. We then converted these insights into optimized budget allocations under fixed-budget and target-ROI scenarios. Finally, we exported the results and persisted the fitted model, allowing us to repeat analysis, test new scenarios, and adapt the workflow to real business data without rerunning the most computationally expensive steps.
—
## Frequently Asked Questions (FAQ)
**What is Google Meridian and why is it used here?**
Google Meridian is a probabilistic marketing mix modeling platform built on TensorFlow Probability. It enables Bayesian inference of channel contributions, saturation effects, and carryover using flexible hierarchical models. It is used here to demonstrate a complete workflow from data loading to optimization and reporting.
**Do I need a GPU to run this tutorial?**
No, but a GPU significantly speeds up sampling. The code detects GPU availability and will fall back to CPU if needed.
**What kind of dataset is used in this tutorial?**
The tutorial uses a simulated geo-level marketing dataset that includes weekly impressions, spend, controls, promotions, conversions, population, and revenue across multiple channels.
**How does Meridian handle diminishing returns and carryover?**
Meridian uses Hill functions and adstock transformations to model saturation and carryover effects, capturing realistic nonlinear responses to spend.
**Can I use my own data with this workflow?**
Yes. You can replace the data loading and `CoordToColumns` mapping with your dataset’s column names and structure.
**How do I interpret ROI and marginal ROI?**
ROI represents average return per unit spend, while marginal ROI reflects the incremental return of an additional dollar of spend — which is used for optimization.
**What does R-hat indicate?**
R-hat measures convergence of multiple MCMC chains. Values below 1.05 generally indicate good convergence.
**How can I compare channels probabilistically?**
Posterior ROI draws allow computation of probabilities such as P(ROI_ch1 > ROI_ch0), providing direct probabilistic comparisons.
—
**Check out the FULL CODES here. Also, feel free to follow us on Twitter and don’t forget to join our 150k+ ML SubReddit and Subscribe to our Newsletter. Wait! are you on telegram? now you can join us on telegram as well.**
**Need to partner with us for promoting your GitHub Repo OR Hugging Face Page OR Product Release OR Webinar etc.? Connect with us**
—
*Thanks for reading — next steps with YOUR data:*
1. Replace `CSV_URL` and `CoordToColumns` with your columns.
2. Calibrate per-channel ROI priors with experiment results.
3. Check R-hat < 1.05 before trusting any output.
4. Use `holdout_id` in `ModelSpec` for out-of-sample validation.---



