Challenges · C15 · ★★★★☆

Should Parkay run a promotion?

Difficulty
★★★★☆
Time
4-5 hours
Data
The margarine scanner panel of Allenby & Rossi (1991; A.C. Nielsen, distributed with the R package bayesm): 4,470 purchases by 516 households among 10 products, each purchase with the shelf price of every product on that shopping trip
Skills
Multinomial logit for choice · price-coefficient sanity checks and elasticities · IIA and what it does to cannibalisation · hierarchical (random-coefficient) logit with correlated household tastes and a lognormal price sensitivity · centred vs non-centred hierarchies with ~500 groups · held-out scoring of a panel model · state dependence vs heterogeneity · counterfactual simulation with carry-over · posterior of incremental profit and a go/no-go decision

The brief

You work for the brand manager of Parkay margarine. Parkay sells sticks (its big seller) and tubs. The retailer has offered a slot for a temporary price reduction of 20% on Parkay sticks, funded entirely by Parkay. The brand manager asks:

"If we cut the stick price by 20% for a promotion period, how much extra share do Parkay sticks get? How much of that comes out of our own tubs, and how much do we steal from Blue Bonnet, the house brand and the rest? And after all that - does it make money? My team says promotions also win us loyal customers who keep buying Parkay afterwards, so count that too. I want to be at least 80% sure a promotion pays before I sign."

The economics, as the brand manager gives them (treat them as fixed):

  • Regular prices are each product's median shelf price in the panel (computed below). The promotion cuts the Parkay stick price by 20% for one shopping trip of every household (the promotion period); all other products stay at their regular prices.
  • Parkay's contribution margin at regular prices is $0.25 per lb on sticks and $0.45 per lb on tubs. Parkay funds the cut, so every promoted pound of sticks earns 20% of the regular shelf price less.
  • The region sells about 200,000 lb of margarine (all brands) in a promotion period. Treat every purchase as one pound (the panel has no quantities).
  • Carry-over counts: purchases on each household's next 6 shopping trips, at regular prices, that the promotion changed.

This is the pricing sequel to example E84 (market structure from the same panel). Do E84 first: it introduces the data, the pooled logit and its IIA property, and the idea that apparent loyalty can be heterogeneity in disguise. Here the question is a price decision, the model is a full random-coefficient logit, and the output is a profit distribution.

How this notebook works

  • Each task states what to deliver, not how. Write your code in the YOUR CODE HERE cells.
  • Stuck? h.hint("task2") reveals hints one level at a time: nudge → approach → code skeleton. Try to get by on nudges.
  • h.check("task2", price_sd=...) compares your numbers with the reference solution.
  • A full worked solution lives in notebooks/solutions/. Open it only when you are done (or truly stuck).
import logging
import time

import arviz as az
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import pymc as pm
import pytensor.tensor as pt
from scipy import special

from pymc_challenges import Hints, data

RANDOM_SEED = 1991
rng = np.random.default_rng(RANDOM_SEED)
az.style.use("arviz-variat")
logging.getLogger("pymc").setLevel(logging.WARNING)  # several fits: no sampler banner per fit

h = Hints("C15")
h.tasks()

C15 - Should Parkay run a promotion?

  • task0 - Look at Parkay's prices (3 hints, 2 check(s))
  • task1 - The pooled logit (3 hints, 2 check(s))
  • task2 - Households have their own tastes (3 hints, 3 check(s))
  • task3 - Loyalty (3 hints, 2 check(s))
  • task4 - Simulate the promotion (3 hints, 5 check(s))
  • task5 - Does it pay? (3 hints, 3 check(s))

The data

One row per purchase, in time order within each household (the panel has no dates). choice is the product bought; price_<product> is the shelf price in US$ per lb of every product on that trip, bought or not. Products: Pk = Parkay, BB = Blue Bonnet, Fl = Fleischmann's, Hse = the store's house brand, Gen = generic, Imp = Imperial, SS = Shedd's Spread; _Stk = sticks, _Tub = tub.

The cell below adds integer codes (y, hh), each purchase's previous purchase by the same household (prev), and the evaluation split used throughout: a household's first purchase is only used as the "previous purchase" of its second (so that every model sees the same rows), and the last purchase of every household with at least three is held out for scoring. The regular prices of the brief are in REG.

