learn-economics-with-phoebe / Deep-dive session 1 of 10
Learn Economics with Phoebe · Deep-dive session 1 of 10

Demand from data

Every textbook draws the demand curve and no dataset contains it. What a dataset contains is rows: this product, this month, sold this many, at this price. Tonight is the road from those rows to a number that deserves the name elasticity, and the road has three decisions on it that most tutorials skip: which rows count, what "price" is, and what unit you fit the line on. The running data is 112,650 real order items from a Brazilian marketplace. By the end you will have a panel of 2,216 product-months for the 203 products that can be read, a log-log line through each one, and the finding that fitting one product at a time gives you noise: a third of the slopes are past 5 in absolute value, and the best-selling product's slope of -10 is not an elasticity at all.

🟡 Core Analysts, pricing and growth teams Python, pandas 45 min
0-3 · Welcome 3-18 · Rows, the panel, and the line 18-40 · Build-along: items to a fitted slope 40-45 · Q&A
Part 0

Where we start

Elasticity is the percentage change in units for a one percent change in price. On a log-log plot it is the slope of a straight line, which is why the whole of this track fits lines through logs. The trouble is never the fitting. It is what you fit on: an order, an item, a product, a month, a product-month, all products at once. Each choice gives a different slope from the same rows, and deep-dive session 2 shows the differences are not small: the same rows give -0.1 one way and -1.8 another. Tonight builds the rows properly so that argument can be had on solid ground.

🎫 Clear all three quiz questions to stamp your passport Live = presented in the 45 minutes Self-study = the full depth, after class ★ Build-along = run the code yourself

★ What you walk out with tonight. A product-month panel built from raw order items with every decision written as a comment, the eligibility rule that separates a product you can read from one you cannot, a log-log fit you can explain in one sentence, and the reason you will never again quote one product's slope as its elasticity.

Part 1 · covers the row, the price, the snapshot

One row is one unit at one price

5 min live

Olist ships three tables that matter tonight: order items (112,650 rows, one per unit sold, with the price the seller listed and the freight the buyer paid), orders (99,441, with a status and a purchase timestamp), and products (32,951, with a category). Before any of it becomes a panel, three decisions have to be made and written down, and each one changes which rows survive.

THREE TABLES, THREE DECISIONS, ONE PANEL order items 112,650 orders 99,441 products 32,951 1 · delivered, before 2018-09-01 110,197 items 2 · what is the price? item price freight: excluded 3 · join category, join the month from the timestamp group by product x month units, mean price 2,216 rows 203 products 21 months Decision 1 drops 2,453 items. Decision 2 changes every price by the freight, which is set by distance, not by the seller. Decision 3 picks the grain. One line of code and one comment per decision. Two analysts who differ get two panels, and the difference is a diff, not an argument.
🔍 Click to zoom - the crossed-out line is freight: it moves with the buyer's postcode, so it is not the seller's price and it is not in the panel
LiveWhich rows count, and what "price" is3 min▶

Which rows. Delivered orders only, purchased before 1 September 2018. The second half of that rule comes from the marketplace itself: the export's last timestamp is 17 October 2018 but only 20 orders are dated September or October and none was delivered, so the file effectively stops at the end of August. The retention and RFM courses hit the same ragged edge and chose the same date; reusing it means the customer table there and the panel here describe the same months.

What price. An Olist item row carries two money columns: price, which the seller listed, and freight_value, which depends on how far the buyer lives. The seller's decision is the first one. The buyer's cost is the sum. For an elasticity of the seller's price, use price; the freight-inclusive version is run as a check in session 2 and moves the answer by about 0.1, which is inside the range the honest sentence already carries.

What grain. One row per product per calendar month: units sold, and the mean item price that month. Monthly is the finest grain at which most of the 203 products have more than one sale in a row, and it is the grain at which a seller actually changes a price. Weekly would give more rows and more zeros; quarterly would hide most of the price moves.

