Papilon 0.3.0: A Marketing Mix Model That Picks Its Own Carryover Shape, and Tells You When Not to Trust It
An open-source Bayesian MMM for Python, tested against data where every true answer is known, including the parts it still gets wrong.

Papilon 0.3.0 is out. It’s free, open source under the Apache 2.0 license, and one command away:
pip install papilon
I built Papilon at Vector1 Labs as a Python library for causal and complex-systems work, and this release adds the piece I’ve wanted most: a marketing mix model. I’ve spent more than twenty years in marketing modeling and measurement, and two problems kept coming up in every mix model I built or reviewed. The first is that the shape of an ad’s carryover gets chosen by habit, not by the data. The second is that most models report a confident answer even when their own sampler can’t stand behind it. Papilon 0.3.0 is built to handle both.
What’s in 0.3.0
The headline is pp.MMM, a Bayesian marketing mix model with five things you can call:
compare_carryover()fits competing carryover shapes, for example geometric decay against an effect that peaks weeks after spend, and picks between them by Bayes factor.get_roi()returns each channel's return on ad spend with a 90% interval, plus the marginal return on the next dollar.optimize_budget()reallocates a budget across channels using each channel's response curve.response_curve()andcontributions()show diminishing returns and each channel's weekly contribution.gate_resultsrecords whether each fit passed the model's own quality check, and what it took to pass.
The release also includes make_synthetic_mmm(), which generates three years of weekly marketing data along with the parameters that produced it, so you can score any model, not just Papilon's, against a known truth. It ships with a 15-test suite and a quickstart script. The statistical-mechanics tools from 0.2.0 (entropy measures, KDE scenario simulation and relationship analysis) are all still there.
How it works
Most Bayesian mix models run a long Markov chain and leave it to the analyst to judge whether the result can be trusted. Papilon takes a different path, set out in its design document, RFC 001. It runs in four steps.
First, it makes a fast guess at where the answer lies. In 0.3.0, that guess is a Gaussian fit at the most likely parameter values. The roadmap replaces it with a trained neural network that makes the guess almost instantly.
Second, it checks that guess against the exact model with importance sampling: thousands of draws from the guess, each weighted by how well it matches the real posterior.
Third comes the gate. If the weights are spread evenly, the guess was good and the result is accepted. If a few draws carry most of the weight, the guess was off, so Papilon refines it from the weighted draws and tries again. If refining doesn’t help, it escalates rather than reporting a number it can’t support. The effective sample size decides each step, and the model records every one.
Fourth, the same weights estimate how well each candidate model explains the data. That makes choosing a carryover shape a measured comparison, a Bayes factor, rather than a setting someone picked.
Two modeling choices matter for marketers. Channel effects are constrained to be positive, so no channel can come out with a negative return as an artifact of the math. And the baseline, trend, seasonality and controls such as price are integrated out exactly, so the sampling effort goes where the uncertainty is: the media effects.
Tested against a known truth
A model is only as good as its record on data where you know the answer. So I generated three years of weekly sales for a brand running four digital channels, with every true parameter on file. Two traps are built in. Online video’s effect peaks two weeks after the spend, not the same week. And paid search spend rises and falls with seasonal demand, which makes it hard to separate from seasonality.
Papilon found the delay. The delayed-carryover model beat the plain geometric one with a log Bayes factor of 18.7, and it put video’s peak at 2.0 weeks, the true value. Both fits needed the refine step before passing the gate, and both passed.
Here are the returns on ad spend, sales per dollar, with the truth alongside:
channel estimate 90% interval truth paid_search 0.64 0.03 to 1.65 1.25 paid_social 1.02 0.47 to 1.79 1.50 online_video 1.76 1.50 to 2.05 1.57 display 0.71 0.08 to 1.68 0.74
Every true value sits inside its interval. But the point estimates for paid search and paid social are low, and that matters for budgets. When I scored the model’s recommended reallocation against the true response curves, it captured only 12% of the available gain: about 0.7 of the 5.6 extra weekly sales the true optimum would add. It moved money toward video, which was right, but cut search and social too hard.
I’m publishing that result on purpose. Three years of weekly data can’t fully separate a channel that follows demand from demand itself. No modeling choice fixes that, but an experiment does, which is why calibration to lift tests is next on the roadmap. A model that shows you where it’s weak is more useful than one that hides it.
Try it in a few lines
This runs the full test above in about two minutes on a laptop:
import papilon as pp
df, truth = pp.make_synthetic_mmm()
model = pp.MMM(
data=df,
target="sales",
media=["paid_search", "paid_social", "online_video", "display"],
date_col="date",
controls=["price_index", "promo"],
seed=11,
)
# Let the data choose online video's carryover shape
families = model.compare_carryover({
"all_geometric": [],
"video_delayed": ["online_video"],
})
print(model.get_roi())
print(model.optimize_budget(total=df[model.media].sum(axis=1).mean()))To use your own data, swap in a DataFrame with one row per week, a sales column, and a spend column per channel.
What’s next
These are on the roadmap and not in 0.3.0:
- Calibration to lift tests and geo experiments, the fix for the search and social gap above
- The trained amortized network from RFC 001, to make the first guess near-instant
- Geo-level models that share strength across markets
- More carryover and saturation shapes
I’m also running Papilon head to head against PyMC-Marketing and Google Meridian on the same synthetic dataset, scored against the same truth. That comparison will be published on my Substack on December 16.
Get it
- Install:
pip install papilon - Code, design document and tests: github.com/Bodhi8/papilon
- Package: pypi.org/project/papilon
If you run it on your own data, I’d like to hear how it does, especially where it struggles. Issues and pull requests are open on GitHub.
About the Author
Brian James Curry is a Kansas City AI researcher, applied scientist and data scientist with more than twenty years in marketing modeling and measurement. His work today spans marketing mix modeling, causal inference, machine learning and healthcare intelligence. He is the founder of Vector1 Labs, an independent applied AI research lab. His marketing measurement work covers marketing mix modeling, incrementality testing and budget allocation, and his Causal Care series applies causal machine learning to health system decisions: which programs work, for whom, and what they are worth. He also founded KC AI Lab, which began in 2016 as the Kansas City Machine Learning Group and has grown to more than 1,500 members.
His career includes leading analytics, data science and machine learning at Hallmark, Garmin, Koch Industries, Tractor Supply Company and Vail Resorts. His open-source research tools on GitHub include Papilon (marketing mix modeling and complex systems), PyCausalSim (causal discovery through simulation) and tincast (forecasting for AI-critical commodities), along with MeaningFlow and Daedalus. A Kansas State University alum from a small Kansas farming town, he writes about marketing mix modeling, causal inference, agentic AI, healthcare AI and the economics of AI on Medium and Substack. Contact: brian@vector1.ai.