data.describe("margarine")
df = data.load("margarine")
PRODUCTS = ["Pk_Stk", "BB_Stk", "Fl_Stk", "Hse_Stk", "Gen_Stk", "Imp_Stk",
            "SS_Tub", "Pk_Tub", "Fl_Tub", "Hse_Tub"]
J = len(PRODUCTS)
STK, TUB = PRODUCTS.index("Pk_Stk"), PRODUCTS.index("Pk_Tub")
PRICE_COLS = [f"price_{p}" for p in PRODUCTS]

df["y"] = df["choice"].map({p: j for j, p in enumerate(PRODUCTS)})
df["hh"] = pd.factorize(df["hhid"])[0]
H = df["hh"].max() + 1
df["prev"] = df.groupby("hh")["y"].shift(1)
df["T"] = df.groupby("hh")["trip"].transform("max")
rows = df[df["trip"] >= 2].copy()
rows["test"] = (rows["trip"] == rows["T"]) & (rows["T"] >= 3)
train, test = rows[~rows["test"]], rows[rows["test"]]

REG = df[PRICE_COLS].median().to_numpy()          # regular shelf prices (US$ per lb)
DEPTH = 0.20                                      # the promotion: 20% off Parkay sticks
MARGIN_STK, MARGIN_TUB = 0.25, 0.45               # Parkay's margin per lb at regular prices
VOLUME = 200_000                                  # category purchases (lb) in the promotion period
CARRY_TRIPS = 6                                   # later trips that count as carry-over

print(f"{len(df):,} purchases by {H} households; {len(train):,} training rows, "
      f"{len(test)} held-out last purchases")
