Where we start
A line through logs has a slope, and last time the slope was fitted one product at a time and came out as noise. The fix is not more data per product; it is a different unit. Fit all 2,216 rows at once and the slope steadies. But "all at once" hides a choice: whether each product is allowed its own level. Without that allowance, the line is comparing a bath towel that sells 40 a month at R$ 30 with a watch that sells 4 a month at R$ 300, and calling the difference an elasticity. With it, the line only sees how each product's own months differ from each other. Every standard error tonight is a naive standard error: conventional OLS, unclustered, and the page says so each time one appears.
★ What you walk out with tonight. Five fits of the same panel with the number on each; the one sentence that explains why +0.008 and -1.788 are both correct answers to different questions; a demeaning step you can write in one line of pandas and defend in one line of English; a placebo loop that tells you how far from zero "far" is; and the honest label for the result: a within-product association with a placebo behind it, not a causal elasticity.
Rung 1: cheap products are not popular products
5 min liveThe first thing most people fit is the obvious one: one dot per product, its mean price against its total units, a line through all of them. Across 4,731 Olist products with at least five units sold, that line has slope +0.008 with a naive standard error of 0.011. Zero, in other words. Inside each of the eight biggest categories the slope sits between -0.035 and +0.089; none is below -0.04. Price tells you nothing about how much a product sells when the products are different products.
LiveWhat a cross-section across products is actually measuring3 min▶
One dot per product. Mean price on the x axis, total units on the y axis, 4,731 dots, a line. The slope of +0.008 with a naive standard error of 0.011 says the line is flat. The reading people reach for is "demand on this marketplace is perfectly inelastic", and that reading is wrong, because the line never saw a price change. It saw 4,731 different products at 4,731 different prices, and the reason a R$ 300 watch sells fewer units than a R$ 30 towel is that it is a watch, not that it is dear.
Inside a category it does not improve. Bed, bath and table +0.022; sports and leisure -0.013; health and beauty -0.026; furniture and decor -0.028; housewares +0.089; computers and accessories +0.066; telephony -0.009; watches and gifts -0.035. Eight categories, none below -0.04. Two towels at different prices are still two different towels: one has better reviews, a bigger seller, a photo that loads. Their price difference is tangled with everything else that differs between them, and the slope cannot pull the price part out.
The bench's own version. On the 203 eligible products alone, the same cross-section gives -0.001 with a naive standard error of 0.058. Same answer on the rows the browser can recompute in deep-dive session 10, which is why the build-along tonight prints that one and quotes the 4,731-product figure from the offline run.
Self-studyWhy a flat cross-section is not evidence of inelastic demand2 min▶
An elasticity is the answer to "if this product's price moved, how would its units move". A cross-section holds 4,731 answers to a different question, "which products are cheap and which are popular", and those two properties are set by different things. Popularity comes from category, brand, reviews, the seller's reach; cheapness comes from cost, positioning, the seller's margin. There is no reason for them to line up, and on Olist they do not.
The mistake has a name in econometrics, omitted variable bias, and its cure is not a bigger cross-section. Ten times the products would give ten times the dots and the same flat line with a smaller standard error, which would make the wrong answer look more certain. The cure is a within comparison, which is Part 2.
The catalogue slide. A pricing review opens with a scatter of every SKU's price against its volume and a fitted line that is nearly flat. The slide concludes that customers are not price sensitive and recommends a 10 percent rise across the board. Every dot on that slide is a different product; the slide has measured the shape of the catalogue and presented it as the shape of demand. On these rows the within-product number says the rise loses about 7 percent of revenue, not gains 9, and Part 3 shows the two predictions side by side.
Rungs 2 and 3: the same rows, compared within product
5 min liveNow use the panel. Rung 2 fits one line through all 2,216 product-months with no allowance for which product a row belongs to, and gets -0.108 with a naive standard error of 0.028. Rung 3 fits the same 2,216 rows after subtracting each product's own mean of log price and mean of log units, and gets -1.788 with a naive standard error of 0.149. The second fit is the first fit with 203 intercepts, one per product, and those intercepts are a product fixed effect. What they remove is everything about a product that does not change from month to month.
LivePooled is not within, and demeaning is the whole trick3 min▶
Rung 2, pooled. One line through 2,216 rows, no intercepts. Its slope of -0.108 looks like a small elasticity and is not one, because the line is still mostly comparing products with other products: 203 products at 203 levels contribute most of the spread in both price and units, and the month-to-month movement inside a product is a small part of the picture. The pooled number is a blend of the flat across-product story and the steep within-product story, weighted toward the flat one.
Rung 3, within. For each product, subtract its mean log price from every month's log price and its mean log units from every month's log units. Then fit one line through the 2,216 demeaned rows. This is exactly the regression with 203 product dummies, and the demeaned version is the one you can draw: every product now sits at the origin and the only variation left is within it. The slope is -1.788 with a naive standard error of 0.149.
What the fixed effect removes. Everything about a product that is constant across its months: its category, its seller, its photo, its reviews as of the panel, its usual price level and its usual volume. What it cannot remove is anything that changes month to month inside the product, which is where Part 3 starts.
Self-studyWhy 203 dummies and one demeaning step give the same slope2 min▶
Regressing log units on log price plus 203 product indicator columns, and regressing demeaned log units on demeaned log price with no indicators, give the identical slope. This is the Frisch-Waugh-Lovell result: partialling a set of regressors out of both sides first and then fitting the rest is the same as fitting everything at once. Demeaning within product is the partialling-out step for product dummies, and it is why the build-along never constructs a 2,216 by 204 design matrix.
One thing the shortcut does change is the naive standard error's degrees of freedom: the demeaned regression thinks it estimated one parameter when it estimated 204. The correction is small at 2,216 rows, and the honest move anyway is to cluster by product, which conventional OLS does not do. That is the reason every standard error on this page is called naive.
The subscription tier that "proved" price does not matter. A SaaS analyst pools three years of accounts across a starter, a team and an enterprise tier and finds that higher prices go with more seats. Of course they do; enterprise customers buy the enterprise tier. Compare each account with itself across its own renewals, where the price actually moved, and the sign flips. The pooled fit was a cross-section across customers wearing a panel's clothes.
Rungs 4 and 5: the month, the checks, and the shuffle
5 min liveA product fixed effect does nothing about November. If the marketplace grew, or every seller discounted in the same month, then price and units moved together across all products at once, and the within-product slope carries a share of that. Rung 4 removes each month's mean as well as each product's and gets -1.854 with a naive standard error of 0.150: month effects move it a little, not a lot. Three robustness rows move it inside a band. And a placebo tells you what the fit gives when there is nothing to find: shuffle log price within product, refit 200 times, and the slope averages -0.002.
LiveMonth effects, three checks, and the placebo3 min▶
Rung 4, product and month effects. Subtract each product's mean, then each month's mean, and repeat until nothing moves (alternating projections, fifty passes is plenty). The slope is -1.854 with a naive standard error of 0.150, against -1.788 without the month effects. The marketplace grew across 2017 and 2018 and prices drifted, and removing that shared movement changes the answer by 0.07. A small move is itself a finding: the within-product slope is not being driven by the calendar.
Three ways of being wrong that do not change the sign. Weight each row by its units, so big-selling months count more: -2.216 (naive standard error 0.143). Use the freight-inclusive price the buyer paid instead of the seller's: -1.881 (naive standard error 0.154). Drop the months where a product sold a single unit, which leaves 1,850 rows: -1.480 (naive standard error 0.142); the single-unit months pull the slope more negative. All three are rung 3 on the same panel with one decision flipped, and the answer stays between -1.5 and -2.2.
Rung 5, the placebo. Inside each product, shuffle the log prices across its months so that every product keeps its own prices but in a random order, then run the rung 3 fit. Do it 200 times. The slopes have mean -0.002, standard deviation 0.159, and the widest is 0.444 in absolute value. The real -1.788 is more than eleven of those standard deviations from zero. That is what "the slope is not the method" looks like in a number.
Self-studyWhat none of it removes: the seller chose the price after seeing demand3 min▶
Product fixed effects remove "this product is cheap and popular". Month effects remove "the marketplace grew". The placebo shows the slope is not a fitting artefact. None of the three touches the thing that makes an observational price panel hard: a seller cuts price when stock is high, or when a competitor moves, or when units have been slow; and a seller raises price in the last months of a product's life, which is also when it is going out of stock. The price was chosen after looking at demand, so price and units are tied together by the seller's decision as well as by the buyers' response, and the panel cannot tell the two apart.
The honest label for -1.788 is therefore a within-product association with a placebo behind it, not a causal elasticity. It is the number to publish, with its range, and it is a much better number than +0.008 or -0.108. It is not the number an experiment would give. The steps from association to cause, confounding, what a control can and cannot do, and how to read a diagram of the mechanism, are the subject of learn-causal-inference-with-phoebe. The experiment that would settle the Olist question, a price test where the seller's choice is replaced by a random draw, is designed in learn-experimentation-with-phoebe. Deep-dive session 3 uses the number as it is, with the label on.
The clearance that looked like elasticity. In the headline product from deep-dive session 1, the last three months carry prices of R$ 89.90 and R$ 103 and units of 2, 2 and 3. A within-product fit sees a rise and a collapse and reads it as a response. The seller most likely raised the price because the product was ending, and units collapsed for the same reason. Fixed effects cannot see that, month effects cannot see that, and the placebo cannot see that; only knowing why the price moved can. The number survives all three checks and still carries the label.
Rungs 1 to 5 on the saved panel
★ 22 min · everyone buildsStart from panel_2018-09-01.csv, the file deep-dive session 1 wrote, with columns product_id, month, units, price, freight, cat, lp, lq. Seven steps in pandas and numpy, each printing the map's number to three decimals. Every standard error is the conventional OLS one, unclustered, so the code calls it naive_se and the comments do too.
Load the panel and write the slope function once. The slope of y on x, optionally weighted, with the conventional standard error. It is the same function for every rung, which is the point: the rungs differ in what goes in, never in the fit.
Rung 1: the cross-section on the 203. One dot per product, total units against mean price. The 4,731-product version (+0.008, naive standard error 0.011) needs the full item table and is quoted from the offline run; on the 203 the answer is the same: nothing.
Rung 2: pooled. All 2,216 rows, one line, no intercepts per product. The number looks like a small elasticity. It is a blend.
Rung 3: demean by product. One helper, one line per column, then the same fit. This is the regression with 203 product dummies, without building them.
Rung 4: product and month effects by alternating projections. Subtract product means, subtract month means, repeat. Fifty passes is far more than the panel needs and costs nothing.
Three decisions flipped, one at a time. Each is rung 3 with a single change. Print all three before quoting any one of them; together they are the range.
Rung 5: the placebo. Shuffle log price inside each product, demean, refit, 200 times. The mean and sd are "about" because a different generator order gives slightly different draws; the offline run with this seed gave -0.002 and 0.159, and the browser's own generator in deep-dive session 10 gave -0.003 and 0.170.
Print the ladder as one block, every time. Five rungs and three flipped decisions, eight lines, before a single number leaves the notebook. A reader who sees only -1.788 has to trust you; a reader who sees +0.008 above it and -0.002 below it can see for themselves what the number is and what it is not.
Before deep-dive session 3
30 min- Run the seven steps on the saved panel and check every printed number against the comment beside it. Any three-decimal difference means the panel is not the panel; find which of deep-dive session 1's three decisions moved.
- Flip a fourth decision. Restrict rung 3 to one category (bed, bath and table has 22 products and 241 rows) and print the slope with its naive standard error. Write one sentence on whether it is distinguishable from the all-category number.
- Run the placebo with a different seed and a different draw count (500). Write down the mean and sd, and how many placebo standard deviations -1.788 is from zero this time.
- Write the label. One sentence that says what -1.788 is and what it is not, including the words "within product", "placebo" and "not causal". That sentence goes above every chart that uses the number.
Three questions
Clear all three and deep-dive session 2 gets stamped in your passport.
1 · Across 4,731 products, a line through mean price and total units has slope +0.008 with a naive standard error of 0.011. What does that say about demand?
An elasticity answers "if this product's price moved, how would its units move". A cross-section holds thousands of answers to "which products are cheap and which are popular", and those are set by different things. Inside each of the eight biggest categories the slope stays between -0.035 and +0.089.
2 · Rung 2 (pooled) gives -0.108 and rung 3 (product fixed effects) gives -1.788 on the same 2,216 rows. What does the fixed effect do?
Demeaning within product is the same regression as one with 203 product dummies. Option D describes the month effect of rung 4, which moves the answer from -1.788 to -1.854; the product effect is what moves it from -0.108 to -1.788.
3 · The placebo shuffles log price within each product 200 times and the refitted slope averages -0.002 with sd 0.159. The real slope is -1.788. What is the honest label for -1.788?
-1.788 is more than eleven placebo standard deviations from zero, so it is not noise and not the method. It is also not an experiment: a seller cuts price when stock is high and raises it when a product is ending. The number to publish is the range -1.5 to -2.2 with the label on.
What this session teaches, and where it came from
Every fact on this page, and its verification tier, is recorded in the course's source map.
Deep-dive session 2 cheat sheet · pin this
You have a number with a range and a label. Deep-dive session 3 puts it to work: under -0.108 a 10 percent price rise gains 8.9 percent of revenue, and under -1.788 it loses 7.2, so the first thing an elasticity buys you is the sign of a pricing decision. Then the ways a seller charges different prices to different buyers, first, second and third degree, versioning as a menu that sorts customers, and why a free tier is a price too.