Self-studyWhy the row is an item and not an order, and where the 2,453 went2 min▶

An order can hold several items, each with its own product and price, so "units of product X" is a count of item rows, not order rows. Grouping by order would count a basket with three of the same product as one unit and merge two different products into one price. The item table is the one where a unit and a price sit on the same row, which is the only shape a demand curve can be read from.

The 2,453 items dropped by decision 1 are undelivered, cancelled or unavailable orders, plus the twenty stragglers after the snapshot. They are not errors; they are sales that did not complete, and a demand curve is about sales that did. Keep the count in a comment. If it ever changes on a re-run, the export changed and every number downstream is due a re-check.

Real world

Two analysts, two elasticities, one shop. One uses the price the customer paid, freight included; the other uses the list price. On a marketplace where freight is 20 percent of a cheap item's cost and 3 percent of a dear one's, the first analyst's "price" moves whenever the buyer mix shifts toward distant cities, which has nothing to do with the seller. Both get a slope. Only one of them is measuring a decision the seller can make.

Part 2 · covers the panel and the eligibility rule

The panel, and who gets to be in it

5 min live

A product with one price has no elasticity to read; a product that sold six units has a price and not a response. So the panel carries a rule at the door: at least 30 delivered items, at least six months with a sale, at least three distinct prices. Of 32,216 products with a sale, 203 pass. They carry 14.9 percent of every item the marketplace sold.

ONE PRODUCT, THIRTEEN ROWS aca2eb7d, furniture and decor, 520 items THE SAME ROWS AS DOTS log units against log price month units mean price R$ 2017-07575.00 2017-081375.00 2017-10575.00 2017-114375.00 2017-124575.00 2018-0112169.98 2018-024369.90 2018-036569.90 2018-048369.90 2018-059069.90 2018-06289.90 2018-07289.90 2018-083103.23 the cut rise, then collapse LOG UNITS LOG PRICE 69.90 75 89.90 103 five amber months at 69.90 five white months at 75 three hatched months at 89.90 and 103: 2, 2, 3 units A line through these thirteen dots has slope -10.17. Session 3 explains why that is the slope that is too good, and this session builds the rows it sits on.
🔍 Click to zoom - the three hatched dots at the bottom right decide the slope, and they are the months the product was barely on sale
LiveThe three sieves, and what each is for3 min▶

Thirty items is a data floor: below it a month with two units and a month with three units differ by 50 percent for no reason a price could explain. 376 products pass. Three distinct prices is an economics floor: a slope needs the price to have been in more than one place, and two prices give a line through two points that is fitted to nothing. 226 of the 376 pass. Six months with a sale is an identification floor: one price change in a two-month product cannot separate "the price did it" from "the month did it". 203 pass all three.

The thresholds are choices and the page says so. Lower them and more products enter with noisier rows; raise them and the panel shrinks toward the handful of bestsellers. The right move is to write the three numbers as constants at the top of the script and re-run the whole ladder under two other settings before quoting anything, which session 10's bench does with two of them as switches.

The 203 are not a random sample of the marketplace. They are the products that sold a lot and repriced, which is the population any pricing decision is about anyway; the honest sentence says "for the products that can be read", not "for the marketplace".

Self-studyMean price in a month, and why not the median or the mode2 min▶

A product's price can change mid-month, and the month's row then carries a mean of two prices weighted by how many units sold at each. That mean is the price the average unit that month was sold at, which is the quantity a revenue calculation needs. The mode would hide the change; the median would jump. In the headline product, January 2018's mean of 69.98 is 121 units, all but a few at 69.90 and a handful at 75 before the cut took effect. The rest of the year's rows are single prices.

For the same reason, "units" is a count of item rows and not of orders. A basket of three is three units at one price, and it belongs in the month's total three times.

Part 3 · covers the log-log fit and per-product noise

A line through the logs, and the slope that is too good

5 min live

