LightweightMMM: what Google's deprecated MMM did, and how to migrate to Meridian

LightweightMMM was the Python library a lot of marketers learned Bayesian MMM on. Google stopped supporting it when Meridian became generally available, and archived it in January 2026. Here's what it did and where each piece went.

Mike Taylor By Mike Taylor · Updated · 7 min read

LightweightMMM was Google’s open-source Bayesian marketing mix modeling library for Python, built on NumPyro and JAX, and it’s now deprecated: Google stopped supporting it when it made Meridian generally available on 29 January 2025, and archived the GitHub repository on 19 January 2026. If you have a LightweightMMM model in production, the advice from Google’s own README is to switch to Meridian, and the map below shows where each LightweightMMM function and argument went.

I spent a lot of time with this library. When I built the original Vexpower MMM path, LightweightMMM was the Python counterpart to Meta’s Robyn, and the legacy simulator for it was called Can we try Google LightweightMMM?, because in 2022 Google was getting into the game with a library that took a completely different approach from Meta’s. I also wrote How Google LightweightMMM Works, published on Recast’s blog (I’ve advised Recast on MMM), and recorded a walkthrough of Google’s LightweightMMM on YouTube; the archived README still lists both under my name in its community spotlight. So this page is partly a eulogy, but mostly a practical guide for anyone who has to move a LightweightMMM model somewhere else.

What LightweightMMM was

LightweightMMM appeared on GitHub in February 2022, with version 0.1.1 on PyPI that month and nine more releases up to 0.1.9 in May 2023. It was always labeled “not an official Google product”, and it was written by a group of Google engineers and data scientists (the citation lists Pablo Duque, Dirk Nachbar, Yuka Abe, Christiane Ahlheim, Mike Anderson, Yan Sun, Omri Goldstein and Tim Eck). The methods came straight from Google’s 2017 research papers: the carryover and shape effects paper by Jin and colleagues, the geo-level hierarchical model by Sun and colleagues, and Chan and Perry’s survey of MMM’s challenges. Where Robyn searched over thousands of ridge regressions, LightweightMMM fitted one Bayesian model with MCMC and gave you posterior distributions for every parameter.

The “lightweight” was real. You passed NumPy arrays of media, extra features, costs and the target, scaled them yourself, called fit(), and got a model you could plot, use for prediction and optimize a budget with. The whole API fitted on one page of the README:

import jax.numpy as jnp
from lightweight_mmm import lightweight_mmm, optimize_media, preprocessing

media_scaler = preprocessing.CustomScaler(divide_operation=jnp.mean)
media_data_train = media_scaler.fit_transform(media_data_train)
# ...the same for target, extra features and costs...

mmm = lightweight_mmm.LightweightMMM(model_name="hill_adstock")
mmm.fit(media=media_data_train, media_prior=costs, target=target_train,
        extra_features=extra_features_train,
        number_warmup=1000, number_samples=1000, number_chains=2)

media_effect_hat, roi_hat = mmm.get_posterior_metrics()
solution = optimize_media.find_optimal_budgets(
    n_time_periods=extra_features_test.shape[0], media_mix_model=mmm,
    budget=40, extra_features=extra_features_test, prices=prices)

That’s adapted from the archived README, which is the last word on the API since nothing will change now.

How the LightweightMMM model worked

The model was a regression of the KPI on an intercept, a trend, seasonality, transformed media and extra features. The trend was a power curve in time (a coefficient coef_trend times time raised to expo_trend, with the exponent’s prior between 0.5 and 1.5), and seasonality was a set of sine and cosine terms with two degrees and a 52-week period by default, plus optional day-of-week effects for daily data. Media coefficients had a HalfNormal prior whose scale was the channel’s cost, which you passed as media_prior, so channels you spent more on were allowed larger effects. For geo data you passed 3-D arrays and the media coefficients became hierarchical across regions.