print("regular prices:", dict(zip(PRODUCTS, REG.round(2))))
df.head()
margarine: 4,470 margarine purchases by 516 households, in time order within household (no dates): hhid, trip (1, 2, ... within household), choice (one of 10 products: Pk = Parkay, BB = Blue Bonnet, Fl = Fleischmann's, Hse = the store's house brand, Gen = generic, Imp = Imperial, SS = Shedd's Spread; _Stk = sticks, _Tub = tub), and price_<product>: the shelf price (US$ per pound) of every product on that shopping trip.
  source: Allenby & Rossi (1991), 'Quality perceptions and asymmetric switching between brands', Marketing Science 10:185-205: an A.C. Nielsen scanner panel, distributed as `margarine` in the R package bayesm (Rossi; GPL >= 2; the URL is its CRAN source tarball). CONVERTED from .rda by tools/build_e84_margarine.py - keep the cached file.
  url:    https://cran.r-project.org/src/contrib/Archive/bayesm/bayesm_3.1-6.tar.gz
4,470 purchases by 516 households; 3,498 training rows, 456 held-out last purchases
regular prices: {'Pk_Stk': np.float64(0.58), 'BB_Stk': np.float64(0.58), 'Fl_Stk': np.float64(0.99), 'Hse_Stk': np.float64(0.45), 'Gen_Stk': np.float64(0.33), 'Imp_Stk': np.float64(0.75), 'SS_Tub': np.float64(0.85), 'Pk_Tub': np.float64(1.09), 'Fl_Tub': np.float64(1.19), 'Hse_Tub': np.float64(0.59)}
hhid trip choice price_Pk_Stk price_BB_Stk price_Fl_Stk price_Hse_Stk price_Gen_Stk price_Imp_Stk price_SS_Tub price_Pk_Tub price_Fl_Tub price_Hse_Tub y hh prev T
0 2100016 1 Pk_Stk 0.66 0.67 1.09 0.57 0.36 0.93 0.85 1.09 1.19 0.33 0 0 NaN 7
1 2100016 2 Pk_Stk 0.63 0.67 0.99 0.57 0.36 1.03 0.85 1.09 1.19 0.37 0 0 0.0 7
2 2100016 3 Pk_Stk 0.29 0.50 0.99 0.57 0.36 0.69 0.79 1.09 1.19 0.59 0 0 0.0 7
3 2100016 4 Pk_Stk 0.62 0.61 0.99 0.57 0.36 0.75 0.85 1.09 1.19 0.59 0 0 0.0 7
4 2100016 5 Pk_Stk 0.50 0.58 0.99 0.45 0.33 0.72 0.85 1.07 1.19 0.59 0 0 0.0 7

Task 0 · Look at Parkay's prices

Deliver

  1. The distribution of the Parkay stick price over trips. Is a 20% cut from the regular price something the data have seen, or an extrapolation? Where does most of the price variation come from?
  2. The raw promotion response: Parkay stick share and Parkay tub share on trips grouped by the Parkay stick price, with uncertainty. What does it suggest about cannibalisation?
  3. One reason this raw comparison is not the answer to the brief.
Your turn Work this task in the notebook.

Task 1 · The pooled logit, and what it says about the promotion

Fit a multinomial logit in which every household shares the same product intercepts and the same price coefficient (no previous-purchase term yet).

Deliver

  1. The price coefficient, with a sanity check: its sign, and the own-price elasticity of Parkay sticks it implies at regular prices. Is that plausible for a grocery brand?
  2. The promotion at regular prices: Parkay stick share before and after the 20% cut, the gain in percentage points, and where the gain comes from - for each other product, its share of the gain, compared with its share of the non-Parkay-stick market. Explain the pattern you find.
Your turn Work this task in the notebook.

Task 2 · Households have their own tastes

Give every household its own product intercepts and its own price sensitivity, drawn from a population distribution that lets them be correlated (do households who like the premium brands care less about price?). Keep the price coefficient negative for everybody.

Deliver

  1. The model written in its most direct form first. Diagnose what is wrong with the sampling, then fix it. Report what you changed and the diagnostics before and after.
  2. The population median price coefficient and the household-to-household spread of price sensitivity. Which products' fans are least price-sensitive?
  3. A comparison with the pooled logit on the held-out last purchases: the gain in log predictive density, with a standard error.
Your turn Work this task in the notebook.

Task 3 · Loyalty: does buying Parkay make you buy Parkay?

The brand manager's team believes promotions "win loyal customers". In a choice model that is state dependence: buying a product last time raises the chance of buying it again, beyond a household's fixed tastes.

Deliver

  1. Add the previous purchase to the pooled and to the hierarchical model. How big is the loyalty effect in each, as an odds ratio, and why do they disagree?
  2. What adding it does to the estimated heterogeneity (the sds of Task 2), and what the held-out purchases say about the four models.
  3. Which loyalty estimate should go into the promotion's carry-over, and why.
Your turn Work this task in the notebook.

Task 4 · Simulate the promotion

Use the households' current state: each household's most recent purchase in the panel is its "previous purchase" when the promotion trip comes.

Deliver, for the pooled-with-lag and the hierarchical-with-lag model:

  1. The Parkay stick share gain on the promotion trip (percentage points) and the fraction of the gain that is cannibalised from Parkay tub, compared with what IIA would have said.
  2. Which rivals lose the most, relative to their size, under the hierarchical model, and why. (Call a product's share of the gain divided by its IIA share its "lift".)
  3. The carry-over: the extra Parkay (stick + tub) purchases per household on each of the next 6 trips at regular prices. Plot the decay for both models.
Your turn Work this task in the notebook.

Task 5 · Does it pay?

Deliver

  1. The posterior distribution of Parkay's incremental profit from the 20% promotion over the whole period (promotion trip plus carry-over, sticks and tubs, at the brief's margins and volume), and the probability that it pays. Show what the pooled-with-lag model would have told the brand manager.
  2. The break-even stick margin: how large would Parkay's stick margin have to be for the 20% promotion to break even?
  3. Is there a discount depth that the hierarchical model says pays? Plot expected incremental profit against depth (5% to 30%) with uncertainty, for both models.
  4. A recommendation in plain words, including what this analysis cannot see.
Your turn Work this task in the notebook.

Going further

  • Non-linear price response. Utility linear in price learns its slope from the half-price deals and applies it to a 20% cut. Let the response bend (log price, a separate coefficient for the discount from the regular price, or a deal-depth spline) and see whether the shallow-cut profit changes.
  • Demographics in the hierarchy. margarine_demos has income, family size and education for every household. Put them in the population mean of the household coefficients: are poorer or larger households more price-sensitive, and could Parkay target the promotion (a coupon) at them?
  • Reference prices. Households who have seen Parkay at $0.29 may treat $0.46 as "not a deal". Add a reference-price (loss-aversion) term built from each household's past prices.
  • Who should be promoted? Run the same calculation for a 20% cut on Parkay tubs or Blue Bonnet sticks. Which product's promotion steals most from rivals per dollar of margin given away?
h.progress()

C15: 0/18 hints used, 17/17 checks passed.

Reference solution

A complete, executed solution with commentary on every task — including the ways the obvious model fails. Try the hint ladder first.

Open the worked solution →