Take the log of units and the log of price and fit a straight line. The slope is the elasticity: a slope of -2 says a one percent price rise loses two percent of units. Do it for each of the 203 products on its own months and you get 203 slopes, and they are a lesson in why one product is the wrong unit: the median is -2.03, 70 percent are negative, and a third are past 5 in absolute value, which no demand curve for a bath towel or a phone case has ever produced.

203 PRODUCTS, 203 SLOPES, ONE PER PRODUCT bar height = how many products' slopes land in that band · shape is real, bars are sketched tail past -5: noise tail past +1: the sign is wrong -15 -5 -2 +1 +10 median -2.03 aca2eb7d: -10.17 the headline product 70.0 percent negative 59.6 percent below -1 33.5 percent past 5 in absolute value interquartile range -5.09 to +0.70 Thirteen rows cannot hold an elasticity still. The panel of 2,216 can, and session 2 fits it as one line with 203 intercepts instead of 203 lines.
🔍 Click to zoom - the amber bar is where the median sits; the two hatched tails are one product in three, and neither tail is economics
LiveWhy -10.17 is not an elasticity, and what it is instead3 min▶

Read the thirteen rows again. Five months at R$ 75 selling 5 to 45. A cut to R$ 69.90 and five months selling 43 to 121. Then R$ 89.90 and R$ 103, selling 2, 2 and 3. A line through that has to pass near the bottom-right dots, and it comes out at -10, which would mean a 10 percent rise loses 62 percent of units. Nobody's bath mat behaves like that.

What the last three months look like is a product going out of stock or being delisted, with a price raised on the way out, or a seller clearing the last units at a higher price with no promotion behind it. In either case the units did not fall because the price rose; both moved because the product was ending. The rows cannot distinguish demand from supply here, and a slope fitted to them is a description of the product's life, not of its buyers.

This is the general lesson and it is why the track fits the panel and not the product: one product's thirteen months carry every accident of its life, and a slope has no way of knowing which dots are accidents. Two hundred products' months average the accidents out, and what is left is the part they share.

Self-studyLogs, and why the slope is a percentage change2 min▶

Elasticity is a ratio of percentage changes, and the derivative of log x is one over x, so a small change in log x is a percentage change in x. A straight line in logs, log units = a + b log price, therefore says: a one percent change in price goes with a b percent change in units, at every price. That is the constant-elasticity demand curve, and b is the elasticity.

Two consequences worth holding. First, the fit is only as good as the assumption that the elasticity is the same at R$ 70 and R$ 100, which the bench's session tests by fitting on subsets. Second, zeros cannot be logged: a month with no sales is not in the panel at all, which is one reason the eligibility rule asks for six months with a sale rather than six months.

Build-along

Items to a fitted slope, on 110,197 real rows

★ 22 min · everyone builds

Seven steps in pandas and numpy on the public Olist CSVs. Every number printed below is what the code produces; if yours differs, the difference is one of the three decisions and you should be able to name which. Get the CSVs from Kaggle (a free account, or kagglehub.dataset_download("olistbr/brazilian-ecommerce") fetches them with no login).

SEVEN STATIONS 1 · load three CSVs 112,650 items 2 · filter delivered, before snapshot: 110,197 3 · join category, month from timestamp 4 · group product x month units, mean price 5 · three sieves 30 items, 3 prices, 6 months: 203 6 · fit log-log, per product: 203 7 · read one aca2eb7d, 13 rows slope -10.17 Stations 1 to 4 are the same in every panel build. Station 5 is the door. Station 6 is where session 2 changes the unit from the product to the panel. Save the panel with the snapshot date in the filename. Sessions 2 and 10 fit it again and the fits only compare if the rows are the same rows.
🔍 Click to zoom - the amber station is the only one that changes what the answer is about; the rest change how many rows it rests on

Load three tables. Items carry the price, orders carry the status and the timestamp, products carry the category. The translation file turns Portuguese category names into English.