The interesting choice was the media transformation, picked with model_name. There were three, and the README recommended trying all of them and keeping the one with the best out-of-sample fit:

  • adstock: Geometric carryover with a lag_weight (prior Beta(2, 1)), followed by raising the result to an exponent (prior Beta(9, 1)) for diminishing returns.
  • hill_adstock (the default): The same geometric adstock, followed by a Hill saturation curve with a half_max_effective_concentration and a slope, both with Gamma(1, 1) priors.
  • carryover: The delayed-peak carryover from the Jin paper, where the weight on lag l is ad_effect_retention_rate raised to the power (l − peak_effect_delay)², over 13 lags, followed by an exponent.

The geometric adstock was normalized by default (the output was multiplied by one minus the lag weight), and the optimizer, find_optimal_budgets(), held total spend fixed and let each channel move 20 percent either way by default, given a budget, a number of future periods and the price per media unit. It also needed a media_gap argument if there was a gap between the end of your training data and the period you were planning, so the adstock could be computed correctly. If those names are familiar, the map below tells you what replaced each of them.

LightweightMMM deprecated: the timeline

The end came in three steps, all recorded in the repository. On 7 March 2024, the README announced that Google had released Meridian under limited availability for selected advertisers and that LightweightMMM would be sunset once Meridian reached general availability. On 29 January 2025, the README was updated to say Meridian was available to everyone on GitHub, that “LMMM is not supported anymore”, and that Google highly recommends switching. The last commits followed in mid-2025, and on 19 January 2026 the repository was archived, which on GitHub means read-only: no new issues, pull requests or fixes.

In practice that means three things if you still depend on it. The package still installs from PyPI at 0.1.9, but it was written against the JAX and NumPyro versions of 2023 and won’t be updated as they change. Nobody will answer the issues you file. And any methodology questions from a finance team (“is this still the method Google recommends?”) now have an awkward answer. If you only need to reproduce an old result, pin your environment and keep it; for anything you’ll refresh, plan the move.

How LightweightMMM maps to Meridian

Google’s migration guide says the input data for the two projects is the same, and the conceptual core survives: Both are Bayesian models with geometric adstock and Hill saturation, sampled with NUTS, with a hierarchical option for geo data. What changes is almost everything around that core, and some of it changes your results, not just your code. Search the map for any function or argument you used:

Interactive
LightweightMMM to Meridian migration map

Search for a LightweightMMM function, argument or concept, or filter by stage, and see what replaces it in Meridian 2.1.

44 mappings

Setup
LightweightMMMpip install lightweight_mmm
Meridianpip install google-meridian (add [and-cuda] on Linux with an NVIDIA GPU)

Meridian needs Python 3.11 to 3.13. Install it in a fresh environment rather than on top of an old LightweightMMM one.

Setup
LightweightMMMNumPyro on JAX
MeridianTensorFlow Probability; JAX backend by default since 2.0 (TensorFlow still supported)

Distributions come from tensorflow_probability.substrates.jax in the current demo, not numpyro.distributions.

Setup
LightweightMMMStatus: archived 19 Jan 2026, last release 0.1.9 (May 2023)
MeridianActive: 2.1.0 (September 2026)

LightweightMMM’s README says it is not supported anymore and recommends Meridian.

Setup
LightweightMMMnumpyro.set_host_device_count(n) to run chains in parallel on CPU
MeridianRun on a GPU where you can (Google tests on a T4 with 16 GB RAM)

Meridian runs on CPU too, but geo models with many regions are slow without a GPU.

Data
LightweightMMMutils.simulate_dummy_data(data_size, n_media_channels, n_extra_features, geos)
MeridianSimulated CSVs in meridian/data/simulated_data/csv (for example geo_all_channels.csv)

The getting-started notebook reads geo_all_channels.csv straight from GitHub.

Data
LightweightMMMmedia array, 2-D (time, channel) or 3-D (time, channel, geo)
MeridianDataFrameInputDataBuilder(...).with_media(df, media_cols, media_spend_cols, media_channels)

Long-format DataFrame with time and (optionally) geo columns instead of NumPy arrays.

Data
LightweightMMMtarget
Meridianbuilder.with_kpi(df) with kpi_type="revenue" or "non_revenue"

For non-revenue KPIs, add with_revenue_per_kpi(df) so ROI can be expressed in money.

