The Synthetic Crowd

Inferring Consumers to Understand Markets with PyMC-Marketing

Nathaniel Forde

Data Science @ PandaDoc

and Open Source Contributor @ PyMC

2026-11-15

Preliminaries

Who am I?

Link to my website

Code or it didn’t Happen

The companion notebook builds every figure in this talk, from the first integral to the cereal counterfactual: here. The model ships as BayesianBLP in PyMC-Marketing.

The Pitch

How much would sales of Corn Flakes fall if a competitor cut prices by 10%?

Answering needs a demand model, and real markets are full of things nobody observes:

  • how much each individual cares about price versus quality,
  • the unmeasured appeal of a product that drives both its price and its sales,
  • the composition of the customer base itself.

The argument: the Berry–Levinsohn–Pakes model is a stack of integrals over a crowd we never see. Treat the crowd’s composition as a latent variable and the classical computational tricks become a generative model.

Agenda

Act I: The Crowd Behind the Shares. What aggregate data hide, and why the crowd changes the answer

Act II: Integration All the Way Down. How economists handle the crowd, and the Bayesian alternative

Act III: Building the Crowd in PyMC. A Bayesian BLP from scratch, then the library

Act IV: Counterfactual Markets. Nevo’s cereal, and whether the crowd mattered

Act I: The Crowd Behind the Shares

What We Observe

and what we don’t

Nevo’s (2000) cereal panel: one row per brand, city and quarter.

brand share price sugar mushy
Kelloggs_4 0.0124 0.072 2 1
Kelloggs_6 0.0078 0.114 18 1
Kelloggs_7 0.0130 0.132 4 1
Kelloggs_9 0.0058 0.130 3 0

24 brands × 47 cities × 2 quarters. Inside shares sum to about 0.48; the rest buy no cereal.

Never observed

  • Who bought: their price sensitivity, their taste for sugar
  • Why a brand sells beyond its price and characteristics: its unmeasured appeal \(\xi_{jm}\)
  • How the crowd is composed, market by market

A share is the average decision of a crowd. To answer “what if?”, we need the crowd.

Demand Is an Integral

BLP (1995): consumer \(i\) in market \(m\) gets utility from product \(j\)