★ Step 1 · loadimport numpy as np import pandas as pd D = "data/" # the Kaggle CSVs items = pd.read_csv(D + "olist_order_items_dataset.csv") orders = pd.read_csv(D + "olist_orders_dataset.csv", parse_dates=["order_purchase_timestamp"]) products = pd.read_csv(D + "olist_products_dataset.csv") names = pd.read_csv(D + "product_category_name_translation.csv") print(len(items), len(orders), len(products)) # 112650 99441 32951

Decision 1: which rows. Delivered, purchased before the snapshot. The snapshot is a literal with its reason beside it, never a computed maximum.

★ Step 2 · filter# last complete month is August 2018; Sep-Oct hold 20 undelivered stragglers SNAPSHOT = pd.Timestamp("2018-09-01") x = items.merge(orders[["order_id", "order_status", "order_purchase_timestamp"]], on="order_id") x = x[(x["order_status"] == "delivered") & (x["order_purchase_timestamp"] < SNAPSHOT)] print(len(x), x["product_id"].nunique()) # 110197 32216

Decision 2 and the joins. The price is the seller's price; freight stays in a column of its own for the session 2 check. The month comes from the purchase timestamp.

★ Step 3 · join category and monthx = x.merge(products[["product_id", "product_category_name"]], on="product_id", how="left") x = x.merge(names, on="product_category_name", how="left") x["cat"] = x["product_category_name_english"].fillna("unknown") x["month"] = x["order_purchase_timestamp"].dt.to_period("M") # price = the seller's listed item price; freight_value is the buyer's, kept separately

Decision 3: the grain. One row per product per month. Units is a count of item rows; price is the mean item price that month.

★ Step 4 · the panelpanel = (x.groupby(["product_id", "month"]) .agg(units=("order_id", "size"), price=("price", "mean"), freight=("freight_value", "mean"), cat=("cat", "first")) .reset_index()) print(len(panel)) # 60881 product-months before the sieves

The three sieves. Written as constants so that changing one is an edit, not an archaeology. Print the count each sieve leaves; the three numbers are the first thing to say about the panel.

★ Step 5 · eligibilityMIN_ITEMS, MIN_MONTHS, MIN_PRICES = 30, 6, 3 g = x.groupby("product_id").agg(n=("order_id", "size"), months=("month", "nunique"), prices=("price", "nunique")) print((g["n"] >= MIN_ITEMS).sum()) # 376 print(((g["n"] >= MIN_ITEMS) & (g["prices"] >= MIN_PRICES)).sum()) # 226 keep = g[(g["n"] >= MIN_ITEMS) & (g["months"] >= MIN_MONTHS) & (g["prices"] >= MIN_PRICES)].index print(len(keep)) # 203 panel = panel[panel["product_id"].isin(keep)].copy() panel["lp"] = np.log(panel["price"]) panel["lq"] = np.log(panel["units"]) print(len(panel), panel["month"].nunique()) # 2216 21 print(round(g.loc[keep, "n"].sum() / g["n"].sum(), 4)) # 0.1493

Fit one line per product. The slope of log units on log price is the elasticity. Skip a product whose price never varied within the panel (there are none among the 203, but the guard costs nothing).

★ Step 6 · 203 slopesdef slope(lp, lq): return np.polyfit(lp, lq, 1)[0] per = (panel.groupby("product_id") .apply(lambda s: slope(s["lp"].values, s["lq"].values) if s["lp"].std() > 0 else np.nan) .dropna()) print(len(per)) # 203 print(round(per.median(), 2)) # -2.03 print(round((per < 0).mean(), 3)) # 0.700 print(round((per < -1).mean(), 3)) # 0.596 print(round((per.abs() > 5).mean(), 3)) # 0.335 print(per.quantile([.25, .75]).round(2).tolist()) # [-5.09, 0.7]

Read the one that sold most. Thirteen rows, a cut, a rise, a collapse, and a slope of -10.17. Look at the rows before believing the slope; that is the whole habit.