Data
LightweightMMMextra_features
Meridianbuilder.with_controls(df, control_cols=[...])

Variables you control and might change, such as promotions or price, can go in with_non_media_treatments instead.

Data
LightweightMMMmedia_prior (cost per channel, also used as the HalfNormal scale of the media coefficient)
Meridianmedia_spend_cols in with_media

Meridian uses spend to turn ROI priors into coefficients, rather than using cost as a prior scale.

Data
LightweightMMMpreprocessing.CustomScaler(divide_operation=jnp.mean) for media, target, costs and features
MeridianAutomatic

Google’s migration table lists input scaling as manual in LightweightMMM and automatic in Meridian. Drop your scalers.

Data
LightweightMMMNo population input
Meridianbuilder.with_population(df)

One population per geo. Meridian uses it to scale the KPI and media across geos of different sizes.

Model
LightweightMMMLightweightMMM(model_name="hill_adstock") (the default)
Meridianmodel.Meridian(input_data=data, model_spec=spec.ModelSpec(...))

Meridian’s default is adstock then Hill, the closest match. Set hill_before_adstock=True to reverse the order.

Model
LightweightMMMmodel_name="adstock" (adstock plus an exponent for saturation)
MeridianNo direct equivalent

Meridian only supports Hill saturation or none (saturation_spec="hill" or "none"), so expect a different curve shape.

Model
LightweightMMMmodel_name="carryover" (ad_effect_retention_rate, peak_effect_delay, number_lags=13)
MeridianNo delayed-peak adstock; adstock_decay_spec="geometric" (default) or "binomial", max_lag=8 by default

If a delayed peak mattered in your LightweightMMM results, check the response timing carefully after migrating.

Model
LightweightMMMlag_weight prior Beta(2, 1)
Meridianalpha_m prior Uniform(0, 1)

Meridian’s decay prior is uninformative by default; LightweightMMM’s leaned towards long carryover.

Model
LightweightMMMhalf_max_effective_concentration prior Gamma(1, 1)
Meridianec_m prior TruncatedNormal(0.8, 0.8, 0.1, 10)

Both are the half-saturation point of the Hill curve on scaled media.

Model
LightweightMMMslope prior Gamma(1, 1)
Meridianslope_m Deterministic(1)

Meridian fixes the paid-media Hill slope at one by default, so curves are concave, never S-shaped. You can override the prior.

Model
LightweightMMMMedia coefficient HalfNormal(scale = media_prior)
Meridianroi_m prior LogNormal(0.2, 0.9) by default; paid_media_prior_type="coefficient" gives beta_m HalfNormal(5)

The biggest conceptual change: Meridian puts priors on ROI by default, which is what makes experiment calibration straightforward.

Model
LightweightMMMcustom_priors={"intercept": numpyro.distributions.Uniform(1, 5)}
Meridianprior_distribution.PriorDistribution(...) passed as ModelSpec(prior=...)

Parameter names differ (roi_m, alpha_m, ec_m, slope_m, beta_m, gamma_c, sigma and so on).

Model
LightweightMMMTrend: coef_trend and expo_trend (a power curve in time)
MeridianKnots: ModelSpec(knots=...) or enable_aks=True

Google’s table: LightweightMMM uses a straight line plus sinusoids, Meridian uses knots.

Model
LightweightMMMdegrees_seasonality=2, seasonality_frequency=52
MeridianKnots, plus controls for any known seasonal events

There are no Fourier seasonality arguments; more knots let the baseline follow seasonal shape.

Model
LightweightMMMweekday_seasonality=True (daily data)
MeridianNo dedicated argument

My suggestion rather than Google’s: add day-of-week indicators as controls if you model daily data.

Model
LightweightMMMGeo hierarchy with no extra free parameters
Meridianeta_m (spread of media effects across geos); controls hierarchical via xi_c

Meridian’s hierarchy can be normal or log-normal (media_effects_dist) and estimates how much geos differ.

Model
LightweightMMMNo reach and frequency
Meridianbuilder.with_reach(df, reach_cols, frequency_cols, rf_spend_cols, rf_channels)

R&F channels get their own Hill-then-adstock treatment and an optimal frequency.

