Google Meridian is Google’s open-source marketing mix modeling framework: a Python library that fits a Bayesian model of your sales or KPI on media spend, controls and time effects, at national level or across regions, and that lets you state your prior beliefs about each channel’s ROI directly. Google made it generally available on 29 January 2025 as the official successor to its earlier LightweightMMM, and in October 2026 it’s on version 2.1.0, with reach and frequency modeling, experiment calibration, a budget optimizer and a scenario planner built in.
I’ve advised Google on marketing mix modeling, and the original Vexpower MMM path taught LightweightMMM as the Python option, so I’ve watched this project go from an “unofficial” library to the thing Google’s measurement teams now point advertisers towards. Meridian is the most opinionated of the big open-source tools, and the opinion is a Bayesian one: Your model starts from priors, and in Meridian the main priors are on ROI, which is the number marketers actually argue about. That makes it unusually easy to bring experiment results and business knowledge into a model, and unusually easy to bake in assumptions without noticing. I’ll explain how it works, what the default priors really believe (with an explorer you can use on your own numbers), and how to run it, using code I’ve tested against the current release.
Where Meridian came from: from LightweightMMM to version 2
Meridian builds on a line of Google research from 2017 onwards, in particular the Jin et al. paper on carryover and shape effects and the geo-level hierarchical model by Sun and colleagues, plus later work on ROI priors and on reach and frequency. The GitHub repository dates from January 2024, and LightweightMMM’s README records that Meridian was released under limited availability for selected advertisers on 7 March 2024. Tagged releases on GitHub started that summer (0.2.0 in July 2024), version 1.0.0 landed on 24 January 2025 and was the first release on PyPI, and LightweightMMM’s README was updated on 29 January 2025 to say Meridian was now available to everyone and that LightweightMMM was no longer supported.
Development has been brisk since. The changelog shows a steady run of 1.x releases through 2025 and 2026, adding automatic knot selection, an exploratory data analysis engine, model-quality checks, serialisation and the scenario planner. Version 2.0.0 on 2 September 2026 made the biggest change: It switched the default backend from TensorFlow to JAX (the TensorFlow backend still exists), added prior calibration from incrementality experiments, including Google’s new open-source geo-testing tool Meridian GeoX, and standardized how dates are passed to the analysis functions, which broke some older code. Version 2.1.0 followed on 17 September 2026 with a more readable way to specify calibration windows and holdouts using dates and channel names rather than index arrays. If you’re reading a tutorial from 2025, expect small API differences.
How the Meridian model works
At its core Meridian is a regression, like every MMM, but each part of it has a prior distribution and the whole thing is estimated by MCMC (the No-U-Turn Sampler, which draws thousands of plausible parameter sets consistent with both the priors and the data). Each media channel is transformed with adstock for carryover, using geometric or binomial decay over a maximum lag of eight periods by default, and a Hill saturation curve for diminishing returns. By default the Hill slope for paid media is fixed at one, which restricts every curve to be concave (no S-shapes), a deliberate choice so the budget optimizer always finds a single best answer. Adstock is applied before saturation by default; you can flip that with hill_before_adstock. Time effects such as trend and seasonality are handled with “knots”, a piecewise-linear baseline whose flexibility you control with the number of knots, and since 2025 there’s an automatic knot selection option (enable_aks=True).
Geo-level data is where Meridian earns its keep. If you have weekly data by region (DMAs, states, countries), each region gets its own media coefficients, but they’re drawn from a shared distribution, so regions with little data borrow strength from the others (partial pooling, which I explain on the hierarchical MMM page). Control variables are hierarchical across regions as well. Fifty regions over two years give you 5,200 observations instead of 104, and that extra variation, spend moving differently in different places, is often what separates a model that can tell channels apart from one that can’t. Meridian works nationally too, but you give up that advantage, as you’ll see in my run below.
Two other inputs are worth knowing about. For video and other channels where you can get reach and frequency, Meridian can model the two separately, with Hill and adstock applied in a fixed order, which lets it recommend an optimal frequency as well as a budget. And for paid search, Google recommends including Google search query volume as a control, to separate organic demand from the effect of the ads.
ROI priors: the default and what it actually believes
In most Bayesian MMMs you set priors on regression coefficients, which nobody in a marketing meeting can interpret. Meridian’s default is to parameterize paid media by ROI instead: You set a prior on each channel’s return on investment, and the model works out what coefficient that implies given the spend and the transformations. You can also choose priors on marginal ROI or on contribution share, or go back to plain coefficients (paid_media_prior_type in ModelSpec).
The default ROI prior for every paid channel is LogNormal(0.2, 0.9). Google’s documentation gives its median as about 1.22 and its mean as about 1.83. Its quoted intervals (80 percent between 0.5 and 6.0, 95 percent between 0.25 and 9.0) don’t quite match those parameters, though: Computed directly, 80 percent of LogNormal(0.2, 0.9) lies between about 0.39 and 3.9, and 90 percent between 0.28 and 5.4, which is what the explorer below shows. That’s a reasonable “we don’t know much” prior for most advertisers, but notice what it implies: about a 41 percent chance, before seeing any data, that a channel doesn’t pay back. Load the default below and then the experiment preset, and you’ll see how much an informative prior narrows things.
The “from an experiment” preset assumes a test found an ROI of 1.8 with a standard error of 0.3, and widens the SD to 0.5 before using it. That widening matters. Google’s calibration guidance points out that an experiment’s ROI and the model’s ROI often aren’t the same quantity: The model’s counterfactual is zero spend over the whole period, while a test measures the lift from a particular amount of spend over a few weeks, often at a different spend level, in a different season. So the prior should carry extra uncertainty beyond the test’s own standard error, and there’s no single formula for how much. If your experiment only applies to a window, ModelSpec can restrict the prior to that period (roi_calibration, or the older roi_calibration_period array), though Google recommends setting it over the full modeling window where you can. I cover the trade-offs of folding tests into a model on the calibration page, and the general logic of priors on the Bayesian MMM page.
Calibration, GeoX and the partner ecosystem
Calibration in Meridian means turning outside evidence into priors. You can do it by hand, as in the explorer, using two helpers in the prior_distribution module, lognormal_dist_from_range() (a low and high value with a probability mass between them) and lognormal_dist_from_mean_std(). Since version 2.0 there’s also a CalibrationBuilder that collects experiment results and builds calibrated ROI priors for you, and a recommendation feature that suggests which channels are most worth testing. Meridian GeoX, a separate open-source repository (google/meridian-geox), designs and analyzes geo experiments, including multi-cell tests, and converts their results into Meridian priors.
Around the library sits an ecosystem that’s useful to know about even if you never touch it. Google’s Meridian Partners directory listed 52 certified partners in October 2026, filterable by region and by whether they offer MMM, GeoX or both; the list is mostly agencies and measurement consultancies such as Adswerve, Jellyfish, Accenture, KINESSO and Ekimetrics. There’s an MMM Data Platform, available by request, that helps advertisers pull campaign data from Google Ads and Display & Video 360 into the right shape. And the repository now ships “agent skills” that help AI coding assistants configure models, which says something about where Google expects people to be writing their Meridian code.
How to install and run Meridian in Python
Meridian needs Python 3.11 to 3.13 and, for anything beyond a toy model, a GPU; the README says it’s tested on a T4 with 16 GB of RAM, and Google’s getting-started notebook takes about 25 minutes to sample on Colab’s free T4. On Linux with an NVIDIA GPU you install the CUDA extra; on a Mac or CPU-only machine, the plain package (there’s no official GPU support on macOS):
pip install --upgrade "google-meridian[and-cuda]" # Linux + NVIDIA GPU
pip install --upgrade google-meridian # macOS or CPU only
To check the current API end to end, I fitted Meridian 2.1.0 to the 104-week practice dataset used across this library, on a laptop CPU. The data has revenue and spend for search, social and video plus a promotion flag, and no regions, so this is a national model. The data goes in through a builder that maps your DataFrame columns to roles (spend doubles as the media metric because the file has no impressions):
import numpy as np
import pandas as pd
import tensorflow_probability.substrates.jax as tfp_jax
from meridian import constants
from meridian.analysis import analyzer, optimizer, visualizer
from meridian.data import data_frame_input_data_builder
from meridian.model import model, prior_distribution, spec
df = pd.read_csv("mmm-practice-synthetic.csv").rename(columns={"week": "time"})
channels = ["search", "social", "video"]
builder = data_frame_input_data_builder.DataFrameInputDataBuilder(
kpi_type="revenue", default_kpi_column="revenue"
)
data = (
builder.with_kpi(df)
.with_controls(df, control_cols=["promotion"])
.with_media(
df,
media_cols=[f"{c}_spend" for c in channels],
media_spend_cols=[f"{c}_spend" for c in channels],
media_channels=channels,
)
.build()
)
Then the priors and the model. Here search gets an informative prior, as if a test had put its ROI between 1.2 and 2.0 with 90 percent probability, and the other two keep the default LogNormal(0.2, 0.9). Passing arrays to LogNormal gives each channel its own prior, in channel order:
search = prior_distribution.lognormal_dist_from_range(1.2, 2.0, mass_percent=0.9)
prior = prior_distribution.PriorDistribution(
roi_m=tfp_jax.distributions.LogNormal(
np.array([float(search.loc), 0.2, 0.2]),
np.array([float(search.scale), 0.9, 0.9]),
name=constants.ROI_M,
)
)
mmm = model.Meridian(input_data=data, model_spec=spec.ModelSpec(prior=prior))
mmm.sample_prior(200)
mmm.sample_posterior(n_chains=4, n_adapt=500, n_burnin=500, n_keep=500, seed=0)
Google’s own demo uses 10 chains with 1,000 adaptation steps, 500 burn-in and 1,000 kept draws, which is the better default if you have a GPU. After sampling, check convergence and look at the results. ModelReviewer(mmm).run() produces a health card of automated checks, the r-hat summary tells you whether the chains agree (values near 1.0 are good; Google treats under 1.2 as approximate convergence), and MediaSummary gives you the table of spend, contribution, ROI, marginal ROI and cost per incremental KPI with credible intervals:
mmm_analyzer = analyzer.Analyzer(model_context=mmm.model_context, inference_data=mmm.inference_data)
print(mmm_analyzer.rhat_summary())
print(visualizer.MediaSummary(mmm).summary_table())
visualizer.ModelFit(mmm).plot_model_fit()
visualizer.MediaEffects(mmm).plot_response_curves()
results = optimizer.BudgetOptimizer(mmm).optimize() # fixed budget, ±30% per channel
results.output_optimization_summary("optimization_output.html", ".")
On my run the whole script took under a minute on a laptop CPU, every r-hat was under 1.01, and the posterior ROIs were 1.5 for search (90 percent credible interval 1.1 to 1.9), 1.2 for social (0.3 to 2.6) and 0.7 for video (0.2 to 1.7). Because the practice data is synthetic, you can check those against the truth, and it’s worth knowing exactly what that truth is: The file was generated from a plain linear equation with no noise, revenue = 20,000 + 1.4 × search + 0.8 × social + 0.6 × video + 3,000 × sin(πt/26) + 1,800 × promotion, with no adstock and no saturation at all. So the true ROIs are 1.4, 0.8 and 0.6. Search’s interval is tight mostly because I gave it a tight prior, and the posterior barely moved from it, which is exactly the “baked-in assumption” risk (here the prior happened to be close to right). Social’s estimate is too high and its interval is wide, because 104 national weeks with three correlated channels don’t contain much information once Meridian also has to estimate decay, half-saturation and a time baseline, so the default prior is doing a lot of the work. The fitted decay rates (posterior means between 0.3 and 0.5, with intervals spanning almost 0 to 0.9) aren’t a finding either: The true carryover is zero, and the model simply can’t rule out other values. The default optimization then moved about a third of video’s budget to search and social, which happens to be the right direction, but with intervals that wide I’d want a test on video before acting on it. (These are Vexpower’s synthetic numbers, not a benchmark.)
For a reusable model, save it with meridian_serde.save_meridian(mmm, "model.binpb") from meridian.schema.serde (it needs the schema extra, pip install "google-meridian[schema]"), and load it later with load_meridian, so you don’t resample every time you want a chart. The Summarizer class writes a two-page HTML results report, and optimize() accepts a budget, a fixed or flexible budget scenario (flexible lets total spend change to hit a target ROI or marginal ROI), spend constraints per channel, and hypothetical future data for planning.
The budget optimizer and Scenario Planner
BudgetOptimizer.optimize() defaults to a fixed-budget scenario over the historical period: Keep total spend the same, and let each channel move up to 30 percent either way from its historical share to maximize incremental outcome. Because the default media curves are concave, there’s a single optimum. For planning ahead, you pass new data (spend patterns, costs and revenue per KPI for the future period) and the optimizer uses those instead of the history. The output includes a two-page HTML summary with the optimized allocation, the change in ROI and the response curves. I’d read that alongside my notes on budget allocation and scenario planning, because an optimizer will happily push spend into the part of a curve your data never observed.
The Meridian Scenario Planner, in open beta, takes a saved model and generates an interactive dashboard in Data Studio from a Colab notebook (Google’s notebooks also call it Looker Studio). Viewers can check model fit, see contribution and ROI by channel, compare ROI with marginal ROI, and rerun fixed or flexible budget optimizations with their own date ranges and channel bounds, without touching Python. For teams where the analyst isn’t the person making budget calls, that’s a meaningful improvement on emailing HTML files around, though the usual rule applies: Give people viewer rather than editor access, as Google’s own page recommends.
Strengths, limits and who Meridian is for
Meridian’s strengths follow from its design. ROI priors make Bayesian MMM legible to marketers and give experiments a natural way in, geo hierarchy extracts far more signal from regional data than any national model can, reach and frequency support is unique among the major open-source tools, and the surrounding pieces (health checks, two-pagers, the optimizer, the scenario planner, GeoX) mean you’re not building your own reporting layer. It’s also under heavier active development than any of its peers, with releases most months in 2026.
The limits are mostly about cost and control. Sampling is slow without a GPU once you have many regions, and the TensorFlow-to-JAX switch in 2.0 shows the API is still moving. The defaults are opinionated, concave curves only, a common ROI prior for all channels, and if you accept them unexamined, they shape your answer more than you might think on thin data, as my run shows. Google says it answers GitHub questions in weekly batches and rarely merges outside pull requests because the code is tied to internal systems, so you’re dependent on its roadmap. And as a Google product, it’s reasonable for a CFO to ask whether Google-built measurement flatters Google channels; the model is open source and nothing in it favors any channel, but it’s a fair question to be ready for, and an experiment-calibrated prior is the best answer to it.
I’d choose Meridian if you work in Python, have regional data or reach and frequency data, and have (or plan to run) experiments you want the model to respect; for a Google-heavy media mix it’s also the tool your Google account team will understand. If you want more freedom over the model’s structure, PyMC-Marketing is the more flexible Bayesian option, and if your team is in R with national data, Robyn is still a sound choice; the open-source library comparison puts all of them side by side. If you’re coming from LightweightMMM, my migration map lists where each function went. And if you’re still building intuition for what any of these models are doing, the Vexpower MMM course and the overview of what marketing mix modeling is are the place to start, because Meridian rewards people who know which assumptions they’re making.