★ Step 7 · the headline producttop = g.loc[keep, "n"].idxmax() rows = panel[panel["product_id"] == top][["month", "units", "price"]] print(top[:8], int(g.loc[top, "n"])) # aca2eb7d 520 print(rows.to_string(index=False)) # month units price # 2017-07 5 75.00 # 2017-08 13 75.00 # ... # 2018-01 121 69.98 # ... # 2018-06 2 89.90 # 2018-08 3 103.23 print(round(per[top], 2)) # -10.17 panel.to_csv("panel_2018-09-01.csv", index=False) # the date in the filename, always

Keep the three sieves as constants and the counts as comments. When a colleague gets 190 products instead of 203, the diff between your script and theirs is the answer, and it takes a minute. Without the constants and the comments it takes an afternoon and ends in an argument about whose panel is "right".

Homework

Before deep-dive session 2

30 min
  1. Run the seven steps on Olist, or on your own item-level sales table. Write the three decisions as comments at the top before any code below them.
  2. Change one sieve (30 items to 20, or 6 months to 4) and re-run. Write down the new product count and the new median slope. That pair of numbers is the cost of the threshold.
  3. Find your own headline product, the one with the most rows, and read its rows before its slope. Write one sentence on whether its slope is about buyers or about its life.
  4. Bring the panel. Session 2 fits it five ways and the five answers span a factor of sixteen.
Check yourself

Three questions

Clear all three and deep-dive session 1 gets stamped in your passport.

1 · Why is the panel built on item rows with the seller's listed price, and not on the price the buyer paid including freight?

Freight stays in its own column, and the freight-inclusive fit is run as a check in session 2, where it moves the within-product answer by about 0.1. The decision is about what question is being asked, not about the size of the number.

2 · The eligibility rule leaves 203 of 32,216 products. What is each of the three sieves for?

The thresholds are choices, written as constants, and the honest sentence says "for the products that can be read", not "for the marketplace". The 203 carry 14.9 percent of all items sold.

3 · The best-selling eligible product's own slope is -10.17. What is the honest reading?

A third of the 203 per-product slopes are past 5 in absolute value, which no demand curve for these goods produces. The panel of 2,216 rows, fitted as one line with 203 intercepts, is the unit that averages the accidents out; that is session 2.

Sources covered

What this session teaches, and where it came from

✓Price elasticity of demand as the slope of a log-log lineStandard economics, no single source; the constant-elasticity form is the one fitted here
✓The panel: 112,650 items, 110,197 delivered before the snapshot, 32,216 products, 2,216 rows for 203 products over 21 monthsComputed from the Brazilian E-Commerce Public Dataset by Olist (Kaggle, CC BY-NC-SA 4.0) for these pages; the snapshot rule is the one the RFM course derived from the same export
✓The three sieves: 376, 226, 203; 14.9 percent of items; median price range 1.294Computed; thresholds are stated choices
✓Per-product slopes: median -2.03, 70.0 percent negative, 59.6 percent below -1, 33.5 percent past 5 in absolute value, IQR -5.09 to +0.70; the headline product's 13 rows and slope -10.17Computed; the reading of the last three months as a stock-out or delisting is an interpretation and the page says so
◐Fixed effects, month effects, the placeboNamed as session 2; not taught here

Every fact on this page, and its verification tier, is recorded in the course's source map.

Deep-dive session 1 cheat sheet · pin this

One row = one unit at one priceItems, not orders. Delivered, before the snapshot. 110,197 of 112,650.
Price = the seller's numberFreight is the buyer's distance. Keep it in its own column for the check.
Product x monthUnits = item count, price = mean that month. The grain a seller reprices at.
Three sieves, three floors30 items (data), 3 prices (economics), 6 months (identification). 32,216 to 203.
Slope of log on log = elasticity-2 means one percent up in price, two percent down in units.
One product is the wrong unit203 slopes, a third past 5. The -10.17 is a product's life, not its buyers.