Model
LightweightMMMSearch query volume as a confounder: possible but manual
MeridianA control variable, with documented guidance for paid search

From Google’s migration table.

Model
LightweightMMMExperiment calibration: possible but manual, through custom priors
MeridianROI priors, ModelSpec(roi_calibration=...), CalibrationBuilder, Meridian GeoX

Make experiment priors wider than the test’s standard error, since a test’s ROI and the model’s ROI aren’t defined identically.

Fitting
LightweightMMMmmm.fit(media=..., media_prior=..., target=..., extra_features=..., number_warmup=1000, number_samples=1000, number_chains=2)
Meridianmmm.sample_prior(500); mmm.sample_posterior(n_chains=10, n_adapt=1000, n_burnin=500, n_keep=1000, seed=0)

Settings from Google’s demo. Data goes in when you build InputData, not at fit time.

Fitting
LightweightMMMtarget_accept_prob=.85
Meridiansample_posterior(..., max_tree_depth=10, ...)

Meridian exposes its own NUTS tuning arguments (max_tree_depth, init_step_size and others) rather than a target acceptance rate.

Fitting
LightweightMMMutils.save_model(mmm, file_path) / utils.load_model(file_path)
Meridianmeridian_serde.save_meridian(mmm, path) / meridian_serde.load_meridian(path)

Import from meridian.schema.serde. Models save as .binpb protocol buffers.

Diagnostics
LightweightMMMmmm.print_summary() (r_hat under 1.1)
MeridianAnalyzer(...).rhat_summary(), visualizer.ModelDiagnostics(mmm).plot_rhat_boxplot(), reviewer.ModelReviewer(mmm).run()

Meridian’s docs treat r-hat under 1.2 as approximate convergence; I’d still aim for close to 1.0.

Diagnostics
LightweightMMMplot.plot_model_fit(media_mix_model=mmm, target_scaler=...)
Meridianvisualizer.ModelFit(mmm).plot_model_fit()

No scaler to pass, since Meridian unscales for you.

Diagnostics
LightweightMMMmmm.predict(...) and plot.plot_out_of_sample_model_fit(...)
MeridianModelSpec(holdout=...) and Analyzer.expected_vs_actual_data() / ModelFit

Holdouts are declared in the model spec and excluded from fitting.

Diagnostics
LightweightMMMplot.plot_media_channel_posteriors(...) / plot.plot_prior_and_posterior(...)
Meridianvisualizer.ModelDiagnostics(mmm).plot_prior_and_posterior_distribution()

Comparing prior and posterior tells you which channels the data actually informed.

Results
LightweightMMMmedia_effect_hat, roi_hat = mmm.get_posterior_metrics()
Meridianvisualizer.MediaSummary(mmm).summary_table() or Analyzer(...).summary_metrics() / .roi()

Meridian also reports marginal ROI, effectiveness and cost per incremental KPI, with credible intervals.

Results
LightweightMMMplot.plot_bars_media_metrics(metric=roi_hat, channel_names=...)
Meridianvisualizer.MediaSummary(mmm).plot_roi_bar_chart()
Results
LightweightMMMplot.plot_response_curves(media_mix_model=mmm, media_scaler=..., target_scaler=...)
Meridianvisualizer.MediaEffects(mmm).plot_response_curves()

plot_hill_curves() and plot_adstock_decay() show the two transformations separately.

Results
LightweightMMMplot.plot_media_baseline_contribution_area_plot(...)
Meridianvisualizer.MediaSummary(mmm).plot_channel_contribution_area_chart()

plot_contribution_waterfall_chart() is the other common view.

Results
LightweightMMMNo built-in report
Meridiansummarizer.Summarizer(mmm).output_model_results_summary(filename, filepath, start_date, end_date)

A two-page HTML report; the Scenario Planner turns a saved model into a dashboard.

Optimisation
LightweightMMMoptimize_media.find_optimal_budgets(n_time_periods, media_mix_model, budget, prices, extra_features, ...)
Meridianoptimizer.BudgetOptimizer(mmm).optimize(budget=..., fixed_budget=True, ...)