\[u_{ijm} = \color{teal}{\delta_{jm}} + \color{blue}{\sigma\, \nu_i\, p_{jm}} + \varepsilon_{ijm}, \qquad \color{teal}{\delta_{jm} = \alpha p_{jm} + x_{jm}'\beta + \xi_{jm}}\]

mean utility, shared by everyone + this consumer’s deviation + idiosyncratic shock

and the market share is the fraction of the crowd for whom \(j\) is best:

\[s_{jm} = \int \frac{\exp(\delta_{jm} + \sigma \nu\, p_{jm})}{1 + \sum_k \exp(\delta_{km} + \sigma \nu\, p_{km})}\, dF(\nu)\]

  • \(\alpha\): the average consumer’s price sensitivity; \(\sigma\): how differently consumers feel about price
  • \(\xi_{jm}\): the brand’s unmeasured appeal in that market
  • No closed form: we approximate the integral with a population of synthetic consumers.

The Synthetic Crowd

Price sensitivity varies, \(\alpha_i = \alpha + \sigma\nu_i\). Each of 500 synthetic consumers has their own choice probabilities; the share is the crowd’s average. The crowd buys the premium product at 22%; its “average member” would buy it at 9%.

Same Shares, Different Crowds

A logit and a crowd fitted to the same shares. In the crowd, premium buyers sit in the insensitive tail and mid-range buyers are their nearest neighbours, so a 10% premium price rise sends 28% of lost customers to mid-range (logit: 15%).

Act II: Integration All the Way Down

Integration Is Averaging

\[\int f(\nu)\, dF(\nu) \approx \frac{1}{R}\sum_{r=1}^{R} f(\nu_r)\]

Each point \(\nu_r\) is one synthetic consumer. Scrambled Halton points spread out on purpose; random draws clump.

On a three-dimensional share integral, a crowd of 8,192 Halton consumers is about 33× more accurate than a random one of the same size.

The Classical Estimator

A nested fixed point

For every guess of \(\sigma\):

  1. Invert the shares: find \(\delta\) that reproduces them exactly, by Berry’s contraction mapping \[\delta^{(t+1)} = \delta^{(t)} + \log s^{\text{obs}} - \log \hat s\big(\delta^{(t)}; \sigma\big)\]
  2. Recover \(\xi = \delta - \alpha p - x'\beta\)
  3. Score the guess with GMM: instruments \(z\) must be uncorrelated with \(\xi\)
def invert_shares(s_obs, sigma, nu, price):
    delta = np.log(s_obs) - np.log(1 - s_obs.sum())
    while True:
        s_hat = crowd_shares(delta, sigma, nu, price)
        step = np.log(s_obs) - np.log(s_hat)
        delta = delta + step
        if np.abs(step).max() < 1e-12:
            return delta

Powerful, but computationally delicate: the inner loop’s tolerance leaks into the outer estimates (Dubé, Fox & Su, 2012), and uncertainty is a delta-method afterthought.

The Bayesian Reframing

Every trick has a probabilistic counterpart

Classical device Bayesian counterpart
Simulation draws to approximate the share integral A fixed synthetic crowd: Halton consumers as data
Contraction mapping to invert shares for \(\delta\) No inversion: \(\xi_{jm}\) is sampled as a latent variable
GMM moment conditions \(\mathbb{E}[z\,\xi] = 0\) A joint model of the price equation and \(\xi\) (a control function)
Standard errors by the delta method Posterior integration: every quantity is averaged over the posterior

. . .

Jiang, Manchanda & Rossi (2009) gave BLP a Bayesian likelihood but still ran the contraction inside every MCMC step. Sampling \(\xi\) directly removes the inner loop, in the spirit of Dubé, Fox & Su’s MPEC: unknowns become parameters, not a fixed point.

We Don’t Know the Crowd

Everything so far assumed \(\alpha\) and \(\sigma\) were known. They aren’t.

  • The Halton points are fixed, standardised people \(\nu_r\).
  • Priors on \((\alpha, \sigma, \beta, \xi)\); the posterior moves the crowd \(\alpha + \sigma\nu_r\).
  • Each posterior draw is a different crowd.

The licence: shoppers are exchangeable given their market and taste profile, so de Finetti’s theorem says their choices are draws given a latent parameter with a prior. The crowd’s composition is that parameter.

Three Integrals, One Equation

\[\mathbb{E}[\text{elasticity} \mid \text{shares}] = \color{blue}{\int_\theta} \color{teal}{\int_\nu} \color{gray}{\int_\varepsilon} (\cdots)\, \color{gray}{dF(\varepsilon)}\, \color{teal}{dF(\nu)}\, \color{blue}{p(\theta \mid \text{shares})\, d\theta}\]

Gumbel shocks

The softmax is already an integral over idiosyncratic shocks, with a closed form (McFadden, 1974). Free.

The crowd

Halton average over synthetic consumers, inside the model.

The posterior

MCMC average over every crowd consistent with the shares.

Elasticities, diversion ratios and counterfactual shares are all the same nested average, evaluated at different prices.

Act III: Building the Crowd in PyMC

A Bayesian BLP from Scratch

with pm.Model(coords=coords) as blp_from_scratch:
    # The average consumer, and the spread of the crowd
    alpha = pm.Normal("alpha", 0, 2)
    sigma = pm.HalfNormal("sigma", 1)
    beta = pm.Normal("beta", 0, 1, dims="characteristic")
    # Unmeasured appeal: product effect + market shocks
    xi_j = pm.Normal("xi_j", 0, 0.5, dims="product")
    sigma_xi = pm.HalfNormal("sigma_xi", 0.5)
    xi = xi_j + sigma_xi * pm.Normal("xi_raw", 0, 1, dims=("market", "product"))

    # Mean utility (M, J), then every consumer's utility (M, J, R)
    delta = alpha * P + (X * beta).sum(-1) + xi
    utility = delta[:, :, None] + sigma * crowd[None, None, :] * P[:, :, None]

    # Integrate the logit over the crowd: .mean(axis=-1) IS the integral
    u_full = pt.concatenate([utility, pt.zeros((M, 1, R))], axis=1)
    shares = pt.special.softmax(u_full, axis=1).mean(axis=-1)
    log_ratio = pt.log(shares[:, :J]) - pt.log(shares[:, J])[:, None]

    # Berry (1994) log-share-ratio likelihood
    pm.Normal("log_share_ratio", mu=log_ratio, sigma=noise,
              observed=np.log(S) - np.log(S0)[:, None])

\[\hat s_{jm} = \color{teal}{\frac{1}{R}\sum_r} \text{softmax}_j\big(\delta_{jm} + \sigma \color{teal}{\nu_r} p_{jm}\big)\]

The engineering

  • The crowd is a pm.Data vector of 200 Halton draws: data, not parameters.
  • Broadcasting builds a markets × products × consumers tensor; no Python loops.
  • .mean(axis=-1) is the whole share integral.
  • 40 markets × 3 products fit in 21 s with nutpie.

Recovering the Crowd

  • Simulated panel: 40 markets × 3 products, true \(\alpha = -2\), \(\sigma = 0.6\).
  • Zero divergences; the 94% intervals for \(\alpha\) and both \(\beta\)s cover the truth.
  • \(\sigma\) is the hardest: its interval [0.604, 0.750] just misses the true 0.6, slightly overstating the crowd’s spread. Heterogeneity is identified by how shares bend across price, a second-order signal in aggregate data.

BayesianBLP in PyMC-Marketing

model = BayesianBLP(
    market_data=nevo,                      # long format: one row per market × product
    characteristics=["sugar", "mushy"],
    instruments=[f"z{i}" for i in range(6, 11)],    # price equation for endogenous prices
    random_coef_on=["price", "sugar", "mushy"],     # a three-dimensional crowd
    region_col="region", time_col="period", n_mc_draws=200,
    hierarchical_parameterisation="noncentered",    # few markets per city
    model_config={"alpha": Prior("Normal", mu=0, sigma=3),       # what this data needed
                  "sigma_xi_j": Prior("HalfNormal", sigma=0.25)},
)
model.fit(nuts_sampler="nutpie", draws=2000, tune=1000, chains=4,
          target_accept=0.95, init="adapt_full")    # funnel-prone ξ block
elasticities = model.elasticities(at="samples")
cf = model.counterfactual_shares(price_change={"GenMills_19": -0.10})
  • Endogenous prices: firms price up appealing products, so price and \(\xi\) are correlated. The price equation with instruments models them jointly.
  • The defaults were not enough for Nevo: a wider price prior, a non-centred hierarchy, adapt_full and a high target_accept. Fit time: 3.3 minutes, zero divergences.

Act IV: Counterfactual Markets

Nevo’s Cereal Panel

The standard benchmark for BLP: Nevo’s (2000) Practitioner’s Guide.

  • 24 brands, 8 cities × 2 quarters (16 markets) for a few-minute fit
  • Price in cents; sugar and mushiness as characteristics
  • Hausman instruments: the brand’s price in other cities
  • Three-dimensional crowd of 200 Halton consumers

Nevo’s panel is simulated to resemble the cereal market, and brand names are coded.

What the posterior says about this crowd

  • Average price coefficient \(\alpha = -0.33\) (price in cents)
  • Spread of price sensitivity across the crowd: 0.15 [0.11, 0.19]
  • Prices are endogenous: \(\rho(\xi, \eta) = 0.33\) [0.11, 0.52]

(89% intervals.) A real but modest spread. Does it change the answer to the Corn Flakes question?

Elasticities with Uncertainty

  • Own-price elasticities from −2.4 to −2.9, median −2.78.
  • Every brand gets a full posterior, not a delta-method standard error.
  • Wide intervals flag brands whose demand the data pin down poorly. Kelloggs_11, our stand-in for Corn Flakes, is one of them: worth knowing before you price it.

The Corn Flakes Question

A competitor cuts its price by 10%

GenMills_19 cuts its price by 10% and gains about 29% share [25%, 33%]. Kelloggs_11, our largest Kellogg’s brand, loses about 0.7% [0.5%, 0.9%]. Only 4% of the competitor’s gain comes from us; 55% comes from people who weren’t buying cereal at all.

Did the Crowd Matter Here?

The same question, asked of a homogeneous logit

Refit with random_coef_on=[]: the same data, instruments and priors, but one taste for everyone. The answers barely move: GenMills_19 +30.5% vs +29.1%, Kelloggs_11 −0.73% vs −0.69%, and 58% vs 55% of the gain from the outside good. For this question, the posterior says the crowd is doing little work.

Conclusion

The Crowd Is a Latent Variable

“The representation theorem … proves that if observations are judged to be exchangeable, then they must indeed be a random sample from some model and there must exist a prior probability distribution over the parameter of the model, hence requiring a Bayesian approach.”

– José Bernardo, The Concept of Exchangeability and its Applications (1996)

  • The average consumer can mislead: identical shares can hide crowds with different counterfactuals.
  • BLP is three nested integrals: Gumbel shocks in closed form, the crowd by Halton averaging, the parameters by the posterior.
  • Treat the crowd’s composition as a latent parameter with a prior, and the classical machinery dissolves into one PyMC model. The posterior then tells you, market by market, whether the crowd mattered.

“The crowd is not a numerical trick.

Its shape is what we infer.”

Resources

Resources

  • Companion notebook: github.com/NathanielF/…/synthetic_crowd.ipynb
  • PyMC-Marketing: pymc-marketing.io, BayesianBLP and its synthetic and Nevo example notebooks
  • Berry, Levinsohn & Pakes (1995), “Automobile Prices in Market Equilibrium,” Econometrica
  • Berry (1994), “Estimating Discrete-Choice Models of Product Differentiation,” RAND J. Econ.
  • Nevo (2000), “A Practitioner’s Guide to Estimation of Random-Coefficients Logit Models of Demand,” JEMS
  • Jiang, Manchanda & Rossi (2009), “Bayesian Analysis of Random Coefficient Logit Models Using Aggregate Data,” J. Econometrics
  • Dubé, Fox & Su (2012), “Improving the Numerical Performance of … Random Coefficients Demand Estimation,” Econometrica
  • Bernardo (1996), “The Concept of Exchangeability and its Applications,” Far East J. Math. Sci.
  • Train (2009), Discrete Choice Methods with Simulation

Coming soon: Bayesian Causal Inference (Vincent & Forde). Its discrete-choice chapter works through the individual-level counterpart of this talk.