Google has released Meridian, an open-source tool for end-to-end Bayesian marketing mix modeling that handles media measurement, ROI analysis, and budget optimisation. The software runs on TensorFlow Probability and uses Hamiltonian Monte Carlo to sample from posterior distributions. It requires a dataset containing media impressions, spend, control variables, promotions, conversions, population figures, and revenue.
In this article
Installation and environment checks
The first step involves installing the package with GPU support for TensorFlow. The code verifies that the runtime environment is ready and lists available graphics cards. If no GPUs are detected, the sampling process will run on the CPU, which slows execution.
!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"<h3 style='font-family:sans-serif'>{title}</h3>"))
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!")
CSV_URL = (
"https://raw.githubusercontent.com/google/meridian/refs/heads/main/"
"meridian/data/simulated_data/csv/geo_all_channels.csv"
)
df = pd.read_csv(CSV_URL)
print("\nShape:", df.shape)
print("Geos:", df["geo"].nunique(), "| Weeks:", df["time"].nunique())
print("Date range:", df["time"].min(), "->", df["time"].max())
display(df.head())
spend_cols = [c for c in df.columns if c.endswith("_spend")]
spend_share = df[spend_cols].sum().rename("total_spend").reset_index()
spend_share["share_%"] = 100 * spend_share["total_spend"] / spend_share["total_spend"].sum()
display(spend_share)
kpi_by_week = df.groupby("time")["conversions"].sum().reset_index()
show(
alt.Chart(kpi_by_week).mark_line().encode(
x=alt.X("time:T", title="Week"),
y=alt.Y("conversions:Q", title="Total conversions (all geos)"),
).properties(width=700, height=250),
"National KPI over time",
)
The script loads the necessary libraries for modelling, visualisation, and analysis. It checks for the TensorFlow version and lists detected GPUs. The code then pulls a simulated geo-level dataset from a public GitHub repository. Initial exploratory analysis reviews the data dimensions, date coverage, spend distribution, and national conversion trends.
Data mapping and model specification
The raw dataset columns map to Meridian’s expected schema using the CoordToColumns class. The configuration defines paid media, spend, organic channels, control variables, and treatments. The dataset includes population, a key performance indicator, and revenue per conversion fields. The code loads the structured input data.
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=CSV_URL,
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("\nInputData loaded. Media tensor shape (geo, time, channel):", data.media.shape)
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 code configures ROI-based priors, creates the model specification, and initialises the Meridian model. The input data shape reflects the geo, time, and channel dimensions.
Fitting the model and checking convergence
The workflow samples from the prior and fits the Bayesian model using posterior NUTS sampling across multiple chains. The configuration sets the number of adaptation steps, burn-in periods, and retained draws. After training, the code evaluates convergence using R-hat diagnostics.
mmm.sample_prior(500)
mmm.sample_posterior(
n_chains=7,
n_adapt=500,
n_burnin=500,
n_keep=1000,
seed=1,
)
print("Sampling complete.")
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())
media_summary = visualizer.MediaSummary(mmm)
display(media_summary.summary_table())
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")
The code compares prior and posterior distributions and assesses model fit against observed outcomes. It displays a table of predictive accuracy and a summary of the media data. Visualisations include an area chart of outcome decomposition over time, a pie chart of share of outcome between baseline and media, and a bar chart showing ROI by channel with credible intervals.
Visualising response curves and effects
The visualiser examines channel response curves, adstock decay, and Hill saturation behaviour to understand diminishing returns and carryover effects. The code uses the Analyzer API to extract posterior ROI draws and calculate channel-level means and credible intervals.