With no arguments Meridian optimises the historical period at historical total spend.

Optimisation
LightweightMMMbounds_lower_pct=.2, bounds_upper_pct=.2
Meridianspend_constraint_lower / spend_constraint_upper (default 0.3 each for a fixed budget)

Same idea: each channel can move a percentage either way from its current share.

Optimisation
LightweightMMMprices (cost per media unit)
MeridianDerived from media and spend in the data; pass new_data for a future period

For a forecast scenario, new_data carries future media, spend and revenue per KPI.

Optimisation
LightweightMMMmedia_gap (media between the end of training and the optimisation period)
MeridianCarryover from earlier media is included automatically

Meridian’s optimizer counts lagged effects of media run before the start date.

Optimisation
LightweightMMMplot.plot_pre_post_budget_allocation_comparison(...)
Meridianresults.plot_budget_allocation(), results.plot_spend_delta(), results.output_optimization_summary(filename, filepath)
Optimisation
LightweightMMMNo target-ROI scenario
Meridianoptimize(fixed_budget=False, target_roi=... or target_mroi=...)

Flexible-budget scenarios let total spend change to hit an efficiency target.

Checked against LightweightMMM 0.1.9's source (google/lightweight_mmm, archived 19 January 2026), the google-meridian 2.1.0 package and Google's migration guide, October 2026. Rows marked as my suggestion are judgement, not Google guidance.

Four of those changes matter more than the rest. The first is priors on ROI. LightweightMMM scaled the media coefficient’s prior by cost; Meridian, by default, puts a LogNormal(0.2, 0.9) prior directly on each channel’s ROI, which is easier to reason about and to calibrate with experiments, but it means your channels start from a different place, and on thin data that shows up in the answer. The second is the time effects. LightweightMMM’s power-curve trend and sinusoidal seasonality are replaced by knots, a piecewise baseline, so how much of your sales gets attributed to the baseline versus media can shift. The third is curve shapes: Meridian has no exponent-only model and no delayed-peak adstock, and it fixes the Hill slope at one by default, so curves are always concave. If your LightweightMMM model chose carryover because TV peaked a few weeks after airing, check the response timing after migrating. The fourth is scaling, which Meridian now does for you, so drop your CustomScaler objects rather than feeding Meridian pre-scaled data.

Migrating a LightweightMMM model, step by step

This is the order I’d do it in, so that you can tell which differences come from the method and which from mistakes in the move:

  1. Freeze the old model’s outputs: ROI by channel with intervals, the decomposition over time, response curves and the last budget recommendation, all in original units.
  2. Reshape the data into one long DataFrame with a time column, a geo column if you had geo data, and one column per KPI, media metric, spend and control. Add population per geo for geo models.
  3. Build the input with DataFrameInputDataBuilder (with_kpi, with_media, with_controls, and with_population or with_revenue_per_kpi as needed), and check the data summaries before modeling.
  4. Fit Meridian with its default priors first, and a sensible number of knots, so you see what the data says under Meridian’s assumptions before you add your own.
  5. Compare against the frozen outputs channel by channel. Differences in baseline share usually trace back to knots versus trend and seasonality; differences in a single channel usually trace back to the prior or the curve shape.
  6. Only then add informative ROI priors from experiments, with standard deviations wider than the tests’ standard errors, and rerun the comparison.
  7. Rebuild the optimization with BudgetOptimizer and the same spend bounds you used before (LightweightMMM’s default was ±20 percent; Meridian’s is ±30 percent for a fixed budget).

The Meridian page has a complete, tested script for steps three, four and seven, run on the same practice data used across this library.

py
Meridian quickstart (Python)The Meridian end of a migration: data builder, ROI priors, sampling, r-hat check, media summary and optimisation. Tested with google-meridian 2.1.0.
Download

Should you still use LightweightMMM?

For new work, no. Its methods were sound, and they live on in Meridian, which is the natural home for anyone who liked LightweightMMM’s Bayesian approach and Google’s research lineage. If what you liked was the small, transparent API and the freedom to choose your own transformations, PyMC-Marketing is the closer spiritual successor: Its default model is also adstock plus a saturation curve, with priors you set on the transformation parameters, and it offers the delayed-peak adstock that Meridian dropped. My comparison of open-source MMM libraries covers the rest of the options, and if you’re learning the method rather than migrating a model, start with what marketing mix modeling is or the Vexpower MMM course.

LightweightMMM taught a lot of marketers, me included, that a marketing mix model could be a few dozen lines of Python rather than a six-figure consultancy project. That lesson outlived the library, and it’s the reason the move to Meridian, for most teams, is an afternoon of reshaping data and a week of checking results rather than starting over.

Downloads

py
Meridian quickstart on the practice data (Python)Where a LightweightMMM user lands: data builder, ROI priors, sampling, diagnostics and optimization in Meridian 2.1.0. Tested on CPU.
Download
csv
MMM practice dataset (104 weeks, CSV)Synthetic weekly revenue, search, social and video spend and a promotion flag.
Download

Frequently asked questions

Is LightweightMMM deprecated?

Yes. When Google released Meridian generally on 29 January 2025, it updated LightweightMMM's README to say the library is not supported anymore and to recommend switching to Meridian. The GitHub repository was archived and made read-only on 19 January 2026. The last release, version 0.1.9, came out in May 2023.

What replaced LightweightMMM?

Google Meridian, which Google's documentation calls the official evolution of its MMM approach and the updated version of LightweightMMM. Meridian keeps the Bayesian adstock-and-Hill core but adds ROI priors, reach and frequency modeling, built-in experiment calibration, automatic scaling, knot-based time effects and budget optimization tools.

Can I still install LightweightMMM?

The package is still on PyPI as lightweight_mmm, at version 0.1.9 from May 2023, so pip will install it. But it gets no fixes, it was written against the JAX and NumPyro versions of 2023, and the repository is read-only, so use it only to reproduce an existing model, not to start a new one.

What were the three LightweightMMM models?

LightweightMMM offered three media transformations, chosen with model_name: adstock (geometric carryover with an exponent for diminishing returns), hill_adstock (geometric carryover followed by a Hill saturation curve, the default), and carryover (a delayed-peak carryover from Google's 2017 paper, with an exponent). The README recommended trying all three and keeping the one with the best out-of-sample fit.

How do I migrate from LightweightMMM to Meridian?

Google's guide says the input data is the same, so you mainly reshape it into a long DataFrame, load it with Meridian's DataFrameInputDataBuilder, replace your custom priors with Meridian's PriorDistribution (ideally ROI priors), drop your manual scalers, and sample with sample_posterior. Expect somewhat different results, because the trend, seasonality, priors and available curve shapes all differ.

References

  1. LightweightMMM on GitHub (google/lightweight_mmm), archived README deprecation notice, archive date 19 January 2026, model descriptions, API examples, citation authors, community spotlight.
  2. LightweightMMM source: lightweight_mmm.py, models.py, media_transforms.py, optimize_media.py model_name options, fit() defaults, default priors, adstock/hill/carryover maths, find_optimal_budgets arguments.
  3. LightweightMMM documentation (Read the Docs) Model documentation and custom priors.
  4. lightweight_mmm on PyPI Releases 0.1.1 (February 2022) to 0.1.9 (May 2023).
  5. Meridian docs: Migrate from LightweightMMM Google's feature comparison and model differences.
  6. Meridian on GitHub (google/meridian) Current API, changelog, getting-started notebook.
  7. Jin, Wang, Sun, Chan and Koehler (2017), Bayesian Methods for Media Mix Modeling with Carryover and Shape Effects The carryover and Hill shapes LightweightMMM implemented.
Mike Taylor
Mike Taylor

I've built marketing mix models for Monzo Bank, Booking.com, Time Out, The AA and others, advised Google, Appsflyer and Recast on MMM, and teach marketing mix modeling on LinkedIn Learning. Before that I co-founded a 50-person growth marketing agency. I write the Vexpower MMM course.

Keep reading

Learn MMM by doing itThe Vexpower course walks through thirty lessons on one invented brand, with simulators, Python labs and a decision case you can defend.
See the course